pygo/sgc/widgets/input_box.py

326 lines
14 KiB
Python
Raw Normal View History

2012-04-14 22:38:47 +00:00
#!/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