LUIInputField.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import re
  2. from LUIObject import LUIObject
  3. from LUISprite import LUISprite
  4. from LUILabel import LUILabel
  5. from LUIInitialState import LUIInitialState
  6. from LUILayouts import LUIHorizontalStretchedLayout
  7. __all__ = ["LUIInputField"]
  8. class LUIInputField(LUIObject):
  9. """ Simple input field, accepting text input. This input field supports
  10. entering text and navigating. Selecting text is (currently) not supported.
  11. The input field also supports various keyboard shortcuts:
  12. [pos1] Move to the beginning of the text
  13. [end] Move to the end of the text
  14. [arrow_left] Move one character to the left
  15. [arrow_right] Move one character to the right
  16. [ctrl] + [arrow_left] Move to the left, skipping over words
  17. [ctrl] + [arrow_right] Move to the right, skipping over words
  18. [escape] Un-focus input element
  19. """
  20. re_skip = re.compile("\W*\w+\W")
  21. def __init__(self, parent=None, obscured=False, width=200, placeholder=u"Enter some text ..", value=u"", **kwargs):
  22. """ Constructs a new input field. An input field always needs a width specified """
  23. LUIObject.__init__(self, x=0, y=0, solid=True)
  24. self.set_width(width)
  25. self._layout = LUIHorizontalStretchedLayout(parent=self, prefix="InputField", width="100%")
  26. # Container for the text
  27. self._text_content = LUIObject(self)
  28. self._text_content.margin = (5, 7, 5, 7)
  29. self._text_content.clip_bounds = (0,0,0,0)
  30. self._text_content.set_size("100%", "100%")
  31. # Scroller for the text, so we can move right and left
  32. self._text_scroller = LUIObject(parent=self._text_content)
  33. self._text_scroller.center_vertical = True
  34. self._text = LUILabel(parent=self._text_scroller, text=u"")
  35. # Cursor for the current position
  36. self._cursor = LUISprite(self._text_scroller, "blank", "skin", x=0, y=0, w=2, h=15)
  37. self._cursor.color = (0.5, 0.5, 0.5)
  38. self._cursor.margin.top = 2
  39. self._cursor.z_offset = 20
  40. self._cursor_index = 0
  41. self._cursor.hide()
  42. self._value = value
  43. # Placeholder text, shown when out of focus and no value exists
  44. self._placeholder = LUILabel(parent=self._text_content, text=placeholder, shadow=False,
  45. center_vertical=True, alpha=0.2)
  46. # Various states
  47. self._tickrate = 1.0
  48. self._tickstart = 0.0
  49. self._obscured = obscured
  50. self._render_text()
  51. if parent is not None:
  52. self.parent = parent
  53. LUIInitialState.init(self, kwargs)
  54. @property
  55. def value(self):
  56. """ Returns the value of the input field """
  57. return self._value
  58. @value.setter
  59. def value(self, new_value):
  60. """ Sets the value of the input field """
  61. self._value = (new_value)
  62. self._render_text()
  63. self.trigger_event("changed", self._value)
  64. def clear(self):
  65. """ Clears the input value """
  66. self.value = u""
  67. @property
  68. def cursor_pos(self):
  69. """ Set the cursor position """
  70. return self._cursor_index
  71. @cursor_pos.setter
  72. def cursor_pos(self, pos):
  73. """ Set the cursor position """
  74. if pos >= 0:
  75. self._cursor_index = max(0, min(len(self._value), pos))
  76. else:
  77. self._cursor_index = max(len(self._value) + pos + 1, 0)
  78. self._reset_cursor_tick()
  79. self._render_text()
  80. def on_tick(self, event):
  81. """ Tick handler, gets executed every frame """
  82. frame_time = globalClock.get_frame_time() - self._tickstart
  83. show_cursor = frame_time % self._tickrate < 0.5 * self._tickrate
  84. if show_cursor:
  85. self._cursor.color = (0.5, 0.5, 0.5, 1)
  86. else:
  87. self._cursor.color = (1, 1, 1, 0)
  88. def on_click(self, event):
  89. """ Internal on click handler """
  90. self.request_focus()
  91. def on_mousedown(self, event):
  92. """ Internal mousedown handler """
  93. local_x_offset = self._text.text_handle.get_relative_pos(event.coordinates).x
  94. self.cursor_pos = self._text.text_handle.get_char_index(local_x_offset)
  95. def _reset_cursor_tick(self):
  96. """ Internal method to reset the cursor tick """
  97. self._tickstart = globalClock.get_frame_time()
  98. def on_focus(self, event):
  99. """ Internal focus handler """
  100. self._cursor.show()
  101. self._placeholder.hide()
  102. self._reset_cursor_tick()
  103. self._layout.color = (0.9, 0.9, 0.9, 1)
  104. def on_keydown(self, event):
  105. """ Internal keydown handler. Processes the special keys, and if none are
  106. present, redirects the event """
  107. key_name = event.message
  108. if key_name == "backspace":
  109. self._value = self._value[:max(0, self._cursor_index - 1)] + self._value[self._cursor_index:]
  110. self.cursor_pos -= 1
  111. self.trigger_event("changed", self._value)
  112. elif key_name == "delete":
  113. post_value = self._value[min(len(self._value), self._cursor_index + 1):]
  114. self._value = self._value[:self._cursor_index] + post_value
  115. self.cursor_pos = self._cursor_index
  116. self.trigger_event("changed", self._value)
  117. elif key_name == "arrow_left":
  118. if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
  119. self.cursor_skip_left()
  120. else:
  121. self.cursor_pos -= 1
  122. elif key_name == "arrow_right":
  123. if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
  124. self.cursor_skip_right()
  125. else:
  126. self.cursor_pos += 1
  127. elif key_name == "escape":
  128. self.blur()
  129. elif key_name == "home":
  130. self.cursor_pos = 0
  131. elif key_name == "end":
  132. self.cursor_pos = len(self.value)
  133. self.trigger_event(key_name, self._value)
  134. def on_keyrepeat(self, event):
  135. """ Internal keyrepeat handler """
  136. self.on_keydown(event)
  137. def on_textinput(self, event):
  138. """ Internal textinput handler """
  139. self._value = self._value[:self._cursor_index] + event.message + \
  140. self._value[self._cursor_index:]
  141. self.cursor_pos = self._cursor_index + len(event.message)
  142. self.trigger_event("changed", self._value)
  143. def on_blur(self, event):
  144. """ Internal blur handler """
  145. self._cursor.hide()
  146. if len(self._value) < 1:
  147. self._placeholder.show()
  148. self._layout.color = (1, 1, 1, 1)
  149. def _render_text(self):
  150. """ Internal method to render the text """
  151. if self._obscured: text = '*' * len(self._value)
  152. else: text = self._value
  153. self._text.set_text(text)
  154. self._cursor.left = self._text.left + \
  155. self._text.text_handle.get_char_pos(self._cursor_index) + 1
  156. max_left = self.width - 15
  157. if self._value:
  158. self._placeholder.hide()
  159. else:
  160. if not self.focused:
  161. self._placeholder.show()
  162. # Scroll if the cursor is outside of the clip bounds
  163. rel_pos = self.get_relative_pos(self._cursor.get_abs_pos()).x
  164. if rel_pos >= max_left:
  165. self._text_scroller.left = min(0, max_left - self._cursor.left)
  166. if rel_pos <= 0:
  167. self._text_scroller.left = min(0, - self._cursor.left - rel_pos)
  168. def cursor_skip_left(self):
  169. """ Moves the cursor to the left, skipping the previous word """
  170. left_hand_str = ''.join(reversed(self.value[0:self.cursor_pos]))
  171. match = self.re_skip.match(left_hand_str)
  172. if match is not None:
  173. self.cursor_pos -= match.end() - 1
  174. else:
  175. self.cursor_pos = 0
  176. def cursor_skip_right(self):
  177. """ Moves the cursor to the right, skipping the next word """
  178. right_hand_str = self.value[self.cursor_pos:]
  179. match = self.re_skip.match(right_hand_str)
  180. if match is not None:
  181. self.cursor_pos += match.end() - 1
  182. else:
  183. self.cursor_pos = len(self.value)