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
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
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
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',
103 'gucharmap',
104 'BabelMap.exe',
105 'charmap.exe',
106 'gm-unicode2clipboard'
107
108 ]
109
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
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
145 if not gmShellAPI.run_command_in_shell(command = cUnicodeInsertion_TextCtrlMixin._unicode_selector, blocking = True):
146 wx.TheClipboard.Close()
147 return False
148
149
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
158 if curr_clip == prev_clip:
159
160 return False
161
162 self.WriteText(curr_clip)
163 return True
164
165
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
206
207 - def _mixin_on_find(self, evt):
208
209
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
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
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
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
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
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
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
261 self.__mixin_find_replace_dlg.Destroy()
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
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
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
350
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
362 self.Bind(wx.lib.expando.EVT_ETC_LAYOUT_NEEDED, self._on_expando_needs_layout)
363
364
365
367
368
369
370
371 self.FitInside()
372
373 if self.HasScrollbar(wx.VERTICAL):
374
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
390
391
392
393
394
395
396 if y_desired_visible < y_view:
397
398 self.Scroll(0, y_desired_visible)
399
400 if y_desired_visible > h_view:
401
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):
424
425
426
427
429 self.Bind(wx.EVT_SET_FOCUS, self.__cExpandoTextCtrl_on_focus)
430
431
433 evt.Skip()
434 wx.CallAfter(self._cExpandoTextCtrl_after_on_focus)
435
436
438
439
440
441
442 if not self:
443 return
444
445
446 evt = wx.PyCommandEvent(wx.lib.expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
447 evt.SetEventObject(self)
448
449
450
451
452 self.GetEventHandler().ProcessEvent(evt)
453
454
455
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
466
467
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
479 count_of_extra_lines_needed += 1
480
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
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
507 app = wx.PyWidgetTester(size = (200, 50))
508 tc = cTextCtrl(app.frame, -1)
509
510
511 app.frame.Show(True)
512 app.MainLoop()
513 return True
514
515 test_gm_textctrl()
516