You've already forked pie-time
Initial public release of PieTime!
\o/
This commit is contained in:
12
pie_time/__init__.py
Normal file
12
pie_time/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__title__ = 'pie_time'
|
||||
__version__ = '1.0'
|
||||
__author__ = u'Tomek Wójcik'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = (
|
||||
u'Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>'
|
||||
)
|
||||
|
||||
from .application import PieTime
|
||||
from .card import AbstractCard
|
||||
511
pie_time/application.py
Normal file
511
pie_time/application.py
Normal file
@@ -0,0 +1,511 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
pie_time.application
|
||||
====================
|
||||
|
||||
This module implements the PieTime application.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import argparse
|
||||
import imp
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pygame
|
||||
|
||||
from pie_time import __copyright__ as copyright, __version__ as version
|
||||
|
||||
RET_OK = 0
|
||||
RET_ERROR = 99
|
||||
|
||||
MOTD_PICLOCK_BANNER = u"PieTime v%s by Tomek Wójcik" % (
|
||||
version
|
||||
)
|
||||
MOTD_LICENSE_BANNER = u"Released under the MIT license"
|
||||
|
||||
EVENT_QUIT = 0
|
||||
EVENT_CLICK_TO_UNBLANK = 1
|
||||
EVENT_CLICK_TO_PREV_CARD = 2
|
||||
EVENT_CLICK_TO_NEXT_CARD = 3
|
||||
|
||||
|
||||
class Quit(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PieTimeEvent(object):
|
||||
def __init__(self, app, event):
|
||||
self.event = event
|
||||
self.app = app
|
||||
|
||||
def is_quit(self):
|
||||
return (self.event.type == pygame.QUIT)
|
||||
|
||||
def is_key_quit(self):
|
||||
return (
|
||||
self.event.type == pygame.KEYDOWN
|
||||
and self.event.key == self.app.KEY_QUIT
|
||||
)
|
||||
|
||||
def is_click_to_unblank(self):
|
||||
return (
|
||||
self.event.type == pygame.MOUSEBUTTONDOWN
|
||||
and self.app._click_to_unblank_interval is not None
|
||||
and self.app._is_blanked is True
|
||||
)
|
||||
|
||||
def is_click_to_prev_card(self):
|
||||
return (
|
||||
self.event.type == pygame.MOUSEBUTTONDOWN
|
||||
and self.app._click_to_transition is True
|
||||
and self.app._is_blanked is False
|
||||
and self.app._ctt_region_prev.collidepoint(self.event.pos) == 1
|
||||
)
|
||||
|
||||
def is_click_to_next_card(self):
|
||||
return (
|
||||
self.event.type == pygame.MOUSEBUTTONDOWN
|
||||
and self.app._click_to_transition is True
|
||||
and self.app._is_blanked is False
|
||||
and self.app._ctt_region_next.collidepoint(self.event.pos) == 1
|
||||
)
|
||||
|
||||
|
||||
class PieTime(object):
|
||||
"""
|
||||
The PieTime application.
|
||||
|
||||
:param deck: the deck
|
||||
:param screen_size: tuple of (width, height) to use as the screen size
|
||||
:param fps: number of frames per second to limit rendering to
|
||||
:param blanker_schedule: blanker schedule
|
||||
:param click_to_unblank_interval: time interval for click to unblank
|
||||
:param click_to_transition: boolean defining if click to transition is
|
||||
enabled
|
||||
:param verbose: boolean defining if verbose logging should be on
|
||||
:param log_path: path to log file (if omitted, *stdout* will be used)
|
||||
"""
|
||||
|
||||
#: Default background color
|
||||
BACKGROUND_COLOR = (0, 0, 0)
|
||||
|
||||
#: Blanked screen color
|
||||
BLANK_COLOR = (0, 0, 0)
|
||||
|
||||
#: Default card display duration interval
|
||||
CARD_INTERVAL = 60
|
||||
|
||||
#: Defines key which quits the application
|
||||
KEY_QUIT = pygame.K_ESCAPE
|
||||
|
||||
#: Defines size of click to transition region square
|
||||
CLICK_TO_TRANSITION_REGION_SIZE = 30
|
||||
|
||||
_DEFAULT_OUTPUT_STREAM = sys.stdout
|
||||
|
||||
_STREAM_FACTORY = file
|
||||
|
||||
def __init__(self, deck, screen_size=(320, 240), fps=20,
|
||||
blanker_schedule=None, click_to_unblank_interval=None,
|
||||
click_to_transition=True, verbose=False, log_path=None):
|
||||
self._deck = deck
|
||||
|
||||
#: The screen surface
|
||||
self.screen = None
|
||||
#: The screen size tuple
|
||||
self.screen_size = screen_size
|
||||
#: List of events captured in this frame
|
||||
self.events = []
|
||||
#: Path to log file. If `None`, *stdout* will be used for logging.
|
||||
self.log_path = log_path
|
||||
|
||||
self._fps = fps
|
||||
self._verbose = verbose
|
||||
self._blanker_schedule = blanker_schedule
|
||||
self._click_to_unblank_interval = click_to_unblank_interval
|
||||
self._click_to_transition = click_to_transition
|
||||
self._clock = None
|
||||
self._cards = []
|
||||
self._is_blanked = False
|
||||
self._current_card_idx = None
|
||||
self._current_card_time = None
|
||||
self._should_quit = False
|
||||
self._internal_events = set()
|
||||
self._ctu_timer = None
|
||||
self._output_stream = None
|
||||
|
||||
self._ctt_region_prev = pygame.Rect(
|
||||
0,
|
||||
self.screen_size[1] - self.CLICK_TO_TRANSITION_REGION_SIZE,
|
||||
self.CLICK_TO_TRANSITION_REGION_SIZE,
|
||||
self.CLICK_TO_TRANSITION_REGION_SIZE
|
||||
)
|
||||
|
||||
self._ctt_region_next = pygame.Rect(
|
||||
self.screen_size[0] - self.CLICK_TO_TRANSITION_REGION_SIZE,
|
||||
self.screen_size[1] - self.CLICK_TO_TRANSITION_REGION_SIZE,
|
||||
self.CLICK_TO_TRANSITION_REGION_SIZE,
|
||||
self.CLICK_TO_TRANSITION_REGION_SIZE
|
||||
)
|
||||
|
||||
def _should_blank(self, now=None):
|
||||
if self._has_click_to_unblank_event() or self._ctu_timer is not None:
|
||||
if self._is_blanked is False and self._ctu_timer is None:
|
||||
self._ctu_timer = None
|
||||
return False
|
||||
|
||||
if self._click_to_unblank_interval is not None:
|
||||
if self._ctu_timer is None:
|
||||
self._ctu_timer = self._click_to_unblank_interval
|
||||
return False
|
||||
|
||||
self._ctu_timer -= self._clock.get_time() / 1000.0
|
||||
if self._ctu_timer <= 0:
|
||||
self._ctu_timer = None
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if self._blanker_schedule:
|
||||
delta_blanker_start, delta_blanker_end = self._blanker_schedule
|
||||
|
||||
if now is None:
|
||||
now = datetime.datetime.now()
|
||||
|
||||
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
blanker_start = midnight + delta_blanker_start
|
||||
blanker_end = midnight + delta_blanker_end
|
||||
|
||||
if blanker_start > blanker_end:
|
||||
if now.hour < 12:
|
||||
blanker_start -= datetime.timedelta(days=1)
|
||||
else:
|
||||
blanker_end += datetime.timedelta(days=1)
|
||||
|
||||
if now >= blanker_start and now < blanker_end:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _blank(self):
|
||||
if self._is_blanked is False:
|
||||
self.logger.debug('Blanking the screen!')
|
||||
self.will_blank()
|
||||
|
||||
self._is_blanked = True
|
||||
self.screen.fill(self.BLANK_COLOR)
|
||||
|
||||
def _unblank(self):
|
||||
if self._is_blanked:
|
||||
self.logger.debug('Unblanking the screen!')
|
||||
self.will_unblank()
|
||||
|
||||
self._is_blanked = False
|
||||
self._current_card_idx = 0
|
||||
self._current_card_time = 0
|
||||
|
||||
self._cards[self._current_card_idx][0].show()
|
||||
|
||||
def _transition_cards(self, direction=1, force=False):
|
||||
if self._current_card_idx is None and force is False:
|
||||
self._current_card_idx = 0
|
||||
self._current_card_time = 0
|
||||
self._cards[self._current_card_idx][0].show()
|
||||
elif len(self._cards) > 1:
|
||||
self._current_card_time += self._clock.get_time() / 1000.0
|
||||
card_interval = self._cards[self._current_card_idx][1]
|
||||
if self._current_card_time >= card_interval or force is True:
|
||||
new_card_idx = self._current_card_idx + direction
|
||||
if new_card_idx >= len(self._cards):
|
||||
new_card_idx = 0
|
||||
elif new_card_idx < 0:
|
||||
new_card_idx = len(self._cards) - 1
|
||||
|
||||
self.logger.debug('Card transition: %d -> %d' % (
|
||||
self._current_card_idx, new_card_idx
|
||||
))
|
||||
|
||||
self._cards[self._current_card_idx][0].hide()
|
||||
|
||||
self._current_card_idx = new_card_idx
|
||||
self._current_card_time = 0
|
||||
|
||||
self._cards[self._current_card_idx][0].show()
|
||||
|
||||
def _get_events(self):
|
||||
self._internal_events = set()
|
||||
self.events = []
|
||||
for event in pygame.event.get():
|
||||
pie_time_event = PieTimeEvent(self, event)
|
||||
if pie_time_event.is_quit():
|
||||
self.logger.debug('_get_events: QUIT')
|
||||
self._internal_events.add(EVENT_QUIT)
|
||||
elif pie_time_event.is_key_quit():
|
||||
self.logger.debug('_get_events: KEY_QUIT')
|
||||
self._internal_events.add(EVENT_QUIT)
|
||||
elif pie_time_event.is_click_to_unblank():
|
||||
self.logger.debug('_get_events: CLICK_TO_UNBLANK')
|
||||
self._internal_events.add(EVENT_CLICK_TO_UNBLANK)
|
||||
elif pie_time_event.is_click_to_prev_card():
|
||||
self.logger.debug('_get_events: CLICK_TO_PREV_CARD')
|
||||
self._internal_events.add(EVENT_CLICK_TO_PREV_CARD)
|
||||
elif pie_time_event.is_click_to_next_card():
|
||||
self.logger.debug('_get_events: CLICK_TO_NEXT_CARD')
|
||||
self._internal_events.add(EVENT_CLICK_TO_NEXT_CARD)
|
||||
else:
|
||||
self.events.append(event)
|
||||
|
||||
def _has_quit_event(self):
|
||||
return (EVENT_QUIT in self._internal_events)
|
||||
|
||||
def _has_click_to_unblank_event(self):
|
||||
return (EVENT_CLICK_TO_UNBLANK in self._internal_events)
|
||||
|
||||
def _start_clock(self):
|
||||
self._clock = pygame.time.Clock()
|
||||
|
||||
def _setup_output_stream(self):
|
||||
if self.log_path is None:
|
||||
self._output_stream = self._DEFAULT_OUTPUT_STREAM
|
||||
else:
|
||||
self._output_stream = self._STREAM_FACTORY(self.log_path, 'a')
|
||||
|
||||
def _setup_logging(self):
|
||||
logger = logging.getLogger('PieTime')
|
||||
requests_logger = logging.getLogger('requests')
|
||||
|
||||
if self._verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
requests_logger.setLevel(logging.WARNING)
|
||||
|
||||
handler = logging.StreamHandler(self._output_stream)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s PieTime: %(levelname)s: %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
for requests_handler in requests_logger.handlers:
|
||||
requests_logger.removeHandler(requests_handler)
|
||||
|
||||
requests_logger.addHandler(handler)
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
"""The application-wide :py:class:`logging.Logger` object."""
|
||||
if not hasattr(self, '_logger'):
|
||||
self._logger = logging.getLogger('PieTime')
|
||||
|
||||
return self._logger
|
||||
|
||||
def init_pygame(self):
|
||||
"""Initializes PyGame and the internal clock."""
|
||||
self.logger.debug('Initializing PyGame.')
|
||||
pygame.init()
|
||||
pygame.mouse.set_visible(False)
|
||||
|
||||
def quit_pygame(self):
|
||||
"""Quits PyGame."""
|
||||
self.logger.debug('Quitting PyGame.')
|
||||
pygame.quit()
|
||||
|
||||
self._clock = None
|
||||
|
||||
def init_cards(self):
|
||||
"""
|
||||
Initializes the cards.
|
||||
|
||||
Initialization of a card consits of the following steps:
|
||||
|
||||
* Creating an instance of the card class,
|
||||
* Binding the card with the application
|
||||
(:py:meth:`pie_time.AbstractCard.set_app`),
|
||||
* Setting the card's settings
|
||||
(:py:meth:`pie_time.AbstractCard.set_settings`),
|
||||
* Initializing the card (:py:meth:`pie_time.AbstractCard.initialize`).
|
||||
"""
|
||||
self.logger.debug('Initializing cards.')
|
||||
for i in xrange(0, len(self._deck)):
|
||||
card_def = self._deck[i]
|
||||
|
||||
klass = None
|
||||
interval = self.CARD_INTERVAL
|
||||
settings = {}
|
||||
|
||||
if not isinstance(card_def, tuple):
|
||||
klass = card_def
|
||||
elif len(card_def) == 2:
|
||||
klass, interval = card_def
|
||||
elif len(card_def) == 3:
|
||||
klass, interval, settings = card_def
|
||||
|
||||
if klass is not None:
|
||||
card = klass()
|
||||
card.set_app(self)
|
||||
card.set_settings(settings)
|
||||
card.initialize()
|
||||
|
||||
self._cards.append((card, interval))
|
||||
else:
|
||||
self.logger.warning('Invalid deck entry at index: %d' % i)
|
||||
|
||||
def destroy_cards(self):
|
||||
"""
|
||||
Destroys the cards.
|
||||
|
||||
Calls the :py:meth:`pie_time.AbstractCard.quit` of each card.
|
||||
"""
|
||||
self.logger.debug('Destroying cards.')
|
||||
while len(self._cards) > 0:
|
||||
card, _ = self._cards.pop()
|
||||
|
||||
try:
|
||||
card.quit()
|
||||
except:
|
||||
self.logger.error('ERROR!', exc_info=True)
|
||||
|
||||
def get_screen(self):
|
||||
"""Creates and returns the screen screen surface."""
|
||||
self.logger.debug('Creating screen.')
|
||||
return pygame.display.set_mode(self.screen_size)
|
||||
|
||||
def fill_screen(self):
|
||||
"""
|
||||
Fills the screen surface with color defined in
|
||||
:py:attr:`pie_time.PieTime.BACKGROUND_COLOR`.
|
||||
"""
|
||||
self.screen.fill(self.BACKGROUND_COLOR)
|
||||
|
||||
def run(self, standalone=True):
|
||||
"""
|
||||
Runs the application.
|
||||
|
||||
This method contains the app's main loop and it never returns. Upon
|
||||
quitting, this method will call the :py:func:`sys.exit` function with
|
||||
the status code (``99`` if an unhandled exception occurred, ``0``
|
||||
otherwise).
|
||||
|
||||
The application will quit under one of the following conditions:
|
||||
|
||||
* An unhandled exception reached this method,
|
||||
* PyGame requested to quit (e.g. due to closing the window),
|
||||
* Some other code called the :py:meth:`pie_time.PieTime.quit` method on
|
||||
the application.
|
||||
|
||||
Before quitting the :py:meth:`pie_time.PieTime.destroy_cards` and
|
||||
:py:meth:`pie_time.PieTime.quit_pygame` methods will be called to clean
|
||||
up.
|
||||
"""
|
||||
result = RET_OK
|
||||
|
||||
self._setup_output_stream()
|
||||
self._setup_logging()
|
||||
|
||||
try:
|
||||
self.logger.info(MOTD_PICLOCK_BANNER)
|
||||
self.logger.info(copyright)
|
||||
self.logger.info(MOTD_LICENSE_BANNER)
|
||||
self.logger.debug('My PID = %d' % os.getpid())
|
||||
|
||||
self.init_pygame()
|
||||
self.screen = self.get_screen()
|
||||
|
||||
self.init_cards()
|
||||
|
||||
self._start_clock()
|
||||
|
||||
while True:
|
||||
self._get_events()
|
||||
|
||||
if self._should_quit or self._has_quit_event():
|
||||
raise Quit()
|
||||
|
||||
if not self._should_blank():
|
||||
self._unblank()
|
||||
|
||||
if EVENT_CLICK_TO_PREV_CARD in self._internal_events:
|
||||
self._transition_cards(direction=-1, force=True)
|
||||
elif EVENT_CLICK_TO_NEXT_CARD in self._internal_events:
|
||||
self._transition_cards(direction=1, force=True)
|
||||
else:
|
||||
self._transition_cards()
|
||||
|
||||
card = self._cards[self._current_card_idx][0]
|
||||
|
||||
card.tick()
|
||||
|
||||
self.fill_screen()
|
||||
self.screen.blit(
|
||||
card.surface, (0, 0, card.width, card.height)
|
||||
)
|
||||
else:
|
||||
self._blank()
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
self._clock.tick(self._fps)
|
||||
except Exception as exc:
|
||||
if not isinstance(exc, Quit):
|
||||
self.logger.error('ERROR!', exc_info=True)
|
||||
result = RET_ERROR
|
||||
finally:
|
||||
self.destroy_cards()
|
||||
self.quit_pygame()
|
||||
|
||||
if standalone:
|
||||
sys.exit(result)
|
||||
else:
|
||||
return result
|
||||
|
||||
def quit(self):
|
||||
"""Tells the application to quit."""
|
||||
self._should_quit = True
|
||||
|
||||
def will_blank(self):
|
||||
"""
|
||||
Called before blanking the screen.
|
||||
|
||||
This method can be used to perform additional operations before
|
||||
blanking the screen.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def will_unblank(self):
|
||||
"""
|
||||
Called before unblanking the screen.
|
||||
|
||||
This method can be used to perform additional operations before
|
||||
unblanking the screen.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
|
||||
pass
|
||||
195
pie_time/card.py
Normal file
195
pie_time/card.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
pie_time.card
|
||||
=============
|
||||
|
||||
This module contains the AbstractCard class.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pygame
|
||||
|
||||
|
||||
class AbstractCard(object):
|
||||
"""
|
||||
The abstract card class.
|
||||
|
||||
All the custom cards **must** inherit from this class.
|
||||
|
||||
**Application binding and settings.**
|
||||
|
||||
The application calls the card's :py:meth:`pie_time.AbstractCard.set_app`
|
||||
and :py:meth:`pie_time.AbstractCard.set_settings` methods during
|
||||
initialization (before calling the
|
||||
:py:meth:`pie_time.AbstractCard.initialize` method).
|
||||
|
||||
The application reference is stored in ``_app`` attribute.
|
||||
|
||||
The settings dictionary is stored in ``_settings`` attribute and defaults
|
||||
to an empty dictionary.
|
||||
|
||||
**Drawing**
|
||||
|
||||
All the drawing on the card's surface should be done in the
|
||||
:py:meth:`pie_time.AbstractCard.tick` method. The method's implementation
|
||||
should be as fast as possible to avoid throttling the FPS down.
|
||||
|
||||
**Resources**
|
||||
|
||||
The :py:meth:`pie_time.AbstractCard.path_for_resource` method can be used
|
||||
to get an absolute path to a resource file. The card's resource folder
|
||||
should be placed along with the module containing the card's class.
|
||||
|
||||
Name of the resource folder can be customized by overriding the
|
||||
:py:attr:`pie_time.AbstractCard.RESOURCE_FOLDER` attribute.
|
||||
"""
|
||||
#: Name of the folder containing the resources
|
||||
RESOURCE_FOLDER = 'resources'
|
||||
|
||||
def __init__(self):
|
||||
self._app = None
|
||||
self._settings = {}
|
||||
self._surface = None
|
||||
|
||||
def set_app(self, app):
|
||||
"""Binds the card with the *app*."""
|
||||
self._app = app
|
||||
|
||||
def set_settings(self, settings):
|
||||
"""Sets *settings* as the card's settings."""
|
||||
self._settings = settings
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""The card's surface width. Defaults to the app screen's width."""
|
||||
return self._app.screen_size[0]
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""The card's surface height. Defaults to the app screen's height."""
|
||||
return self._app.screen_size[1]
|
||||
|
||||
@property
|
||||
def surface(self):
|
||||
"""
|
||||
The cards surface. The surface width and height are defined by the
|
||||
respective properties of the class.
|
||||
"""
|
||||
if self._surface is None:
|
||||
self._surface = pygame.surface.Surface((self.width, self.height))
|
||||
|
||||
return self._surface
|
||||
|
||||
@property
|
||||
def background_color(self):
|
||||
"""
|
||||
The background color. Defaults to
|
||||
:py:attr:`pie_time.PieTime.BACKGROUND_COLOR`.
|
||||
"""
|
||||
return self._settings.get(
|
||||
'background_color', self._app.BACKGROUND_COLOR
|
||||
)
|
||||
|
||||
def path_for_resource(self, resource, folder=None):
|
||||
"""
|
||||
Returns an absolute path for *resource*. The optional *folder*
|
||||
keyword argument allows specifying a subpath.
|
||||
"""
|
||||
_subpath = ''
|
||||
if folder:
|
||||
_subpath = folder
|
||||
|
||||
module_path = sys.modules[self.__module__].__file__
|
||||
|
||||
return os.path.join(
|
||||
os.path.abspath(os.path.dirname(module_path)),
|
||||
self.RESOURCE_FOLDER, _subpath, resource
|
||||
)
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Initializes the card.
|
||||
|
||||
The application calls this method right after creating an instance of
|
||||
the class.
|
||||
|
||||
This method can be used to perform additional initialization on the
|
||||
card, e.g. loading resources, setting the initial state etc.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def quit(self):
|
||||
"""
|
||||
Initializes the card.
|
||||
|
||||
This method can be used to perform additional cleanup on the
|
||||
card, e.g. stop threads, free resources etc.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Shows the card.
|
||||
|
||||
The application calls this method each time the card becomes the
|
||||
current card.
|
||||
|
||||
This method can be used to reset initial state, e.g. sprite positions.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hides the card.
|
||||
|
||||
The application calls this method each time the card resignes the
|
||||
current card.
|
||||
|
||||
This method can be used to e.g. stop threads which aren't supposed to
|
||||
be running when the card isn't being displayed.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def tick(self):
|
||||
"""
|
||||
Ticks the card.
|
||||
|
||||
The application calls this method on the current card in every main
|
||||
loop iteration.
|
||||
|
||||
This method should be used to perform drawing and other operations
|
||||
needed to properly display the card on screen.
|
||||
|
||||
Subclasses **must** override this method.
|
||||
"""
|
||||
raise NotImplementedError('TODO')
|
||||
3
pie_time/cards/__init__.py
Normal file
3
pie_time/cards/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .clock import ClockCard
|
||||
from .picture import PictureCard
|
||||
from .weather import WeatherCard
|
||||
154
pie_time/cards/clock.py
Normal file
154
pie_time/cards/clock.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
pie_time.cards.clock
|
||||
====================
|
||||
|
||||
This module containse the ClockCard class.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import pygame
|
||||
|
||||
from pie_time.card import AbstractCard
|
||||
|
||||
|
||||
class ClockCard(AbstractCard):
|
||||
"""
|
||||
The clock card.
|
||||
|
||||
This card displays a digital clock and date.
|
||||
|
||||
**Settings dictionary keys**:
|
||||
|
||||
* **time_format** (*string*) - time format string (*strftime()*
|
||||
compatible). Defaults to :py:attr:`pie_time.cards.ClockCard.TIME_FORMAT`
|
||||
* **time_blink** (*boolean*) - if set to ``True`` the semicolons will
|
||||
blink. Defaults to ``True``.
|
||||
* **time_color** (*tuple*) - time text color. Defaults to
|
||||
:py:attr:`pie_time.cards.ClockCard.GREEN`
|
||||
* **date_format** (*string*) - date format string (*strftime()*
|
||||
compatible). Defaults to :py:attr:`pie_time.cards.ClockCard.DATE_FORMAT`
|
||||
* **date_color** (*tuple*) - date text color. Defaults to
|
||||
:py:attr:`pie_time.cards.ClockCard.GREEN`
|
||||
"""
|
||||
|
||||
#: Green color for text
|
||||
GREEN = (96, 253, 108)
|
||||
|
||||
#: Default time format
|
||||
TIME_FORMAT = '%I:%M %p'
|
||||
|
||||
#: Default date format
|
||||
DATE_FORMAT = '%a, %d %b %Y'
|
||||
|
||||
def initialize(self):
|
||||
self._time_font = pygame.font.Font(
|
||||
self.path_for_resource('PTM55FT.ttf'), 63
|
||||
)
|
||||
|
||||
self._date_font = pygame.font.Font(
|
||||
self.path_for_resource('opensans-light.ttf'), 36
|
||||
)
|
||||
|
||||
self._now = None
|
||||
self._current_interval = 0
|
||||
|
||||
def _render_time(self, now):
|
||||
time_format = self._settings.get('time_format', self.TIME_FORMAT)
|
||||
|
||||
if self._settings.get('time_blink', True) and now.second % 2 == 1:
|
||||
time_format = time_format.replace(':', ' ')
|
||||
|
||||
current_time = now.strftime(time_format)
|
||||
|
||||
text = self._time_font.render(
|
||||
current_time, True, self._settings.get('time_color', self.GREEN)
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
def _render_date(self, now):
|
||||
date_format = self._settings.get('date_format', self.DATE_FORMAT)
|
||||
|
||||
current_date = now.strftime(date_format)
|
||||
|
||||
text = self._date_font.render(
|
||||
current_date, True, self._settings.get('date_color', self.GREEN)
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
def _update_now(self):
|
||||
if self._now is None:
|
||||
self._now = datetime.datetime.now()
|
||||
self._current_interval = 0
|
||||
|
||||
return True
|
||||
else:
|
||||
self._current_interval += self._app._clock.get_time()
|
||||
|
||||
if self._current_interval >= 1000:
|
||||
self._now = datetime.datetime.now()
|
||||
self._current_interval = self._current_interval - 1000
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def show(self):
|
||||
self._now = None
|
||||
|
||||
def tick(self):
|
||||
now_updated = self._update_now()
|
||||
|
||||
if now_updated:
|
||||
time_text = self._render_time(self._now)
|
||||
date_text = self._render_date(self._now)
|
||||
|
||||
time_text_size = time_text.get_size()
|
||||
date_text_size = date_text.get_size()
|
||||
|
||||
time_text_origin_y = (
|
||||
(self.height - time_text_size[1] - date_text_size[1]) / 2.0
|
||||
)
|
||||
|
||||
time_text_rect = (
|
||||
(self.width - time_text_size[0]) / 2.0,
|
||||
time_text_origin_y,
|
||||
time_text_size[0],
|
||||
time_text_size[1]
|
||||
)
|
||||
|
||||
date_text_rect = (
|
||||
(self.width - date_text_size[0]) / 2.0,
|
||||
time_text_origin_y + time_text_size[1],
|
||||
date_text_size[0],
|
||||
date_text_size[1]
|
||||
)
|
||||
|
||||
self.surface.fill(self.background_color)
|
||||
|
||||
self.surface.blit(time_text, time_text_rect)
|
||||
self.surface.blit(date_text, date_text_rect)
|
||||
140
pie_time/cards/picture.py
Normal file
140
pie_time/cards/picture.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
pie_time.cards.picture
|
||||
======================
|
||||
|
||||
This module containse the PictureCard class.
|
||||
"""
|
||||
|
||||
import cStringIO
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
import pygame
|
||||
import requests
|
||||
|
||||
from pie_time.card import AbstractCard
|
||||
|
||||
|
||||
class PictureCard(AbstractCard):
|
||||
"""
|
||||
The picture card.
|
||||
|
||||
This cards displays a picture from list of pre-defined pictures. If more
|
||||
than one picture is defined, it's changed each time the card transitions
|
||||
to current card.
|
||||
|
||||
**Settings dictionary keys**:
|
||||
|
||||
* **urls** (*list*) - **required** list of picture URLs. Currently, only
|
||||
``file://``, ``http://`` and ``https://`` URL schemes are supported.
|
||||
"""
|
||||
|
||||
def initialize(self):
|
||||
self._pictures = []
|
||||
self._current_picture_idx = None
|
||||
self._should_redraw = True
|
||||
|
||||
for url in self._settings['urls']:
|
||||
self._pictures.append(self._load_picture(url))
|
||||
|
||||
def _load_picture(self, url):
|
||||
self._app.logger.debug(
|
||||
'PictureCard: Attempting to load picture: %s' % url
|
||||
)
|
||||
|
||||
parsed_url = urlparse.urlparse(url)
|
||||
|
||||
surface = None
|
||||
try:
|
||||
format = None
|
||||
if parsed_url.scheme == 'file':
|
||||
surface = pygame.image.load(parsed_url.path)
|
||||
|
||||
_, ext = os.path.splitext(parsed_url.path)
|
||||
format = ext.lower()
|
||||
elif parsed_url.scheme.startswith('http'):
|
||||
rsp = requests.get(url)
|
||||
assert rsp.status_code == 200
|
||||
|
||||
format = rsp.headers['Content-Type'].replace('image/', '')
|
||||
|
||||
surface = pygame.image.load(
|
||||
cStringIO.StringIO(rsp.content), 'picture.%s' % format
|
||||
)
|
||||
|
||||
if surface and format:
|
||||
if format.lower().endswith('png'):
|
||||
surface = surface.convert_alpha(self._app.screen)
|
||||
else:
|
||||
surface = surface.convert(self._app.screen)
|
||||
except Exception as exc:
|
||||
self._app.logger.error(
|
||||
'PictureCard: Could not load picture: %s' % url, exc_info=True
|
||||
)
|
||||
|
||||
return surface
|
||||
|
||||
def show(self):
|
||||
if len(self._pictures) == 0:
|
||||
self._current_picture_idx = None
|
||||
elif len(self._pictures) == 1:
|
||||
self._current_picture_idx = 0
|
||||
else:
|
||||
if self._current_picture_idx is None:
|
||||
self._current_picture_idx = 0
|
||||
else:
|
||||
new_picture_idx = self._current_picture_idx + 1
|
||||
if new_picture_idx >= len(self._pictures):
|
||||
new_picture_idx = 0
|
||||
|
||||
self._app.logger.debug(
|
||||
'PictureCard: Picture transition %d -> %d' % (
|
||||
self._current_picture_idx, new_picture_idx
|
||||
)
|
||||
)
|
||||
|
||||
self._current_picture_idx = new_picture_idx
|
||||
|
||||
self._should_redraw = True
|
||||
|
||||
def tick(self):
|
||||
if self._should_redraw:
|
||||
self.surface.fill(self.background_color)
|
||||
|
||||
if self._current_picture_idx is not None:
|
||||
picture = self._pictures[self._current_picture_idx]
|
||||
picture_size = picture.get_size()
|
||||
|
||||
picture_rect = picture.get_rect()
|
||||
if picture_size != self._app.screen_size:
|
||||
picture_rect = (
|
||||
(self.width - picture_size[0]) / 2.0,
|
||||
(self.height - picture_size[1]) / 2.0,
|
||||
picture_size[0], picture_size[1]
|
||||
)
|
||||
|
||||
self.surface.blit(picture, picture_rect)
|
||||
|
||||
self._should_redraw = False
|
||||
BIN
pie_time/cards/resources/PTM55FT.ttf
Executable file
BIN
pie_time/cards/resources/PTM55FT.ttf
Executable file
Binary file not shown.
BIN
pie_time/cards/resources/linea-weather-10.ttf
Normal file
BIN
pie_time/cards/resources/linea-weather-10.ttf
Normal file
Binary file not shown.
BIN
pie_time/cards/resources/opensans-light.ttf
Executable file
BIN
pie_time/cards/resources/opensans-light.ttf
Executable file
Binary file not shown.
286
pie_time/cards/weather.py
Normal file
286
pie_time/cards/weather.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
pie_time.cards.weather
|
||||
======================
|
||||
|
||||
This module containse the WeatherCard class.
|
||||
"""
|
||||
|
||||
from threading import Timer
|
||||
|
||||
import pygame
|
||||
import requests
|
||||
|
||||
from pie_time.card import AbstractCard
|
||||
|
||||
URL_TEMPLATE = (
|
||||
'http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&APPID=%s'
|
||||
)
|
||||
|
||||
|
||||
class WeatherCard(AbstractCard):
|
||||
"""
|
||||
The weather card.
|
||||
|
||||
This cards displays the current weather for a selected city. The weather
|
||||
information is obtained from OpenWeatherMap.
|
||||
|
||||
**Settings dictionary keys**:
|
||||
|
||||
* **api_key** (*string*) - **required** API key.
|
||||
* **city** (*string*) - **required** name of the city.
|
||||
* **units** (*string*) - units name (``metric`` or ``imperial``). Defaults
|
||||
to :py:attr:`pie_time.cards.WeatherCard.UNITS`
|
||||
* **refresh_interval** (*int*) - refresh interval in seconds. Defaults to
|
||||
:py:attr:`pie_time.cards.WeatherCard.REFRESH_INTERVAL`
|
||||
* **city_color** (*tuple*) - city text color. Defaults to
|
||||
:py:attr:`pie_time.cards.WeatherCard.WHITE`
|
||||
* **icon_color** (*tuple*) - icon text color. Defaults to
|
||||
:py:attr:`pie_time.cards.WeatherCard.WHITE`
|
||||
* **temperature_color** (*tuple*) - temperature text color. Defaults to
|
||||
:py:attr:`pie_time.cards.WeatherCard.WHITE`
|
||||
* **conditions_color** (*tuple*) - conditions text color. Defaults to
|
||||
:py:attr:`pie_time.cards.WeatherCard.WHITE`
|
||||
"""
|
||||
|
||||
#: Default units
|
||||
UNITS = 'metric'
|
||||
|
||||
#: Default refresh interval
|
||||
REFRESH_INTERVAL = 600
|
||||
|
||||
#: White color for text
|
||||
WHITE = (255, 255, 255)
|
||||
|
||||
WEATHER_CODE_TO_ICON = {
|
||||
'01d': u'',
|
||||
'01n': u'',
|
||||
'02d': u'',
|
||||
'02n': u'',
|
||||
'03d': u'',
|
||||
'03n': u'',
|
||||
'04d': u'',
|
||||
'04n': u'',
|
||||
'09d': u'',
|
||||
'09n': u'',
|
||||
'10d': u'',
|
||||
'10n': u'',
|
||||
'11d': u'',
|
||||
'11n': u'',
|
||||
'13d': u'',
|
||||
'13n': u'',
|
||||
'50d': u'',
|
||||
'50n': u''
|
||||
}
|
||||
ICON_SPACING = 24
|
||||
|
||||
def initialize(self, refresh=True):
|
||||
assert 'api_key' in self._settings,\
|
||||
'Configuration error: missing API key'
|
||||
assert 'city' in self._settings, 'Configuration error: missing city'
|
||||
|
||||
self._text_font = pygame.font.Font(
|
||||
self.path_for_resource('opensans-light.ttf'), 30
|
||||
)
|
||||
|
||||
self._temp_font = pygame.font.Font(
|
||||
self.path_for_resource('opensans-light.ttf'), 72
|
||||
)
|
||||
|
||||
self._icon_font = pygame.font.Font(
|
||||
self.path_for_resource('linea-weather-10.ttf'), 128
|
||||
)
|
||||
|
||||
self._timer = None
|
||||
self._current_conditions = None
|
||||
self._should_redraw = True
|
||||
|
||||
if refresh:
|
||||
self._refresh_conditions()
|
||||
|
||||
def _refresh_conditions(self):
|
||||
self._app.logger.debug('Refreshing conditions.')
|
||||
self._timer = None
|
||||
|
||||
try:
|
||||
rsp = requests.get(
|
||||
URL_TEMPLATE % (
|
||||
self._settings['city'],
|
||||
self._settings.get('units', self.UNITS),
|
||||
self._settings['api_key']
|
||||
)
|
||||
)
|
||||
|
||||
if rsp.status_code != 200:
|
||||
self._app.logger.error(
|
||||
'WeatherCard: Received HTTP %d' % rsp.status_code
|
||||
)
|
||||
else:
|
||||
try:
|
||||
payload = rsp.json()
|
||||
self._current_conditions = {
|
||||
'conditions': payload['weather'][0]['main'],
|
||||
'icon': payload['weather'][0].get('icon', None),
|
||||
'temperature': payload['main']['temp']
|
||||
}
|
||||
self._should_redraw = True
|
||||
except:
|
||||
self._app.logger.error(
|
||||
'WeatherCard: ERROR!', exc_info=True
|
||||
)
|
||||
except:
|
||||
self._app.logger.error('WeatherCard: ERROR!', exc_info=True)
|
||||
|
||||
self._timer = Timer(
|
||||
self._settings.get('refresh_interval', self.REFRESH_INTERVAL),
|
||||
self._refresh_conditions
|
||||
)
|
||||
self._timer.start()
|
||||
|
||||
def _render_city(self):
|
||||
city_text = self._text_font.render(
|
||||
self._settings['city'], True,
|
||||
self._settings.get('city_color', self.WHITE)
|
||||
)
|
||||
|
||||
return city_text
|
||||
|
||||
def _render_conditions(self):
|
||||
conditions_text = self._text_font.render(
|
||||
self._current_conditions['conditions'].capitalize(),
|
||||
True, self._settings.get('conditions_color', self.WHITE)
|
||||
)
|
||||
|
||||
return conditions_text
|
||||
|
||||
def _render_icon(self):
|
||||
icon = self._current_conditions['icon']
|
||||
weather_icon = None
|
||||
|
||||
if icon in self.WEATHER_CODE_TO_ICON:
|
||||
weather_icon = self._icon_font.render(
|
||||
self.WEATHER_CODE_TO_ICON[icon],
|
||||
True, self._settings.get('icon_color', self.WHITE)
|
||||
)
|
||||
|
||||
return weather_icon
|
||||
|
||||
def _render_temperature(self):
|
||||
temp_text = self._temp_font.render(
|
||||
u'%d°' % self._current_conditions['temperature'],
|
||||
True,
|
||||
self._settings.get('temperature_color', self.WHITE)
|
||||
)
|
||||
|
||||
return temp_text
|
||||
|
||||
def quit(self):
|
||||
if self._timer is not None:
|
||||
self._timer.cancel()
|
||||
|
||||
def tick(self):
|
||||
if self._should_redraw:
|
||||
self.surface.fill(self.background_color)
|
||||
|
||||
city_text = self._render_city()
|
||||
city_text_size = city_text.get_size()
|
||||
city_text_rect = (
|
||||
(self.width - city_text_size[0]) / 2.0,
|
||||
0,
|
||||
city_text_size[0],
|
||||
city_text_size[1]
|
||||
)
|
||||
self.surface.blit(city_text, city_text_rect)
|
||||
|
||||
if self._current_conditions:
|
||||
conditions_text = self._render_conditions()
|
||||
conditions_text_size = conditions_text.get_size()
|
||||
conditions_text_rect = (
|
||||
(self.width - conditions_text_size[0]) / 2.0,
|
||||
self.height - conditions_text_size[1],
|
||||
conditions_text_size[0],
|
||||
conditions_text_size[1]
|
||||
)
|
||||
self.surface.blit(conditions_text, conditions_text_rect)
|
||||
|
||||
icon = self._render_icon()
|
||||
has_icon = (icon is not None)
|
||||
|
||||
temp_text = self._render_temperature()
|
||||
temp_text_size = temp_text.get_size()
|
||||
|
||||
if has_icon:
|
||||
icon_size = icon.get_size()
|
||||
icon_origin_x = (
|
||||
(
|
||||
self.width - (
|
||||
icon_size[0] + self.ICON_SPACING +
|
||||
temp_text_size[0]
|
||||
)
|
||||
) / 2.0
|
||||
)
|
||||
icon_origin_y = (
|
||||
city_text_size[1] + (
|
||||
self.height - conditions_text_size[1] -
|
||||
city_text_size[1] - icon_size[1]
|
||||
) / 2.0
|
||||
)
|
||||
icon_rect = (
|
||||
icon_origin_x,
|
||||
icon_origin_y,
|
||||
icon_size[0],
|
||||
icon_size[1]
|
||||
)
|
||||
|
||||
self.surface.blit(icon, icon_rect)
|
||||
|
||||
temp_text_origin_y = (
|
||||
city_text_size[1] + (
|
||||
self.height - conditions_text_size[1] -
|
||||
city_text_size[1] - temp_text_size[1]
|
||||
) / 2.0
|
||||
)
|
||||
|
||||
if has_icon:
|
||||
temp_text_origin_x = (
|
||||
icon_rect[0] + icon_size[0] +
|
||||
self.ICON_SPACING
|
||||
)
|
||||
temp_text_rect = (
|
||||
temp_text_origin_x,
|
||||
temp_text_origin_y,
|
||||
temp_text_size[0],
|
||||
temp_text_size[1]
|
||||
)
|
||||
else:
|
||||
temp_text_rect = (
|
||||
(self.width - temp_text_size[0]) / 2.0,
|
||||
temp_text_origin_y,
|
||||
temp_text_size[0],
|
||||
temp_text_size[1]
|
||||
)
|
||||
|
||||
self.surface.blit(temp_text, temp_text_rect)
|
||||
|
||||
self._should_redraw = False
|
||||
0
pie_time/scripts/__init__.py
Normal file
0
pie_time/scripts/__init__.py
Normal file
104
pie_time/scripts/pie_time.py
Normal file
104
pie_time/scripts/pie_time.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
import ConfigParser
|
||||
import imp
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
RET_OK = 0
|
||||
RET_NO_ARGS = 1
|
||||
RET_ERROR = 99
|
||||
|
||||
CONFIG_SECTION_PIE_TIME = 'PieTime'
|
||||
CONFIG_SECTION_SDL = 'SDL'
|
||||
|
||||
SDL_DEFAULTS = {
|
||||
'VIDEODRIVER': None
|
||||
}
|
||||
|
||||
def _find_module(name, search_path):
|
||||
import_path = name.split('.', 1)
|
||||
|
||||
mod_f, mod_path, mod_desc = imp.find_module(import_path[0], search_path)
|
||||
if mod_desc[2] == imp.PKG_DIRECTORY:
|
||||
return _find_module(
|
||||
import_path[1], [os.path.abspath(mod_path)]
|
||||
)
|
||||
else:
|
||||
return mod_f, mod_path, mod_desc
|
||||
|
||||
def main():
|
||||
try:
|
||||
config_file_path = sys.argv[1]
|
||||
except IndexError:
|
||||
print 'usage: %s [CONFIG_FILE]' % sys.argv[0]
|
||||
return RET_NO_ARGS
|
||||
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.optionxform = str
|
||||
config.read(config_file_path)
|
||||
|
||||
app_spec = config.get(CONFIG_SECTION_PIE_TIME, 'app_module', True)
|
||||
try:
|
||||
app_module, app_obj = app_spec.split(':')
|
||||
except ValueError:
|
||||
print "%s: failed to find application '%s'" % (
|
||||
sys.argv[0], app_spec
|
||||
)
|
||||
return RET_ERROR
|
||||
|
||||
mod_f = None
|
||||
result = RET_OK
|
||||
try:
|
||||
mod_search_path = [os.getcwd()] + sys.path
|
||||
mod_f, mod_path, mod_desc = _find_module(app_module, mod_search_path)
|
||||
|
||||
mod = imp.load_module(app_module, mod_f, mod_path, mod_desc)
|
||||
app = getattr(mod, app_obj)
|
||||
|
||||
if config.has_option(CONFIG_SECTION_PIE_TIME, 'log_path'):
|
||||
app.log_path = config.get(CONFIG_SECTION_PIE_TIME, 'log_path')
|
||||
|
||||
sdl_config = dict(SDL_DEFAULTS)
|
||||
if config.has_section(CONFIG_SECTION_SDL):
|
||||
sdl_config.update({
|
||||
x[0]: x[1] for x in config.items(CONFIG_SECTION_SDL)
|
||||
})
|
||||
|
||||
for k, v in sdl_config.iteritems():
|
||||
if v:
|
||||
os.environ['SDL_%s' % k] = v
|
||||
|
||||
result = app.run(standalone=False)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
result = RET_ERROR
|
||||
finally:
|
||||
if mod_f:
|
||||
mod_f.close()
|
||||
|
||||
return result
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user