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 expand 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 #wx.lib.expando.EVT_ETC_LAYOUT_NEEDED(expando, expando.GetId(), self._on_expando_needs_layout) 364 365 #--------------------------------------------------------
366 - def _on_expando_needs_layout(self, evt):
367 # need to tell ourselves to re-Layout to refresh scroll bars 368 369 # provoke adding scrollbar if needed 370 #self.Fit() # works on Linux but not on Windows 371 self.FitInside() # needed on Windows rather than self.Fit() 372 373 if self.HasScrollbar(wx.VERTICAL): 374 # scroll panel to show cursor 375 expando = self.FindWindowById(evt.GetId()) 376 y_expando = expando.GetPositionTuple()[1] 377 h_expando = expando.GetSize()[1] 378 line_of_cursor = expando.PositionToXY(expando.GetInsertionPoint())[2] + 1 379 if expando.NumberOfLines == 0: 380 no_of_lines = 1 381 else: 382 no_of_lines = expando.NumberOfLines 383 y_cursor = int(round((float(line_of_cursor) / no_of_lines) * h_expando)) 384 y_desired_visible = y_expando + y_cursor 385 386 y_view = self.ViewStart[1] 387 h_view = self.GetClientSize()[1] 388 389 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines 390 # print "cursor :", y_cursor, "at line", line_of_cursor, ", insertion point:", expando.GetInsertionPoint() 391 # print "wanted :", y_desired_visible 392 # print "view-y :", y_view 393 # print "scroll2:", h_view 394 395 # expando starts before view 396 if y_desired_visible < y_view: 397 # print "need to scroll up" 398 self.Scroll(0, y_desired_visible) 399 400 if y_desired_visible > h_view: 401 # print "need to scroll down" 402 self.Scroll(0, y_desired_visible)
403 404 #============================================================
405 -class cExpandoTextCtrl(gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin, cTextSearch_TextCtrlMixin, cColoredStatus_TextCtrlMixin, wx.lib.expando.ExpandoTextCtrl):
406 """Expando based text ctrl 407 408 - auto-sizing on input 409 - keyword based text expansion 410 - text search on show_find_dialog() 411 - (on demand) status based background color 412 413 Parent panels should apply the cExpandoTextCtrlHandling_PanelMixin. 414 """
415 - def __init__(self, *args, **kwargs):
416 417 wx.lib.expando.ExpandoTextCtrl.__init__(self, *args, **kwargs) 418 gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self) 419 cTextSearch_TextCtrlMixin.__init__(self) 420 cColoredStatus_TextCtrlMixin.__init__(self) 421 422 self.__register_interests() 423 self.enable_keyword_expansions()
424 425 #------------------------------------------------ 426 # event handling 427 #------------------------------------------------
428 - def __register_interests(self):
429 self.Bind(wx.EVT_SET_FOCUS, self.__cExpandoTextCtrl_on_focus)
430 431 #--------------------------------------------------------
432 - def __cExpandoTextCtrl_on_focus(self, evt):
433 evt.Skip() # allow other processing to happen 434 wx.CallAfter(self._cExpandoTextCtrl_after_on_focus)
435 436 #--------------------------------------------------------
438 # robustify against Py__DeadObjectError (RuntimeError) - since 439 # we are called from wx's CallAfter this SoapCtrl may be gone 440 # by the time we get around to handling this layout request, 441 # say, on patient change or some such 442 if not self: 443 return 444 445 #wx. CallAfter(self._adjustCtrl) 446 evt = wx.PyCommandEvent(wx.lib.expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) 447 evt.SetEventObject(self) 448 #evt.height = None 449 #evt.numLines = None 450 #evt.height = self.GetSize().height 451 #evt.numLines = self.GetNumberOfLines() 452 self.GetEventHandler().ProcessEvent(evt)
453 454 #------------------------------------------------ 455 # fix platform expando.py if needed 456 #------------------------------------------------
457 - def _wrapLine(self, line, dc, max_width):
458 459 if wx.MAJOR_VERSION > 2: 460 return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width) 461 462 if (wx.MAJOR_VERSION == 2) and (wx.MINOR_VERSION > 8): 463 return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width) 464 465 # THIS FIX LIFTED FROM TRUNK IN SVN: 466 # Estimate where the control will wrap the lines and 467 # return the count of extra lines needed. 468 partial_text_extents = dc.GetPartialTextExtents(line) 469 max_width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) 470 idx = 0 471 start = 0 472 count_of_extra_lines_needed = 0 473 idx_of_last_blank = -1 474 while idx < len(partial_text_extents): 475 if line[idx] == ' ': 476 idx_of_last_blank = idx 477 if (partial_text_extents[idx] - start) > max_width: 478 # we've reached the max width, add a new line 479 count_of_extra_lines_needed += 1 480 # did we see a space? if so restart the count at that pos 481 if idx_of_last_blank != -1: 482 idx = idx_of_last_blank + 1 483 idx_of_last_blank = -1 484 if idx < len(partial_text_extents): 485 start = partial_text_extents[idx] 486 else: 487 idx += 1 488 return count_of_extra_lines_needed
489 490 #=================================================== 491 # main 492 #--------------------------------------------------- 493 if __name__ == '__main__': 494 495 if len(sys.argv) < 2: 496 sys.exit() 497 498 if sys.argv[1] != 'test': 499 sys.exit() 500 501 from Gnumed.pycommon import gmI18N 502 gmI18N.activate_locale() 503 gmI18N.install_domain(domain='gnumed') 504 505 #-----------------------------------------------
506 - def test_gm_textctrl():
507 app = wx.PyWidgetTester(size = (200, 50)) 508 tc = cTextCtrl(app.frame, -1) 509 #tc.enable_keyword_expansions() 510 #tc.Enable(False) 511 app.frame.Show(True) 512 app.MainLoop() 513 return True
514 #----------------------------------------------- 515 test_gm_textctrl() 516