#!/usr/bin/env python # Copyright (C) 2010-2012 Sam Bull """ Input Box for receiving text input. """ import pygame from pygame.locals import * from _locals import * from base_widget import Simple class InputBox(Simple): """ Input box Attributes: text: Text entered in input box. Can be set or retrieved directly. Images: 'image': The background of the input box when focused. 'inactive': The background of the input box when not focused. """ _can_focus = True # Override Simple _default_size = (240, 30) _available_images = ("inactive",) _settings_default = {"label": "", "default": "", "blink_interval": 600, "col_selection": (118, 45, 215), "col_focus": (255,255,255), "col_focus_not": (200,200,200), "max_chars": 80, "repeat_begin": 300, "repeat_interval": 30} _blink_time = 0 _blink = True __cursor_pos = 0 _r = None # Rect for position and size of input box _repeat_key = None _repeat_time = 0 _select = None # Starting point of selection _offset = 6 # Offset to render text in the input box def _config(self, **kwargs): """ default: ``str`` Contains the default text displayed when nothing has been entered and input box does not have focus. label: ``str`` Contains text to be rendered to left of widget. blink_interval: ``int`` Milliseconds between cursor blink. col_focus: ``tuple`` (r,g,b) Background colour when focused. col_focus_not: ``tuple`` (r,g,b) Background colour when not focused. col_selection: ``tuple`` (r,g,b) Colour of selection rectangle. max_chars: ``int`` Maximum number of characters. repeat_begin: ``int`` Milliseconds key is held down before repeating. repeat_interval: ``int`` Milliseconds between key repeats. text: ``str`` Set the text entered in input box. """ if "init" in kwargs: self._text = [] if "default" in kwargs: self._settings["default"] = kwargs["default"] if "label" in kwargs: self._settings["label"] = kwargs["label"] if hasattr(self, "_label"): self._images["image"].fill(0, self._label.rect) # Label to left of input box self._label = Simple(Font["widget"].render(self._settings["label"], True, Font.col)) self._label.rect.centery = self.rect.h/2 # Update input rect self._r = Rect((self._label.rect.w+5, 0), (self.rect.w-(self._label.rect.w+5), self.rect.h)) if "blink_interval" in kwargs: self._settings["blink_interval"] = kwargs["blink_interval"] if "col_focus" in kwargs: self._settings["col_focus"] = kwargs["col_focus"] if "col_focus_not" in kwargs: self._settings["col_focus_not"] = kwargs["col_focus_not"] if "col_selection" in kwargs: self._settings["col_selection"] = kwargs["col_selection"] if "max_chars" in kwargs: self._settings["max_chars"] = kwargs["max_chars"] if "repeat_begin" in kwargs: self._settings["repeat_begin"] = kwargs["repeat_begin"] if "repeat_interval" in kwargs: self._settings["repeat_interval"] = kwargs["repeat_interval"] if "text" in kwargs: self._text = [unicode(char) for char in kwargs["text"]] def _draw(self, draw): # Active state background self._images["image"].fill(self._settings["col_focus"], self._r) draw.rect(self._images["image"], (0,0,1), self._r, 4) self._images["image"].blit(self._label.image, self._label.pos) # Inactive state background self._images["inactive"].fill(self._settings["col_focus_not"], self._r) draw.rect(self._images["inactive"], (0,0,1), self._r, 4) self._images["inactive"].blit(self._label.image, self._label.pos) # Draw image in non-focus state self._focus_exit() # Store the input text as a list @property def text(self): return "".join(self._text) @text.setter def text(self, txt): self._text = [unicode(char) for char in txt] # Re-evaluate cursor position. self._cursor_pos = self._cursor_pos def activate(self): """ Called when the user hits the enter key. Emits an event with attribute 'gui_type' == "activate". Override this function to use as a callback handler. """ ev = pygame.event.Event(GUI, {"gui_type": "activate", "widget_type": self.__class__, "widget": self}) pygame.event.post(ev) def update(self, time): """Update the input box each frame.""" if self.has_focus(): draw = self.get_draw() # Repeat key if held down if self._repeat_key: self._repeat_time += time while self._repeat_time > self._settings["repeat_begin"]: self._repeat_time -= self._settings["repeat_interval"] self._event(self._repeat_key) # Draw input box text = Font["mono"].render(self.text, True, (0,0,0)) y = (self._r.h - text.get_height()) / 2 self.image = self._images["image"].copy() area = ((6-self._offset,0), (self._r.w-8, self._r.h)) self.image.blit(text, (self._r.x+6, y), area) # If enough time has passed, blink cursor self._blink_time += time if self._blink_time > self._settings["blink_interval"]: self._blink_time -= self._settings["blink_interval"] self._blink = not self._blink # Draw cursor in box if self._blink and self._select is None: x = self._cursor_pos*Font.mono_w + self._r.x + self._offset draw.line(self.image, (0,0,1), (x, 6), (x, self._r.h-6)) # Draw selection highlighting if self._select is not None: select = self._select_fix() # Semi-transparent selection rectangle w = ((select[1]*Font.mono_w + self._offset) - max(4, (select[0]*Font.mono_w + self._offset))) selection = Simple((w, self._r.h - 11)) selection.pos = (self._r.x + select[0] * Font.mono_w + self._offset, 6) selection.image.fill(self._settings["col_selection"]) selection.image.set_alpha(100) # Border around selection rectangle selection_b = Simple((selection.rect.w+2, selection.rect.h+2)) draw.rect(selection_b.image, self._settings["col_selection"], selection_b.rect, 1) pos = (max(self._r.x+4, selection.rect.x), selection.rect.y) self.image.blit(selection.image, pos) self.image.blit(selection_b.image, (pos[0]-1, pos[1]-1)) def _event(self, event): """Update text field based on input.""" if event.type == KEYDOWN: # Reset cursor blink when typing self._blink_time = 0 self._blink = True # Save last key press for repeat if self._repeat_key != event: self._repeat_key = event self._repeat_time = 0 if event.key in (9,): # Keys to ignore pass elif event.key == K_ESCAPE: self._select = None elif event.key == K_RETURN: self.activate() elif event.key == K_BACKSPACE: if self._select is not None: self._delete_selection() elif self._cursor_pos > 0: self._cursor_pos -= 1 self._text.pop(self._cursor_pos) elif event.key == K_DELETE: if self._select is not None: self._delete_selection() elif self._cursor_pos < len(self._text): self._text.pop(self._cursor_pos) elif event.key == K_LEFT: if not event.mod & KMOD_SHIFT: self._select = None # Break selection elif self._select is None: # Reset selection if not selecting self._select = self._cursor_pos self._cursor_pos -= 1 # Remove selection when cursor is at same position if self._select == self._cursor_pos: self._select = None elif event.key == K_RIGHT: if not event.mod & KMOD_SHIFT: self._select = None # Break selection elif self._select is None: self._select = self._cursor_pos self._cursor_pos += 1 if self._select == self._cursor_pos: self._select = None elif event.unicode: if event.mod & KMOD_CTRL: if event.key == K_a: # Select all self._select = 0 self._cursor_pos = len(self._text) elif event.key == K_c and self._select is not None: # Copy select = self._select_fix() string = "".join(self._text[select[0]:select[1]]) try: pygame.scrap.put(SCRAP_TEXT, string) except pygame.error: print "Please run 'pygame.scrap.init()'" \ " to use the clipboard." elif event.key == K_v: # Paste text = pygame.scrap.get(SCRAP_TEXT) if text: if self._select is not None: self._delete_selection() # Get list of text to insert into input_text text = [unicode(char) for char in text] self._text[self._cursor_pos:self._cursor_pos] = text self._cursor_pos += len(text) elif event.key == K_x and self._select is not None: # Cut select = self._select_fix() string = "".join(self._text[select[0]:select[1]]) try: pygame.scrap.put(SCRAP_TEXT, string) except pygame.error: print "Please run 'pygame.scrap.init()'" \ " to use the clipboard" self._delete_selection() else: # Delete selection if self._select is not None: self._delete_selection() # Insert new character if len(self._text) < self._settings["max_chars"]: self._text.insert(self._cursor_pos, event.unicode) self._cursor_pos += 1 elif event.type == KEYUP: if self._repeat_key and self._repeat_key.key == event.key: self._repeat_key = None # Stop repeat elif event.type == MOUSEBUTTONDOWN: # Begin drawing selection if pygame.key.get_mods() & KMOD_SHIFT and self._select is None: self._select = self._cursor_pos self._cursor_pos = self._mouse_cursor(event.pos) if not pygame.key.get_mods() & KMOD_SHIFT: self._select = self._cursor_pos elif event.type == MOUSEMOTION and event.buttons[0]: # Continue drawing selection while mouse held down self._cursor_pos = self._mouse_cursor(event.pos) elif event.type == MOUSEBUTTONUP: # Set cursor position with mouse click self._cursor_pos = self._mouse_cursor(event.pos) if self._select == self._cursor_pos: self._select = None def _focus_exit(self): """Draw non-focused input box when focus is lost.""" self.image = self._images["inactive"].copy() if self._text: # Blit input text into box... text = Simple(Font["mono"].render(self.text, True, (70,70,70))) else: # ...or default text if empty. text = Simple(Font["mono"].render(self._settings["default"], True, (70,70,70))) text.rect.midleft = (self._r.x + self._offset, self._r.h / 2) self.image.blit(text.image, text.pos) # Stop repeat key self._repeat_key = None def _mouse_cursor(self, mouse_pos): """Return the text cursor position of the mouse.""" pos = mouse_pos[0] - self.rect_abs.x - self._offset - self._r.x pos = int(round(float(pos) / Font.mono_w)) return max(min(pos, len(self._text)), 0) def _select_fix(self): """If selection is right-to-left then reverse positions.""" if self._select > self._cursor_pos: return (self._cursor_pos, self._select) else: return (self._select, self._cursor_pos) def _delete_selection(self): """Delete the current selection of text.""" select = self._select_fix() del self._text[select[0]:select[1]] self._select = None self._cursor_pos = select[0] @property def _cursor_pos(self): return self.__cursor_pos @_cursor_pos.setter def _cursor_pos(self, value): # Keep cursor position within text self.__cursor_pos = min(max(value, 0), len(self._text)) # Scroll text in input box when it's too long pos = self._cursor_pos * Font.mono_w if pos > (self._r.w - self._offset): self._offset = -(pos - self._r.w + 6) elif pos < (6 - self._offset): self._offset = 6 - pos