1
0
Fork 0
pie-time/pie_time/application.py

512 lines
16 KiB
Python

# -*- 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