|
@@ -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
|