Source code for pibooth.view.window

# -*- coding: utf-8 -*-

"""Pibooth view management.
"""

import os
import time
import contextlib
import pygame
from pygame import gfxdraw
from PIL import Image
from PIL.Image import Resampling
from pibooth import pictures, fonts
from pibooth.view import background
from pibooth.utils import LOGGER
from pibooth.pictures import sizing


[docs] class PiWindow(object): """Class to handle the window. The following attributes are available for use in plugins: :attr surface: surface on which sprites are displayed :type surface: :py:class:`pygame.Surface` :attr is_fullscreen: set to True if the window is display in full screen :type is_fullscreen: bool :attr display_size: tuple (width, height) represneting the size of the screen :type display_size: tuple """ CENTER = 'center' RIGHT = 'right' LEFT = 'left' FULLSCREEN = 'fullscreen' def __init__(self, title, size=(800, 480), color=(0, 0, 0), text_color=(255, 255, 255), arrow_location=background.ARROW_BOTTOM, arrow_offset=0, debug=False): self.__size = size self.debug = debug self.bg_color = color self.text_color = text_color self.arrow_location = arrow_location self.arrow_offset = arrow_offset # Prepare the pygame module for use if 'SDL_VIDEO_WINDOW_POS' not in os.environ: os.environ['SDL_VIDEO_CENTERED'] = '1' pygame.init() # Release the soundcard as we are not using sounds pygame.mixer.quit() # Save the desktop mode, shall be done before `setmode` (SDL 1.2.10, and pygame 1.8.0) info = pygame.display.Info() pygame.display.set_caption(title) self.is_fullscreen = False self.display_size = (info.current_w, info.current_h) self.surface = pygame.display.set_mode(self.__size, pygame.RESIZABLE) self._buffered_images = {} self._current_background = None self._current_foreground = None self._print_number = 0 self._print_failure = False self._capture_number = (0, 4) # (current, max) self._pos_map = {self.CENTER: self._center_pos, self.RIGHT: self._right_pos, self.LEFT: self._left_pos, self.FULLSCREEN: self._center_pos} # Don't use pygame.mouse.get_cursor() because will be removed in pygame2 self._cursor = ((16, 16), (0, 0), (0, 0, 64, 0, 96, 0, 112, 0, 120, 0, 124, 0, 126, 0, 127, 0, 127, 128, 124, 0, 108, 0, 70, 0, 6, 0, 3, 0, 3, 0, 0, 0), (192, 0, 224, 0, 240, 0, 248, 0, 252, 0, 254, 0, 255, 0, 255, 128, 255, 192, 255, 224, 254, 0, 239, 0, 207, 0, 135, 128, 7, 128, 3, 0)) def _update_foreground(self, pil_image, pos=CENTER, resize=True): """Show a PIL image on the foreground. Only one is bufferized to avoid memory leak. """ image_name = id(pil_image) if pos == self.FULLSCREEN: image_size_max = (self.surface.get_size()[0] * 0.9, self.surface.get_size()[1] * 0.9) else: image_size_max = (self.surface.get_size()[0] * 0.48, self.surface.get_size()[1]) buff_size, buff_image = self._buffered_images.get(image_name, (None, None)) if buff_image and image_size_max == buff_size: image = buff_image else: if resize: image = pil_image.resize(sizing.new_size_keep_aspect_ratio( pil_image.size, image_size_max), Resampling.LANCZOS) else: image = pil_image image = pygame.image.frombuffer(image.tobytes(), image.size, image.mode) if self._current_foreground: self._buffered_images.pop(id(self._current_foreground[0]), None) LOGGER.debug("Add to buffer the image '%s'", image_name) self._buffered_images[image_name] = (image_size_max, image) self._current_foreground = (pil_image, pos, resize) if self.debug and resize: # Build rectangle around picture area for debuging purpose outlines = pygame.Surface(image_size_max, pygame.SRCALPHA, 32) pygame.draw.rect(outlines, pygame.Color(255, 0, 0), outlines.get_rect(), 2) self.surface.blit(outlines, self._pos_map[pos](outlines)) return self.surface.blit(image, self._pos_map[pos](image)) def _update_background(self, bkgd): """Show image on the background. """ self._current_background = self._buffered_images.setdefault(str(bkgd), bkgd) self._current_background.set_color(self.bg_color) self._current_background.set_outlines(self.debug) self._current_background.set_text_color(self.text_color) self._current_background.resize(self.surface) self._current_background.paint(self.surface) self._update_capture_number() self._update_print_number() def _update_capture_number(self): """Update the captures counter displayed. """ if not self._capture_number[0]: return # Dont show counter: no picture taken center = self.surface.get_rect().center radius = 10 border = 20 x = center[0] - (2 * radius * self._capture_number[1] + border * (self._capture_number[1] - 1)) // 2 y = self.surface.get_size()[1] - radius - border for nbr in range(self._capture_number[1]): gfxdraw.aacircle(self.surface, x, y, radius, self.text_color) if self._capture_number[0] > nbr: # Because anti-aliased filled circle doesn't exist gfxdraw.aacircle(self.surface, x, y, radius - 3, self.text_color) gfxdraw.filled_circle(self.surface, x, y, radius - 3, self.text_color) x += (2 * radius + border) def _update_print_number(self): """Update the number of files in the printer queue. """ if not self._print_number and not self._print_failure: return # Dont show counter: no file in queue, no failure smaller = self.surface.get_size()[1] if self.surface.get_size( )[1] < self.surface.get_size()[0] else self.surface.get_size()[0] side = int(smaller * 0.05) # 5% of the window if side > 0: if self._print_failure: image = pictures.get_pygame_image('printer_failure.png', (side, side), color=self.text_color) else: image = pictures.get_pygame_image('printer.png', (side, side), color=self.text_color) font = pygame.font.Font(fonts.CURRENT, side) label = font.render(str(self._print_number), True, self.text_color) height = max((image.get_rect().height, label.get_rect().height)) + 20 bg = pygame.Surface((image.get_rect().width + label.get_rect().width + side + 10, height)) bg.fill(self._current_background.get_color()) rect = bg.get_rect() rect.bottomleft = self.get_rect().bottomleft rect_image = image.get_rect(left=10, centery=rect.centery) rect_label = label.get_rect(centerx=rect_image.right + (rect.width - rect_image.right) // 2, centery=rect.centery) self.surface.blit(bg, rect.topleft) self.surface.blit(image, rect_image.topleft) self.surface.blit(label, rect_label.topleft) def _center_pos(self, image): """ Return the position of the given image to be centered on window. """ pos = self.surface.get_rect().center return image.get_rect(center=pos) if image else pos def _left_pos(self, image): """ Return the position of the given image to be put on the left of the screen """ pos = (self.surface.get_rect().centerx // 2, self.surface.get_rect().centery) return image.get_rect(center=pos) if image else pos def _right_pos(self, image): """ Return the position of the given image to be put on the right of the screen """ pos = (self.surface.get_rect().centerx + self.surface.get_rect().centerx // 2, self.surface.get_rect().centery) return image.get_rect(center=pos) if image else pos
[docs] def get_rect(self, absolute=False): """Return a Rect object (as defined in pygame) for this window. :param absolute: absolute position considering the window centered on screen :type absolute: bool """ if absolute: return self.surface.get_rect(center=(self.display_size[0] / 2, self.display_size[1] / 2)) return self.surface.get_rect()
[docs] def get_image(self): """Return the currently displayed foreground image. """ if self._current_foreground: return self._current_foreground[0] return None
[docs] def resize(self, size): """Resize the window keeping aspect ratio. """ if not self.is_fullscreen: self.__size = size # Manual resizing self.surface = pygame.display.set_mode(self.__size, pygame.RESIZABLE) self.update()
[docs] def update(self): """Repaint the window with currently displayed images. """ if self._current_background: self._update_background(self._current_background) else: self._update_capture_number() self._update_print_number() if self._current_foreground: self._update_foreground(*self._current_foreground)
[docs] def show_oops(self): """Show failure view in case of exception. """ self._capture_number = (0, self._capture_number[1]) self._update_background(background.OopsBackground())
[docs] def show_intro(self, pil_image=None, with_print=True): """Show introduction view. """ self._capture_number = (0, self._capture_number[1]) if with_print and pil_image: self._update_background(background.IntroWithPrintBackground(self.arrow_location, self.arrow_offset)) else: self._update_background(background.IntroBackground(self.arrow_location, self.arrow_offset)) if pil_image: self._update_foreground(pil_image, self.RIGHT) elif self._current_foreground: self._buffered_images.pop(id(self._current_foreground[0]), None) self._current_foreground = None
[docs] def show_choice(self, choices, selected=None): """Show the choice view. """ self._capture_number = (0, self._capture_number[1]) if not selected: self._update_background(background.ChooseBackground(choices, self.arrow_location, self.arrow_offset)) else: self._update_background(background.ChosenBackground(choices, selected))
[docs] def show_image(self, pil_image=None, pos=CENTER): """Show PIL image as it (no resize). """ if not pil_image: # Clear the currently displayed image if self._current_foreground: _, image = self._buffered_images.pop(id(self._current_foreground[0])) _, pos, _ = self._current_foreground self._current_foreground = None image.fill((0, 0, 0)) return self.surface.blit(image, self._pos_map[pos](image)) else: return self._update_foreground(pil_image, pos, False)
[docs] def show_work_in_progress(self): """Show wait view. """ self._capture_number = (0, self._capture_number[1]) self._update_background(background.ProcessingBackground())
[docs] def show_print(self, pil_image=None): """Show print view (image resized on the left). """ self._capture_number = (0, self._capture_number[1]) self._update_background(background.PrintBackground(self.arrow_location, self.arrow_offset)) if pil_image: self._update_foreground(pil_image, self.LEFT)
[docs] def show_finished(self, pil_image=None): """Show finished view (image resized fullscreen). """ self._capture_number = (0, self._capture_number[1]) if pil_image: bg = background.FinishedWithImageBackground(pil_image.size) if self._buffered_images.get(str(bg), bg).foreground_size != pil_image.size: self._buffered_images.pop(str(bg)) # Drop cache, foreground size ratio has changed self._update_background(background.FinishedWithImageBackground(pil_image.size)) self._update_foreground(pil_image, self.FULLSCREEN) else: self._update_background(background.FinishedBackground())
[docs] @contextlib.contextmanager def flash(self, count): """Flash the window content. """ if count < 1: raise ValueError("The flash counter shall be greater than 0") for i in range(count): self.surface.fill((255, 255, 255)) if self._current_foreground: # Flash only the background, keep foreground at the top self._update_foreground(*self._current_foreground) pygame.event.pump() pygame.display.update() time.sleep(0.02) if i == count - 1: yield # Let's do actions before end of flash self.update() pygame.event.pump() pygame.display.update() else: self.update() pygame.event.pump() pygame.display.update() time.sleep(0.02)
[docs] def set_capture_number(self, current_nbr, total_nbr): """Set the current number of captures taken. """ if total_nbr < 1: raise ValueError("Total number of captures shall be greater than 0") self._capture_number = (current_nbr, total_nbr) self._update_background(background.CaptureBackground()) if self._current_foreground: self._update_foreground(*self._current_foreground) pygame.display.update()
[docs] def set_print_number(self, current_nbr=None, failure=None): """Set the current number of tasks in the printer queue. """ update = False if current_nbr is not None and self._print_number != current_nbr: self._print_number = current_nbr update = True if failure is not None and self._print_failure != failure: self._print_failure = failure update = True if update: self._update_background(self._current_background) if self._current_foreground: self._update_foreground(*self._current_foreground) pygame.display.update()
[docs] def toggle_fullscreen(self): """Set window to full screen or initial size. """ if self.is_fullscreen: self.is_fullscreen = False # Set before resize pygame.mouse.set_cursor(*self._cursor) self.surface = pygame.display.set_mode(self.__size, pygame.RESIZABLE) else: self.is_fullscreen = True # Set before resize # Make an invisible cursor (don't use pygame.mouse.set_visible(False) because # the mouse event will always return the window bottom-right coordinate) pygame.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0)) self.surface = pygame.display.set_mode(self.display_size, pygame.FULLSCREEN) self.update()
[docs] def drop_cache(self): """Drop all cached background and foreground to force refreshing the view. """ self._current_background = None self._current_foreground = None self._buffered_images = {}