diff --git a/pygame_vkeyboard/examples/resize.py b/pygame_vkeyboard/examples/resize.py new file mode 100644 index 0000000..3feee07 --- /dev/null +++ b/pygame_vkeyboard/examples/resize.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# coding: utf8 + +"""Simple keyboard usage using QWERTY layout and input text.""" + +# pylint: disable=import-error +import pygame +import pygame_vkeyboard as vkboard +# pylint: enable=import-error + + +def on_key_event(text): + """ Print the current text. """ + print('Current text:', text) + + +def main(test=False): + """ Main program. + + :param test: Indicate function is being tested + :type test: bool + :return: None + """ + + # Init pygame + pygame.init() + screen = pygame.display.set_mode((500, 300), pygame.RESIZABLE) + screen.fill((100, 100, 100)) + + # Create keyboard + layout = vkboard.VKeyboardLayout(vkboard.VKeyboardLayout.QWERTY, + allow_special_chars=False, + allow_space=False) + keyboard = vkboard.VKeyboard(screen, + on_key_event, + layout, + renderer=vkboard.VKeyboardRenderer.DARK, + show_text=True, + joystick_navigation=True) + + clock = pygame.time.Clock() + + # Main loop + while True: + clock.tick(100) # Ensure not exceed 100 FPS + + events = pygame.event.get() + + for event in events: + if event.type == pygame.QUIT: + print("Average FPS: ", clock.get_fps()) + exit() + if event.type == pygame.VIDEORESIZE: + screen.fill((100, 100, 100)) + + keyboard.update(events) + rects = keyboard.draw(screen) + + # Flip only the updated area + pygame.display.update(rects) + + # At first loop returns + if test: + break + + +if __name__ == '__main__': + main() diff --git a/pygame_vkeyboard/vkeyboard.py b/pygame_vkeyboard/vkeyboard.py index aef851d..ff01792 100644 --- a/pygame_vkeyboard/vkeyboard.py +++ b/pygame_vkeyboard/vkeyboard.py @@ -172,7 +172,6 @@ class VKeyboardLayout(object): # TODO : Insert special characters layout which include number. SPECIAL = [u'&é"\'(§è!çà)', u'°_-^$¨*ù`%£', u',;:=?.@+<>#', u'€[]{}/\\|'] - """Special characters layout. """ def __init__(self, model, @@ -211,7 +210,6 @@ def __init__(self, self.size = None self.rows = [] self.sprites = pygame.sprite.LayeredDirty() - self.key_size = key_size self.padding = padding self.height_ratio = height_ratio self.selection = None @@ -231,6 +229,17 @@ def __init__(self, if height_ratio is not None and (height_ratio < 0.2 or height_ratio > 1): raise ValueError('Surface height ratio shall be from 0.2 to 1') + self._key_size = key_size + self._key_size_computed = None + + @property + def key_size(self): + return self._key_size or self._key_size_computed + + @key_size.setter + def key_size(self, key_size): + self._key_size = key_size + def hide(self): """Hide all keys.""" for sprite in self.sprites: @@ -310,18 +319,18 @@ def configure_bound(self, surface_size): Size of the surface this layout will be rendered on. """ nb_rows = len(self.rows) - key_size_defined = self.key_size is not None - if self.key_size is None: - self.key_size = int( + if self._key_size is None: + self._key_size_computed = int( (surface_size[0] - (self.padding * (self.max_length + 1))) / self.max_length) height = self.key_size * nb_rows + self.padding * (nb_rows + 1) if height > surface_size[1] * (self.height_ratio or 0.5): - self.key_size = int((surface_size[1] * (self.height_ratio or 0.5) - - (self.padding * (nb_rows + 1))) / nb_rows) + self._key_size_computed = int((surface_size[1] * (self.height_ratio or 0.5) + - (self.padding * (nb_rows + 1))) / nb_rows) height = self.key_size * nb_rows + self.padding * (nb_rows + 1) - if key_size_defined: + if self._key_size: + self._key_size = self._key_size_computed LOGGER.warning('Computed layout height outbound target surface,' ' reducing key_size to %spx', self.key_size) elif self.height_ratio is not None: @@ -505,13 +514,13 @@ def __init__(self, if self.layout.allow_special_chars: if not special_char_layout: special_char_layout = VKeyboardLayout( - VKeyboardLayout.SPECIAL, - key_size=self.layout.key_size, - padding=self.layout.padding, - height_ratio=self.layout.height_ratio, - allow_uppercase=self.layout.allow_uppercase, - allow_special_chars=self.layout.allow_special_chars, - allow_space=self.layout.allow_space) + VKeyboardLayout.SPECIAL, + key_size=self.layout.key_size, + padding=self.layout.padding, + height_ratio=self.layout.height_ratio, + allow_uppercase=self.layout.allow_uppercase, + allow_special_chars=self.layout.allow_special_chars, + allow_space=self.layout.allow_space) self.layouts.append(special_char_layout) for layout in self.layouts: @@ -519,17 +528,12 @@ def __init__(self, layout.configure_renderer(self.renderer) layout.sprites.add(self.background, layer=0) - synchronize_layouts(self.surface.get_size(), *self.layouts) - self.background.set_rect(*self.layout.position + self.layout.size) - # Setup the text input box self.show_text = show_text - self.input = VTextInput((self.layout.position[0], - self.layout.position[1] - - self.layout.key_size), - (self.layout.size[0], - self.layout.key_size), - renderer=self.renderer) + self.input = VTextInput((0, 0), (10, 10), renderer=self.renderer) + + self.resize(surface) + if self.show_text: self.input.enable() @@ -553,6 +557,36 @@ def set_layout(self, layout): self.layout = layout self.layout.show() + def set_eraser(self, surface): + """Setup the surface used to hide/clear the keyboard. + """ + self.eraser = surface.copy() + for layout in self.layouts: + layout.sprites.clear(surface, self.eraser) + + def resize(self, surface): + """Resize the keyboard according to the surface size and the parameters + of the layout(s). + + Parameters + ---------- + surface: + Surface this keyboard will be displayed at. + """ + synchronize_layouts(surface.get_size(), *self.layouts) + self.background.set_rect(*self.layout.position + self.layout.size) + + for layout in self.layouts: + if layout.sprites.get_clip() != self.background.rect: + # Changing the clipping area will force update of all + # sprites without using "dirty mechanism" + layout.sprites.set_clip(self.background.rect) + + self.input.set_line_rect(self.layout.position[0], + self.layout.position[1] - self.layout.key_size, + self.layout.size[0], + self.layout.key_size) + def set_text(self, text): """Set the current text in the internal buffer. @@ -591,13 +625,6 @@ def get_rect(self): rect = rect.union(self.input.get_rect()) return rect - def set_eraser(self, surface): - """Setup the surface used to hide/clear the keyboard. - """ - self.eraser = surface.copy() - for layout in self.layouts: - layout.sprites.clear(surface, self.eraser) - def draw(self, surface=None, force=False): """Draw the virtual keyboard. @@ -625,17 +652,15 @@ def draw(self, surface=None, force=False): """ surface = surface or self.surface + # Check if surface has been resized + if self.eraser and surface.get_rect() != self.eraser.get_rect(): + force = True # To force creating new eraser + self.resize(surface) + # Setup eraser if not self.eraser or force: self.set_eraser(surface) - # Setup new the surface where to draw - for layout in self.layouts: - if layout.sprites.get_clip() != self.background.rect: - # Changing the clipping area will force update of all - # sprites without using "dirty mechanism" - layout.sprites.set_clip(self.background.rect) - rects = self.layout.sprites.draw(surface) rects += self.input.draw(surface, force) diff --git a/pygame_vkeyboard/vtextinput.py b/pygame_vkeyboard/vtextinput.py index 69113ea..d8d5911 100644 --- a/pygame_vkeyboard/vtextinput.py +++ b/pygame_vkeyboard/vtextinput.py @@ -101,6 +101,22 @@ def set_position(self, position): self.rect.topleft = position self.dirty = 1 + def set_size(self, width, height): + """Set the cursor size. + + Parameters + ---------- + width: + Background width. + height: + Background height. + """ + if self.rect.size != (width, height): + self.rect.size = (width, height) + self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) + self.renderer.draw_cursor(self.image, self) + self.dirty = 1 + def set_index(self, index): """Move the cursor at the given index. @@ -183,6 +199,22 @@ def set_position(self, position): self.rect.topleft = position self.dirty = 1 + def set_size(self, width, height): + """Set the line size. + + Parameters + ---------- + width: + Background width. + height: + Background height. + """ + if self.rect.size != (width, height): + self.rect.size = (width, height) + self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) + self.renderer.draw_text(self.image, self.text) + self.dirty = 1 + def clear(self): """Clear the current text.""" if self.text: @@ -265,29 +297,27 @@ def __init__(self, # Define background sprites self.eraser = None self.background = VBackground(size, renderer) - self.background.set_rect(self.position[0], - self.position[1] - 2 * self.text_margin, - self.size[0], - self.size[1] + 2 * self.text_margin) self.sprites = pygame.sprite.LayeredDirty(self.background) # Initialize first line line = VLine((self.size[0] - 2 * self.text_margin, self.size[1]), renderer, True) - line.set_position(( - self.position[0] + self.text_margin, - self.position[1] - self.text_margin)) self.sprites.add(line, layer=1) # Initialize cursor - self.cursor = VCursor((2, size[1] - self.text_margin * 2), renderer) - self.cursor.set_position(( - self.position[0] + self.text_margin, - self.position[1])) + self.cursor = VCursor((2, self.size[1] - self.text_margin * 2), renderer) self.sprites.add(self.cursor, layer=2) self.disable() + self.set_line_rect(*self.position + self.size) + + def set_eraser(self, surface): + """Setup the surface used to hide/clear the text input. + """ + self.eraser = surface.copy() + self.sprites.clear(surface, self.eraser) + def enable(self): """Set this text input as active.""" self.state = 1 @@ -323,11 +353,46 @@ def get_rect(self): """Return text input rect.""" return self.background.rect - def set_eraser(self, surface): - """Setup the surface used to hide/clear the keyboard. + def set_line_rect(self, x, y, width, height): + """Set the input text (one line) absolute position and size. + + Parameters + ---------- + x: + Position x. + y: + Position y. + width: + Background width. + height: + Background height. """ - self.eraser = surface.copy() - self.sprites.clear(surface, self.eraser) + self.size = (width, height) + self.position = (x, y) + self.background.set_rect(self.position[0], + self.position[1] - 2 * self.text_margin, + width, + height + 2 * self.text_margin) + + for line in self.sprites.get_sprites_from_layer(1): + line.clear() + line.set_position((self.position[0] + self.text_margin, + self.position[1] - self.text_margin)) + line.set_size(self.size[0] - 2 * self.text_margin, self.size[1]) + self.update_lines() + + self.cursor.set_position((self.position[0] + self.text_margin, + self.position[1])) + self.cursor.set_size(2, self.size[1] - self.text_margin * 2) + + # Setup new the surface where to draw + clip_rect = pygame.Rect(self.position[0], 0, + self.background.rect.width, + self.background.rect.bottom) + if self.sprites.get_clip() != clip_rect: + # Changing the clipping area will force update of all + # sprites without using "dirty mechanism" + self.sprites.set_clip(clip_rect) def draw(self, surface, force): """Draw the text input box. @@ -339,19 +404,14 @@ def draw(self, surface, force): force: Force the drawing of the entire surface (time consuming). """ + # Check if surface has been resized + if self.eraser and surface.get_rect() != self.eraser.get_rect(): + force = True # To force creating new eraser + # Setup eraser if not self.eraser or force: self.set_eraser(surface) - # Setup new the surface where to draw - clip_rect = pygame.Rect(self.position[0], 0, - self.background.rect.width, - self.background.rect.bottom) - if self.sprites.get_clip() != clip_rect: - # Changing the clipping area will force update of all - # sprites without using "dirty mechanism" - self.sprites.set_clip(clip_rect) - if force: self.sprites.repaint_rect(self.background.rect) return self.sprites.draw(surface)