Package Gnumed :: Package wxpython :: Module gmTextCtrl
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmTextCtrl

  1  __doc__ = """GNUmed TextCtrl sbuclass.""" 
  2  #=================================================== 
  3  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
  4  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
  5   
  6  import logging 
  7  import sys 
  8   
  9   
 10  import wx 
 11  import wx.lib.expando 
 12   
 13   
 14  if __name__ == '__main__': 
 15          sys.path.insert(0, '../../') 
 16   
 17  from Gnumed.pycommon import gmShellAPI 
 18  from Gnumed.wxpython import gmKeywordExpansionWidgets 
 19   
 20   
 21  _log = logging.getLogger('gm.txtctrl') 
 22   
 23  #============================================================ 
 24  color_tctrl_invalid = 'pink' 
 25  color_tctrl_partially_invalid = 'yellow' 
 26   
27 -class cColoredStatus_TextCtrlMixin():
28 """Mixin for setting background color based on validity of content. 29 30 Note that due to Python MRO classes using this mixin must 31 list it before their base class (because we override Enable/Disable). 32 """
33 - def __init__(self, *args, **kwargs):
34 35 if not isinstance(self, (wx.TextCtrl)): 36 raise TypeError('[%s]: can only be applied to wx.TextCtrl, not [%s]' % (cColoredStatus_TextCtrlMixin, self.__class__.__name__)) 37 38 self.__initial_background_color = self.GetBackgroundColour() 39 self.__previous_enabled_bg_color = self.__initial_background_color
40 41 #--------------------------------------------------------
42 - def display_as_valid(self, valid=None):
43 if valid is True: 44 color2show = self.__initial_background_color 45 elif valid is False: 46 color2show = color_tctrl_invalid 47 elif valid is None: 48 color2show = color_tctrl_partially_invalid 49 else: 50 raise ValueError('<valid> must be True or False or None') 51 52 if self.IsEnabled(): 53 self.SetBackgroundColour(color2show) 54 self.Refresh() 55 return 56 57 # remember for when it gets enabled 58 self.__previous_enabled_bg_color = color2show
59 60 #--------------------------------------------------------
61 - def display_as_disabled(self, disabled=None):
62 current_enabled_state = self.IsEnabled() 63 desired_enabled_state = disabled is False 64 if current_enabled_state is desired_enabled_state: 65 return 66 67 if disabled is True: 68 self.__previous_enabled_bg_color = self.GetBackgroundColour() 69 color2show = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND) 70 elif disabled is False: 71 color2show = self.__previous_enabled_bg_color 72 else: 73 raise ValueError('<disabled> must be True or False') 74 75 self.SetBackgroundColour(color2show) 76 self.Refresh()
77 78 #--------------------------------------------------------
79 - def Disable(self):
80 self.Enable(enable = False)
81 82 #--------------------------------------------------------
83 - def Enable(self, enable=True):
84 85 if self.IsEnabled() is enable: 86 return 87 88 wx.TextCtrl.Enable(self, enable) 89 90 if enable is True: 91 self.SetBackgroundColour(self.__previous_enabled_bg_color) 92 elif enable is False: 93 self.__previous_enabled_bg_color = self.GetBackgroundColour() 94 self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)) 95 else: 96 raise ValueError('<enable> must be True or False') 97 98 self.Refresh()
99 100 #============================================================ 101 _KNOWN_UNICODE_SELECTORS = [ 102 'kcharselect', # KDE 103 'gucharmap', # GNOME 104 'BabelMap.exe', # Windows, supposed to be better than charmap.exe 105 'charmap.exe', # Microsoft utility 106 'gm-unicode2clipboard' # generic GNUmed workaround 107 # Mac OSX supposedly features built-in support 108 ] 109
110 -class cUnicodeInsertion_TextCtrlMixin():
111 """Mixin for inserting unicode characters via selection tool.""" 112 113 _unicode_selector = None 114
115 - def __init__(self, *args, **kwargs):
116 if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)): 117 raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cUnicodeInsertion_TextCtrlMixin, self.__class__.__name__)) 118 119 if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None: 120 found, cUnicodeInsertion_TextCtrlMixin._unicode_selector = gmShellAPI.find_first_binary(binaries = _KNOWN_UNICODE_SELECTORS) 121 if found: 122 _log.debug('found [%s] for unicode character selection', cUnicodeInsertion_TextCtrlMixin._unicode_selector) 123 else: 124 _log.error('no unicode character selection tool found')
125 126 #--------------------------------------------------------
128 if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None: 129 return False 130 131 # read clipboard 132 if wx.TheClipboard.IsOpened(): 133 _log.error('clipboard already open') 134 return False 135 if not wx.TheClipboard.Open(): 136 _log.error('cannot open clipboard') 137 return False 138 data_obj = wx.TextDataObject() 139 prev_clip = None 140 got_it = wx.TheClipboard.GetData(data_obj) 141 if got_it: 142 prev_clip = data_obj.Text 143 144 # run selector 145 if not gmShellAPI.run_command_in_shell(command = cUnicodeInsertion_TextCtrlMixin._unicode_selector, blocking = True): 146 wx.TheClipboard.Close() 147 return False 148 149 # read clipboard again 150 got_it = wx.TheClipboard.GetData(data_obj) 151 wx.TheClipboard.Close() 152 if not got_it: 153 _log.debug('clipboard does not contain text') 154 return False 155 curr_clip = data_obj.Text 156 157 # insert clip if so 158 if curr_clip == prev_clip: 159 # nothing put into clipboard (that is, clipboard still the same) 160 return False 161 162 self.WriteText(curr_clip) 163 return True
164 165 #============================================================
166 -class cTextSearch_TextCtrlMixin():
167 """Code using classes with this mixin must call 168 show_find_dialog() at appropriate times. Everything 169 else will be handled. 170 """
171 - def __init__(self, *args, **kwargs):
172 if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)): 173 raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cTextSearch_TextCtrlMixin, self.__class__.__name__)) 174 175 self.__mixin_find_replace_data = None 176 self.__mixin_find_replace_dlg = None 177 self.__mixin_find_replace_last_match_start = 0 178 self.__mixin_find_replace_last_match_end = 0 179 self.__mixin_find_replace_last_match_attr = None
180 181 #--------------------------------------------------------
182 - def show_find_dialog(self, title=None):
183 184 if self.__mixin_find_replace_dlg is not None: 185 return self.__mixin_find_replace_dlg 186 187 self.__mixin_find_replace_last_match_end = 0 188 189 if title is None: 190 title = _('Find text') 191 self.__mixin_find_replace_data = wx.FindReplaceData() 192 self.__mixin_find_replace_dlg = wx.FindReplaceDialog ( 193 self, 194 self.__mixin_find_replace_data, 195 title, 196 wx.FR_NOUPDOWN | wx.FR_NOMATCHCASE | wx.FR_NOWHOLEWORD 197 ) 198 self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND, self._mixin_on_find) 199 self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND_NEXT, self._mixin_on_find) 200 self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND_CLOSE, self._mixin_on_find_close) 201 self.__mixin_find_replace_dlg.Show() 202 return self.__mixin_find_replace_dlg
203 204 #-------------------------------------------------------- 205 # events 206 #--------------------------------------------------------
207 - def _mixin_on_find(self, evt):
208 209 # reset style of previous match 210 if self.__mixin_find_replace_last_match_attr is not None: 211 self.SetStyle ( 212 self.__mixin_find_replace_last_match_start, 213 self.__mixin_find_replace_last_match_end, 214 self.__mixin_find_replace_last_match_attr 215 ) 216 217 # find current match 218 search_term = self.__mixin_find_replace_data.GetFindString().lower() 219 match_start = self.Value.lower().find(search_term, self.__mixin_find_replace_last_match_end) 220 if match_start == -1: 221 # wrap around 222 self.__mixin_find_replace_last_match_start = 0 223 self.__mixin_find_replace_last_match_end = 0 224 wx.Bell() 225 return 226 227 # remember current match for next time around 228 attr = wx.TextAttr() 229 if self.GetStyle(match_start, attr): 230 self.__mixin_find_replace_last_match_attr = attr 231 else: 232 self.__mixin_find_replace_last_match_attr = None 233 self.__mixin_find_replace_last_match_start = match_start 234 self.__mixin_find_replace_last_match_end = match_start + len(search_term) 235 236 # react to current match 237 self.Freeze() 238 self.SetStyle ( 239 self.__mixin_find_replace_last_match_start, 240 self.__mixin_find_replace_last_match_end, 241 wx.TextAttr("red", "black") 242 ) 243 self.ShowPosition(0) 244 self.ShowPosition(self.__mixin_find_replace_last_match_end) 245 self.Thaw()
246 247 #--------------------------------------------------------
248 - def _mixin_on_find_close(self, evt):
249 # cleanup after last match if any 250 if self.__mixin_find_replace_last_match_attr is not None: 251 self.SetStyle ( 252 self.__mixin_find_replace_last_match_start, 253 self.__mixin_find_replace_last_match_end, 254 self.__mixin_find_replace_last_match_attr 255 ) 256 # deactivate the events 257 self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND) 258 self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND_NEXT) 259 self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND_CLOSE) 260 # unshow dialog 261 self.__mixin_find_replace_dlg.DestroyLater() 262 self.__mixin_find_replace_data = None 263 self.__mixin_find_replace_dlg = None 264 self.__mixin_find_replace_last_match_end = 0
265 266 #============================================================
267 -class cTextCtrl(gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin, cTextSearch_TextCtrlMixin, cColoredStatus_TextCtrlMixin, cUnicodeInsertion_TextCtrlMixin, wx.TextCtrl):
268
269 - def __init__(self, *args, **kwargs):
270 271 self._on_set_focus_callbacks = [] 272 self._on_lose_focus_callbacks = [] 273 self._on_modified_callbacks = [] 274 275 wx.TextCtrl.__init__(self, *args, **kwargs) 276 gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self) 277 cTextSearch_TextCtrlMixin.__init__(self) 278 cColoredStatus_TextCtrlMixin.__init__(self) 279 cUnicodeInsertion_TextCtrlMixin.__init__(self) 280 281 self.enable_keyword_expansions()
282 283 #-------------------------------------------------------- 284 # callback API 285 #--------------------------------------------------------
286 - def add_callback_on_set_focus(self, callback=None):
287 """Add a callback for invocation when getting focus.""" 288 if not callable(callback): 289 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback) 290 291 self._on_set_focus_callbacks.append(callback) 292 if len(self._on_set_focus_callbacks) == 1: 293 self.Bind(wx.EVT_SET_FOCUS, self._on_set_focus)
294 #---------------------------------------------------------
295 - def add_callback_on_lose_focus(self, callback=None):
296 """Add a callback for invocation when losing focus.""" 297 if not callable(callback): 298 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback) 299 300 self._on_lose_focus_callbacks.append(callback) 301 if len(self._on_lose_focus_callbacks) == 1: 302 self.Bind(wx.EVT_KILL_FOCUS, self._on_lose_focus)
303 #---------------------------------------------------------
304 - def add_callback_on_modified(self, callback=None):
305 """Add a callback for invocation when the content is modified. 306 307 This callback will NOT be passed any values. 308 """ 309 if not callable(callback): 310 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback) 311 312 self._on_modified_callbacks.append(callback) 313 if len(self._on_modified_callbacks) == 1: 314 self.Bind(wx.EVT_TEXT, self._on_text_update)
315 316 #-------------------------------------------------------- 317 # event handlers 318 #--------------------------------------------------------
319 - def _on_set_focus(self, event):
320 event.Skip() 321 for callback in self._on_set_focus_callbacks: 322 callback() 323 return True
324 #--------------------------------------------------------
325 - def _on_lose_focus(self, event):
326 """Do stuff when leaving the control. 327 328 The user has had her say, so don't second guess 329 intentions but do report error conditions. 330 """ 331 event.Skip() 332 wx.CallAfter(self.__on_lost_focus) 333 return True
334 #--------------------------------------------------------
335 - def __on_lost_focus(self):
336 for callback in self._on_lose_focus_callbacks: 337 callback()
338 #--------------------------------------------------------
339 - def _on_text_update (self, event):
340 """Internal handler for wx.EVT_TEXT. 341 342 Called when text was changed by user or by SetValue(). 343 """ 344 for callback in self._on_modified_callbacks: 345 callback() 346 return
347 348 #============================================================ 349 # expando based text ctrl classes 350 #============================================================
351 -class cExpandoTextCtrlHandling_PanelMixin():
352 """Mixin for panels wishing to handel expando text ctrls within themselves. 353 354 Panels using this mixin will need to call 355 356 self.bind_expando_layout_event(<expando_field>) 357 358 on each <expando_field> they wish to auto-expand. 359 """ 360 #--------------------------------------------------------
361 - def bind_expando_layout_event(self, expando):
362 self.Bind(wx.lib.expando.EVT_ETC_LAYOUT_NEEDED, self._on_expando_needs_layout)
363 364 #--------------------------------------------------------
365 - def _on_expando_needs_layout(self, evt):
366 # need to tell ourselves to re-Layout to refresh scroll bars 367 # provoke adding scrollbar if needed 368 #self.Fit() # works on Linux but not on Windows 369 self.FitInside() # needed on Windows rather than self.Fit() 370 if self.HasScrollbar(wx.VERTICAL): 371 # scroll panel to show cursor 372 expando = self.FindWindowById(evt.GetId()) 373 y_expando = expando.GetPositionTuple()[1] 374 h_expando = expando.GetSize()[1] 375 line_of_cursor = expando.PositionToXY(expando.GetInsertionPoint())[2] + 1 376 if expando.NumberOfLines == 0: 377 no_of_lines = 1 378 else: 379 no_of_lines = expando.NumberOfLines 380 y_cursor = int(round((float(line_of_cursor) / no_of_lines) * h_expando)) 381 y_desired_visible = y_expando + y_cursor 382 y_view = self.GetViewStart()[1] 383 h_view = self.GetClientSize()[1] 384 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines 385 # print "cursor :", y_cursor, "at line", line_of_cursor, ", insertion point:", expando.GetInsertionPoint() 386 # print "wanted :", y_desired_visible 387 # print "view-y :", y_view 388 # print "scroll2:", h_view 389 # expando starts before view 390 if y_desired_visible < y_view: 391 # print "need to scroll up" 392 self.Scroll(0, y_desired_visible) 393 if y_desired_visible > h_view: 394 # print "need to scroll down" 395 self.Scroll(0, y_desired_visible)
396 397 #============================================================
398 -class cExpandoTextCtrl(gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin, cTextSearch_TextCtrlMixin, cColoredStatus_TextCtrlMixin, wx.lib.expando.ExpandoTextCtrl):
399 """Expando based text ctrl 400 401 - auto-sizing on input 402 - keyword based text expansion 403 - text search on show_find_dialog() 404 - (on demand) status based background color 405 406 Parent panels should apply the cExpandoTextCtrlHandling_PanelMixin. 407 """
408 - def __init__(self, *args, **kwargs):
409 410 wx.lib.expando.ExpandoTextCtrl.__init__(self, *args, **kwargs) 411 gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self) 412 cTextSearch_TextCtrlMixin.__init__(self) 413 cColoredStatus_TextCtrlMixin.__init__(self) 414 415 self.__register_interests() 416 self.enable_keyword_expansions()
417 418 #------------------------------------------------ 419 # event handling 420 #------------------------------------------------
421 - def __register_interests(self):
422 self.Bind(wx.EVT_SET_FOCUS, self.__cExpandoTextCtrl_on_focus)
423 424 #--------------------------------------------------------
425 - def __cExpandoTextCtrl_on_focus(self, evt):
426 evt.Skip() # allow other processing to happen 427 wx.CallAfter(self._cExpandoTextCtrl_after_on_focus)
428 429 #--------------------------------------------------------
431 # robustify against Py__DeadObjectError (RuntimeError) - since 432 # we are called from wx's CallAfter this SoapCtrl may be gone 433 # by the time we get around to handling this layout request, 434 # say, on patient change or some such 435 if not self: 436 return 437 438 #wx. CallAfter(self._adjustCtrl) 439 evt = wx.PyCommandEvent(wx.lib.expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) 440 evt.SetEventObject(self) 441 #evt.height = None 442 #evt.numLines = None 443 #evt.height = self.GetSize().height 444 #evt.numLines = self.GetNumberOfLines() 445 self.GetEventHandler().ProcessEvent(evt)
446 447 #------------------------------------------------ 448 # fix platform expando.py if needed 449 #------------------------------------------------
450 - def _wrapLine(self, line, dc, max_width):
451 452 if wx.MAJOR_VERSION > 2: 453 return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width) 454 455 if (wx.MAJOR_VERSION == 2) and (wx.MINOR_VERSION > 8): 456 return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width) 457 458 # THIS FIX LIFTED FROM TRUNK IN SVN: 459 # Estimate where the control will wrap the lines and 460 # return the count of extra lines needed. 461 partial_text_extents = dc.GetPartialTextExtents(line) 462 max_width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) 463 idx = 0 464 start = 0 465 count_of_extra_lines_needed = 0 466 idx_of_last_blank = -1 467 while idx < len(partial_text_extents): 468 if line[idx] == ' ': 469 idx_of_last_blank = idx 470 if (partial_text_extents[idx] - start) > max_width: 471 # we've reached the max width, add a new line 472 count_of_extra_lines_needed += 1 473 # did we see a space? if so restart the count at that pos 474 if idx_of_last_blank != -1: 475 idx = idx_of_last_blank + 1 476 idx_of_last_blank = -1 477 if idx < len(partial_text_extents): 478 start = partial_text_extents[idx] 479 else: 480 idx += 1 481 return count_of_extra_lines_needed
482 483 #=================================================== 484 # main 485 #--------------------------------------------------- 486 if __name__ == '__main__': 487 488 if len(sys.argv) < 2: 489 sys.exit() 490 491 if sys.argv[1] != 'test': 492 sys.exit() 493 494 from Gnumed.pycommon import gmI18N 495 gmI18N.activate_locale() 496 gmI18N.install_domain(domain='gnumed') 497 498 #-----------------------------------------------
499 - def test_gm_textctrl():
500 app = wx.PyWidgetTester(size = (200, 50)) 501 tc = cTextCtrl(app.frame, -1) 502 #tc.enable_keyword_expansions() 503 #tc.Enable(False) 504 app.frame.Show(True) 505 app.MainLoop() 506 return True
507 #----------------------------------------------- 508 test_gm_textctrl() 509