326 lines
14 KiB
Python
326 lines
14 KiB
Python
|
#!/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
|