1
2 """GNUmed keyword expansion widgets."""
3
4 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
5 __license__ = "GPL v2 or later"
6
7 import logging
8 import sys
9 import re as regex
10 import os.path
11
12
13 import wx
14 import wx.stc
15
16
17 if __name__ == '__main__':
18 sys.path.insert(0, '../../')
19 from Gnumed.pycommon import gmDispatcher
20 from Gnumed.pycommon import gmPG2
21 from Gnumed.pycommon import gmTools
22 from Gnumed.business import gmKeywordExpansion
23 from Gnumed.wxpython import gmEditArea
24 from Gnumed.wxpython import gmListWidgets
25
26
27 _log = logging.getLogger('gm.ui')
28
29 _text_expansion_fillin_regex = r'\$\[[^]]*\]\$'
30
31
33
34 - def __init__(self, *args, **kwargs):
35 if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)):
36 raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cKeywordExpansion_TextCtrlMixin, self.__class__.__name__))
37
38
40 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
41 self.Bind(wx.EVT_CHAR, self.__on_char_in_keyword_expansion_mixin)
42
43
45 self.Unbind(wx.EVT_CHAR)
46
47
48 - def attempt_expansion(self, show_list_if_needed=False):
49
50 visible, caret_pos_in_line, line_no = self.PositionToXY(self.InsertionPoint)
51 line = self.GetLineText(line_no)
52 keyword_candidate = self.__keyword_separators.split(line[:caret_pos_in_line])[-1]
53
54 if (
55 (show_list_if_needed is False)
56 and
57 (keyword_candidate != '$$steffi')
58 and
59 (keyword_candidate not in [ r[0] for r in gmKeywordExpansion.get_textual_expansion_keywords() ])
60 ):
61 return
62
63
64
65
66
67 start = self.InsertionPoint - len(keyword_candidate)
68 wx.CallAfter(self.__replace_keyword_with_expansion, keyword = keyword_candidate, position = start, show_list_if_needed = show_list_if_needed)
69 return
70
71
72
73
75 evt.Skip()
76
77
78 if self.LastPosition == 1:
79 return
80
81 char = chr(evt.GetUnicodeKey())
82
83 user_wants_expansion_attempt = False
84 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
85 if evt.GetKeyCode() == wx.WXK_RETURN:
86 user_wants_expansion_attempt = True
87 elif evt.GetKeyCode() == 20:
88 user_wants_expansion_attempt = True
89 else:
90 return
91
92 if user_wants_expansion_attempt is False:
93
94
95
96 if self.__keyword_separators.match(char) is None:
97 return
98
99 self.attempt_expansion(show_list_if_needed = user_wants_expansion_attempt)
100
101
102
103
104 - def __replace_keyword_with_expansion(self, keyword=None, position=None, show_list_if_needed=False):
105
106 expansion = expand_keyword(parent = self, keyword = keyword, show_list_if_needed = show_list_if_needed)
107 if expansion is None:
108 return
109 if expansion == '':
110 return
111
112 if not self.IsMultiLine():
113 expansion_lines = gmTools.strip_leading_empty_lines (
114 lines = gmTools.strip_trailing_empty_lines (
115 text = expansion,
116 return_list = True
117 ),
118 return_list = True
119 )
120 if len(expansion_lines) == 0:
121 return
122 if len(expansion_lines) == 1:
123 expansion = expansion_lines[0]
124 else:
125 msg = _(
126 'The fragment <%s> expands to multiple lines !\n'
127 '\n'
128 'This text field can hold one line only, hwoever.\n'
129 '\n'
130 'Please select the line you want to insert:'
131 ) % keyword
132 expansion = gmListWidgets.get_choices_from_list (
133 parent = self,
134 msg = msg,
135 caption = _('Adapting multi-line expansion to single-line text field'),
136 choices = expansion_lines,
137 selections = [0],
138 columns = [_('Keyword expansion lines')],
139 single_selection = True,
140 can_return_empty = False
141 )
142 if expansion is None:
143 return
144
145 self.Replace (
146 position,
147 position + len(keyword),
148 expansion
149 )
150
151 self.SetInsertionPoint(position + len(expansion) + 1)
152 self.ShowPosition(position + len(expansion) + 1)
153
154 return
155
156
157 from Gnumed.wxGladeWidgets import wxgTextExpansionEditAreaPnl
158
159 -class cTextExpansionEditAreaPnl(wxgTextExpansionEditAreaPnl.wxgTextExpansionEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
160
161 - def __init__(self, *args, **kwds):
162
163 try:
164 data = kwds['expansion']
165 del kwds['expansion']
166 except KeyError:
167 data = None
168
169 wxgTextExpansionEditAreaPnl.wxgTextExpansionEditAreaPnl.__init__(self, *args, **kwds)
170 gmEditArea.cGenericEditAreaMixin.__init__(self)
171
172 self.mode = 'new'
173 self.data = data
174 if data is not None:
175 self.mode = 'edit'
176
177
178 self.__register_interests()
179
180 self.__data_filename = None
181
182
183
184
185
186
187
188
189 - def _valid_for_save(self):
190 validity = True
191
192 has_expansion = (
193 (self._TCTRL_expansion.GetValue().strip() != '')
194 or
195 (self.__data_filename is not None)
196 or
197 ((self.data is not None) and (self.data['is_textual'] is False))
198 )
199
200 if has_expansion:
201 self.display_tctrl_as_valid(tctrl = self._TCTRL_expansion, valid = True)
202 self.display_tctrl_as_valid(tctrl = self._TCTRL_data_file, valid = True)
203 else:
204 validity = False
205 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save keyword expansion without text or data expansion.'), beep = True)
206 self.display_tctrl_as_valid(tctrl = self._TCTRL_expansion, valid = False)
207 self.display_tctrl_as_valid(tctrl = self._TCTRL_data_file, valid = False)
208 if self.data is None:
209 self._TCTRL_expansion.SetFocus()
210 else:
211 if self.data['is_textual']:
212 self._TCTRL_expansion.SetFocus()
213 else:
214 self._BTN_select_data_file.SetFocus()
215
216 if self._TCTRL_keyword.GetValue().strip() == '':
217 validity = False
218 self.display_tctrl_as_valid(tctrl = self._TCTRL_keyword, valid = False)
219 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save keyword expansion without keyword.'), beep = True)
220 self._TCTRL_keyword.SetFocus()
221 else:
222 self.display_tctrl_as_valid(tctrl = self._TCTRL_keyword, valid = True)
223
224 return validity
225
226
227 - def _save_as_new(self):
228 expansion = gmKeywordExpansion.create_keyword_expansion (
229 keyword = self._TCTRL_keyword.GetValue().strip(),
230 text = self._TCTRL_expansion.GetValue(),
231 data_file = self.__data_filename,
232 public = self._RBTN_public.GetValue()
233 )
234
235 if expansion is False:
236 return False
237
238 expansion['is_encrypted'] = self._CHBOX_is_encrypted.IsChecked()
239 expansion.save()
240
241 self.data = expansion
242 return True
243
244
245 - def _save_as_update(self):
246
247 self.data['expansion'] = self._TCTRL_expansion.GetValue().strip()
248 self.data['is_encrypted'] = self._CHBOX_is_encrypted.IsChecked()
249 self.data.save()
250
251 if self.__data_filename is not None:
252 self.data.update_data_from_file(filename = self.__data_filename)
253
254 return True
255
256
257 - def _refresh_as_new(self):
258 self.__data_filename = None
259
260 self._TCTRL_keyword.SetValue('')
261 self._TCTRL_keyword.Enable(True)
262
263 self._LBL_data.Enable(False)
264 self._BTN_select_data_file.Enable(False)
265 self._TCTRL_data_file.SetValue('')
266 self._CHBOX_is_encrypted.SetValue(False)
267 self._CHBOX_is_encrypted.Enable(False)
268
269 self._LBL_text.Enable(False)
270 self._TCTRL_expansion.SetValue('')
271 self._TCTRL_expansion.Enable(False)
272
273 self._RBTN_public.Enable(False)
274 self._RBTN_private.Enable(False)
275 self._RBTN_public.SetValue(1)
276
277 self._TCTRL_keyword.SetFocus()
278
280 self._refresh_from_existing()
281
282 self._TCTRL_keyword.SetValue('%s%s' % (self.data, _('___copy')))
283 self._TCTRL_keyword.Enable(True)
284
285 self._RBTN_public.Enable(True)
286 self._RBTN_private.Enable(True)
287
288 self._TCTRL_keyword.SetFocus()
289
291 self.__data_filename = None
292
293 self._TCTRL_keyword.SetValue(self.data['keyword'])
294 self._TCTRL_keyword.Enable(False)
295
296 if self.data['is_textual']:
297 self._LBL_text.Enable(True)
298 self._TCTRL_expansion.SetValue(gmTools.coalesce(self.data['expansion'], ''))
299
300 self._LBL_data.Enable(False)
301 self._BTN_select_data_file.Enable(False)
302 self._TCTRL_data_file.SetValue('')
303 self._CHBOX_is_encrypted.SetValue(False)
304 self._CHBOX_is_encrypted.Enable(False)
305 else:
306 self._LBL_text.Enable(False)
307 self._TCTRL_expansion.SetValue('')
308
309 self._LBL_data.Enable(True)
310 self._BTN_select_data_file.Enable(True)
311 self._TCTRL_data_file.SetValue(_('Size: %s') % gmTools.size2str(self.data['data_size']))
312 self._CHBOX_is_encrypted.SetValue(self.data['is_encrypted'])
313 self._CHBOX_is_encrypted.Enable(True)
314
315 self._RBTN_public.Enable(False)
316 self._RBTN_private.Enable(False)
317 if self.data['public_expansion']:
318 self._RBTN_public.SetValue(1)
319 else:
320 self._RBTN_private.SetValue(1)
321
322 if self.data['is_textual']:
323 self._TCTRL_expansion.SetFocus()
324 else:
325 self._BTN_select_data_file.SetFocus()
326
327
328
330 self._TCTRL_keyword.Bind(wx.EVT_TEXT, self._on_keyword_modified)
331 self._TCTRL_expansion.Bind(wx.EVT_TEXT, self._on_expansion_modified)
332
333 - def _on_keyword_modified(self, evt):
334 if self._TCTRL_keyword.GetValue().strip() == '':
335 self._LBL_text.Enable(False)
336 self._TCTRL_expansion.Enable(False)
337 self._LBL_data.Enable(False)
338 self._BTN_select_data_file.Enable(False)
339 self._CHBOX_is_encrypted.Enable(False)
340 self._RBTN_public.Enable(False)
341 self._RBTN_private.Enable(False)
342 return
343
344
345
346
347 self._LBL_text.Enable(True)
348 self._TCTRL_expansion.Enable(True)
349 self._LBL_data.Enable(True)
350 self._BTN_select_data_file.Enable(True)
351 self._RBTN_public.Enable(True)
352 self._RBTN_private.Enable(True)
353
355 if self._TCTRL_expansion.GetValue().strip() == '':
356 self._LBL_data.Enable(True)
357 self._BTN_select_data_file.Enable(True)
358 return
359
360 self.__data_filename = None
361 self._LBL_data.Enable(False)
362 self._BTN_select_data_file.Enable(False)
363 self._TCTRL_data_file.SetValue('')
364 self._CHBOX_is_encrypted.Enable(False)
365
367 wildcards = [
368 "%s (*)|*" % _('all files'),
369 "%s (*.*)|*.*" % _('all files (Windows)')
370 ]
371
372 dlg = wx.FileDialog (
373 parent = self,
374 message = _('Choose the file containing the data snippet'),
375 wildcard = '|'.join(wildcards),
376 style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
377 )
378 result = dlg.ShowModal()
379 if result != wx.ID_CANCEL:
380 self.__data_filename = dlg.GetPath()
381 self._TCTRL_data_file.SetValue(self.__data_filename)
382 self._CHBOX_is_encrypted.SetValue(False)
383 self._CHBOX_is_encrypted.Enable(True)
384
385 dlg.Destroy()
386
387
397
398 def edit(expansion=None):
399 ea = cTextExpansionEditAreaPnl(parent, -1, expansion = expansion)
400 dlg = gmEditArea.cGenericEditAreaDlg2(parent, -1, edit_area = ea)
401 if expansion is None:
402 title = _('Adding keyword expansion')
403 else:
404 title = _('Editing keyword expansion "%s"') % expansion['keyword']
405 dlg.SetTitle(title)
406 if dlg.ShowModal() == wx.ID_OK:
407 return True
408
409 return False
410
411 def tooltip(expansion):
412 return expansion.format()
413
414 def refresh(lctrl=None):
415 expansions = gmKeywordExpansion.get_keyword_expansions(order_by = 'is_textual DESC, keyword, public_expansion', force_reload = True)
416 items = [[
417 e['keyword'],
418 gmTools.bool2subst(e['is_textual'], _('text'), _('data')),
419 gmTools.bool2subst(e['public_expansion'], _('public'), _('private'))
420 ] for e in expansions
421 ]
422 lctrl.set_string_items(items)
423 lctrl.set_data(expansions)
424
425
426 gmListWidgets.get_choices_from_list (
427 parent = parent,
428 msg = _('\nSelect the keyword you want to edit !\n'),
429 caption = _('Editing keyword-based expansions ...'),
430 columns = [_('Keyword'), _('Type'), _('Scope')],
431 single_selection = True,
432 edit_callback = edit,
433 new_callback = edit,
434 delete_callback = delete,
435 refresh_callback = refresh,
436 list_tooltip_callback = tooltip
437 )
438
439
440 from Gnumed.wxGladeWidgets import wxgTextExpansionFillInDlg
441
442 -class cTextExpansionFillInDlg(wxgTextExpansionFillInDlg.wxgTextExpansionFillInDlg):
443
444 - def __init__(self, *args, **kwds):
445 wxgTextExpansionFillInDlg.wxgTextExpansionFillInDlg.__init__(self, *args, **kwds)
446
447 self.__expansion = None
448 self.__init_ui()
449
450 - def __init_ui(self):
451 self._LBL_top_part.SetLabel('')
452 font = self._LBL_left_part.GetFont()
453 font.SetPointSize(pointSize = font.GetPointSize() + 1)
454 self._LBL_left_part.SetFont(font)
455 self._LBL_left_part.SetLabel('')
456 self._LBL_left_part.Hide()
457 font = self._TCTRL_fillin.GetFont()
458 font.SetPointSize(pointSize = font.GetPointSize() + 1)
459 self._TCTRL_fillin.SetFont(font)
460 self._TCTRL_fillin.SetValue('')
461 self._TCTRL_fillin.SetBackgroundColour('yellow')
462 self._TCTRL_fillin.Disable()
463 self._TCTRL_fillin.Hide()
464 font = self._LBL_right_part.GetFont()
465 font.SetPointSize(pointSize = font.GetPointSize() + 1)
466 self._LBL_right_part.SetFont(font)
467 self._LBL_right_part.SetLabel('')
468 self._LBL_right_part.Hide()
469 self._LBL_bottom_part.SetLabel('')
470 self._BTN_OK.Disable()
471 self._BTN_forward.Disable()
472 self._BTN_cancel.SetFocus()
473 self._LBL_hint.SetLabel('')
474
476 if self.__expansion is None:
477 return
478
479 if self.__new_expansion:
480 self.__filled_in = self.__expansion
481 self.__new_expansion = False
482 else:
483 self.__filled_in = (
484 self._LBL_top_part.GetLabel() +
485 self.__left_splitter +
486 self._LBL_left_part.GetLabel() +
487 self._TCTRL_fillin.GetValue().strip() +
488 self._LBL_right_part.GetLabel() +
489 self.__right_splitter +
490 self._LBL_bottom_part.GetLabel()
491 )
492
493
494 if regex.search(_text_expansion_fillin_regex, self.__filled_in) is None:
495
496 self._LBL_top_part.SetLabel(self.__filled_in)
497 self._LBL_left_part.SetLabel('')
498 self._LBL_left_part.Hide()
499 self._TCTRL_fillin.SetValue('')
500 self._TCTRL_fillin.Disable()
501 self._TCTRL_fillin.Hide()
502 self._LBL_right_part.SetLabel('')
503 self._LBL_right_part.Hide()
504 self._LBL_bottom_part.SetLabel('')
505 self._BTN_OK.Enable()
506 self._BTN_forward.Disable()
507 self._BTN_OK.SetDefault()
508 return
509
510
511 top, fillin, bottom = regex.split(r'(' + _text_expansion_fillin_regex + r')', self.__filled_in, maxsplit = 1)
512 top_parts = top.rsplit('\n', 1)
513 top_part = top_parts[0]
514 if len(top_parts) == 1:
515 self.__left_splitter = ''
516 left_part = ''
517 else:
518 self.__left_splitter = '\n'
519 left_part = top_parts[1]
520 bottom_parts = bottom.split('\n', 1)
521 if len(bottom_parts) == 1:
522 parts = bottom_parts[0].split(' ', 1)
523 right_part = parts[0]
524 if len(parts) == 1:
525 self.__right_splitter = ''
526 bottom_part = ''
527 else:
528 self.__right_splitter = ' '
529 bottom_part = parts[1]
530 else:
531 self.__right_splitter = '\n'
532 right_part = bottom_parts[0]
533 bottom_part = bottom_parts[1]
534 hint = fillin.strip('$').strip('[').strip(']').strip()
535 self._LBL_top_part.SetLabel(top_part)
536 self._LBL_left_part.SetLabel(left_part)
537 self._LBL_left_part.Show()
538 self._TCTRL_fillin.Enable()
539 self._TCTRL_fillin.SetValue('')
540 self._TCTRL_fillin.Show()
541 self._LBL_right_part.SetLabel(right_part)
542 self._LBL_right_part.Show()
543 self._LBL_bottom_part.SetLabel(bottom_part)
544 self._BTN_OK.Disable()
545 self._BTN_forward.Enable()
546 self._BTN_forward.SetDefault()
547 self._LBL_hint.SetLabel(hint)
548 self._TCTRL_fillin.SetFocus()
549
550 self.Layout()
551 self.Fit()
552
553
554
555 - def _get_expansion(self):
556 return self.__expansion
557
558 - def _set_expansion(self, expansion):
559 self.__expansion = expansion
560 self.__new_expansion = True
561 self.__goto_next_fillin()
562 return
563
564 expansion = property(_get_expansion, _set_expansion)
565
566 - def _get_filled_in(self):
567 return self.__filled_in
568
569 filled_in_expansion = property(_get_filled_in, lambda x:x)
570
571 - def _set_keyword(self, keyword):
572 self.SetTitle(_('Expanding <%s>') % keyword)
573
574 keyword = property(lambda x:x, _set_keyword)
575
576
577
579 self.__goto_next_fillin()
580
581
582 -def expand_keyword(parent=None, keyword=None, show_list_if_needed=False):
638
639
640
641
642 if __name__ == '__main__':
643
644 if len(sys.argv) < 2:
645 sys.exit()
646
647 if sys.argv[1] != 'test':
648 sys.exit()
649
650 from Gnumed.pycommon import gmI18N
651 gmI18N.activate_locale()
652 gmI18N.install_domain(domain = 'gnumed')
653
654
656 expansion = """HEMORR²HAGES: Blutungsrisiko unter OAK
657 --------------------------------------
658 Am Heart J. 2006 Mar;151(3):713-9.
659
660 $[1 oder 0 eingeben]$ H epatische oder Nierenerkrankung
661 $[1 oder 0 eingeben]$ E thanolabusus
662 $[1 oder 0 eingeben]$ M alignom
663 $[1 oder 0 eingeben]$ O ld patient (> 75 Jahre)
664 $[1 oder 0 eingeben]$ R eduzierte Thrombozytenzahl/-funktion
665 $[2 oder 0 eingeben]$ R²ekurrente (frühere) große Blutung
666 $[1 oder 0 eingeben]$ H ypertonie (unkontrolliert)
667 $[1 oder 0 eingeben]$ A nämie
668 $[1 oder 0 eingeben]$ G enetische Faktoren
669 $[1 oder 0 eingeben]$ E xzessives Sturzrisiko
670 $[1 oder 0 eingeben]$ S Schlaganfall in der Anamnese
671 --------------------------------------
672 Summe Rate großer Blutungen
673 pro 100 Patientenjahre
674 0 1.9
675 1 2.5
676 2 5.3
677 3 8.4
678 4 10.4
679 >4 12.3
680
681 Bewertung: Summe = $[Summe ausrechnen und bewerten]$"""
682
683 app = wx.PyWidgetTester(size = (600, 600))
684 dlg = cTextExpansionFillInDlg(None, -1)
685 dlg.expansion = expansion
686 dlg.ShowModal()
687
688
689 test_fillin()
690