1 """GNUmed list controls and widgets.
2
3 TODO:
4
5 From: Rob McMullen <rob.mcmullen@gmail.com>
6 To: wxPython-users@lists.wxwidgets.org
7 Subject: Re: [wxPython-users] ANN: ColumnSizer mixin for ListCtrl
8
9 Thanks for all the suggestions, on and off line. There's an update
10 with a new name (ColumnAutoSizeMixin) and better sizing algorithm at:
11
12 http://trac.flipturn.org/browser/trunk/peppy/lib/column_autosize.py
13
14 sorting: http://code.activestate.com/recipes/426407/
15 """
16
17 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
18 __license__ = "GPL v2 or later"
19
20
21 import sys
22 import types
23 import logging
24 import thread
25 import time
26
27
28 import wx
29 import wx.lib.mixins.listctrl as listmixins
30
31
32 _log = logging.getLogger('gm.list_ui')
33
34
35
36 -def get_choices_from_list (
37 parent=None,
38 msg=None,
39 caption=None,
40 columns=None,
41 choices=None,
42 data=None,
43 selections=None,
44 edit_callback=None,
45 new_callback=None,
46 delete_callback=None,
47 refresh_callback=None,
48 single_selection=False,
49 can_return_empty=False,
50 ignore_OK_button=False,
51 left_extra_button=None,
52 middle_extra_button=None,
53 right_extra_button=None,
54 list_tooltip_callback=None):
119
120 from Gnumed.wxGladeWidgets import wxgGenericListSelectorDlg
121
123 """A dialog holding a list and a few buttons to act on the items."""
124
125
126
149
152
155
160
171
174
177
178
179
181 if not self.__ignore_OK_button:
182 self._BTN_ok.SetDefault()
183 self._BTN_ok.Enable(True)
184
185 if self.edit_callback is not None:
186 self._BTN_edit.Enable(True)
187
188 if self.delete_callback is not None:
189 self._BTN_delete.Enable(True)
190
192 if self._LCTRL_items.get_selected_items(only_one=True) == -1:
193 if not self.can_return_empty:
194 self._BTN_cancel.SetDefault()
195 self._BTN_ok.Enable(False)
196 self._BTN_edit.Enable(False)
197 self._BTN_delete.Enable(False)
198
213
230
251
267
283
299
300
301
315
316 ignore_OK_button = property(lambda x:x, _set_ignore_OK_button)
317
333
334 left_extra_button = property(lambda x:x, _set_left_extra_button)
335
351
352 middle_extra_button = property(lambda x:x, _set_middle_extra_button)
353
369
370 right_extra_button = property(lambda x:x, _set_right_extra_button)
371
373 return self.__new_callback
374
376 if callback is not None:
377 if self.refresh_callback is None:
378 raise ValueError('refresh callback must be set before new callback can be set')
379 if not callable(callback):
380 raise ValueError('<new> callback is not a callable: %s' % callback)
381 self.__new_callback = callback
382
383 if callback is None:
384 self._BTN_new.Enable(False)
385 self._BTN_new.Hide()
386 else:
387 self._BTN_new.Enable(True)
388 self._BTN_new.Show()
389
390 new_callback = property(_get_new_callback, _set_new_callback)
391
393 return self.__edit_callback
394
396 if callback is not None:
397 if not callable(callback):
398 raise ValueError('<edit> callback is not a callable: %s' % callback)
399 self.__edit_callback = callback
400
401 if callback is None:
402 self._BTN_edit.Enable(False)
403 self._BTN_edit.Hide()
404 else:
405 self._BTN_edit.Enable(True)
406 self._BTN_edit.Show()
407
408 edit_callback = property(_get_edit_callback, _set_edit_callback)
409
411 return self.__delete_callback
412
414 if callback is not None:
415 if self.refresh_callback is None:
416 raise ValueError('refresh callback must be set before delete callback can be set')
417 if not callable(callback):
418 raise ValueError('<delete> callback is not a callable: %s' % callback)
419 self.__delete_callback = callback
420
421 if callback is None:
422 self._BTN_delete.Enable(False)
423 self._BTN_delete.Hide()
424 else:
425 self._BTN_delete.Enable(True)
426 self._BTN_delete.Show()
427
428 delete_callback = property(_get_delete_callback, _set_delete_callback)
429
431 return self.__refresh_callback
432
440
442 if callback is not None:
443 if not callable(callback):
444 raise ValueError('<refresh> callback is not a callable: %s' % callback)
445 self.__refresh_callback = callback
446 if callback is not None:
447 wx.CallAfter(self._set_refresh_callback_helper)
448
449 refresh_callback = property(_get_refresh_callback, _set_refresh_callback)
450
453
454 list_tooltip_callback = property(lambda x:x, _set_list_tooltip_callback)
455
456
457
459 if message is None:
460 self._LBL_message.Hide()
461 return
462 self._LBL_message.SetLabel(message)
463 self._LBL_message.Show()
464
465 message = property(lambda x:x, _set_message)
466
467 from Gnumed.wxGladeWidgets import wxgGenericListManagerPnl
468
470 """A panel holding a generic multi-column list and action buttions."""
471
497
498
499
502
512
515
518
521
522
523
525 if self.edit_callback is not None:
526 self._BTN_edit.Enable(True)
527 if self.delete_callback is not None:
528 self._BTN_remove.Enable(True)
529 if self.__select_callback is not None:
530 item = self._LCTRL_items.get_selected_item_data(only_one=True)
531 self.__select_callback(item)
532
534 if self._LCTRL_items.get_selected_items(only_one=True) == -1:
535 self._BTN_edit.Enable(False)
536 self._BTN_remove.Enable(False)
537 if self.__select_callback is not None:
538 self.__select_callback(None)
539
550
552 if self.edit_callback is None:
553 return
554 self._on_edit_button_pressed(event)
555
569
583
599
615
631
632
633
635 return self.__new_callback
636
638 if callback is not None:
639 if not callable(callback):
640 raise ValueError('<new> callback is not a callable: %s' % callback)
641 self.__new_callback = callback
642 self._BTN_add.Enable(callback is not None)
643
644 new_callback = property(_get_new_callback, _set_new_callback)
645
647 return self.__select_callback
648
650 if callback is not None:
651 if not callable(callback):
652 raise ValueError('<select> callback is not a callable: %s' % callback)
653 self.__select_callback = callback
654
655 select_callback = property(_get_select_callback, _set_select_callback)
656
658 return self._LBL_message.GetLabel()
659
661 if msg is None:
662 self._LBL_message.Hide()
663 self._LBL_message.SetLabel(u'')
664 else:
665 self._LBL_message.SetLabel(msg)
666 self._LBL_message.Show()
667 self.Layout()
668
669 message = property(_get_message, _set_message)
670
686
687 left_extra_button = property(lambda x:x, _set_left_extra_button)
688
704
705 middle_extra_button = property(lambda x:x, _set_middle_extra_button)
706
722
723 right_extra_button = property(lambda x:x, _set_right_extra_button)
724
725 from Gnumed.wxGladeWidgets import wxgItemPickerDlg
726
728
730
731 try:
732 msg = kwargs['msg']
733 del kwargs['msg']
734 except KeyError:
735 msg = None
736
737 wxgItemPickerDlg.wxgItemPickerDlg.__init__(self, *args, **kwargs)
738
739 if msg is None:
740 self._LBL_msg.Hide()
741 else:
742 self._LBL_msg.SetLabel(msg)
743
744 self._LCTRL_left.activate_callback = self.__pick_selected
745
746 self.__extra_button_callback = None
747
748 self._LCTRL_left.SetFocus()
749
750
751
752 - def set_columns(self, columns=None, columns_right=None):
753 self._LCTRL_left.set_columns(columns = columns)
754 if columns_right is None:
755 self._LCTRL_right.set_columns(columns = columns)
756 else:
757 if len(columns_right) < len(columns):
758 cols = columns
759 else:
760 cols = columns_right[:len(columns)]
761 self._LCTRL_right.set_columns(columns = cols)
762
770
773
778
784
787
790
791 picks = property(get_picks, lambda x:x)
792
808
809 extra_button = property(lambda x:x, _set_extra_button)
810
811
812
832
834 if self._LCTRL_right.get_selected_items(only_one = True) == -1:
835 return
836
837 for item_idx in self._LCTRL_right.get_selected_items(only_one = False):
838 self._LCTRL_right.remove_item(item_idx)
839
840 if self._LCTRL_right.GetItemCount() == 0:
841 self._BTN_right2left.Enable(False)
842
843
844
846 self._BTN_left2right.Enable(True)
847
849 if self._LCTRL_left.get_selected_items(only_one = True) == -1:
850 self._BTN_left2right.Enable(False)
851
853 self._BTN_right2left.Enable(True)
854
856 if self._LCTRL_right.get_selected_items(only_one = True) == -1:
857 self._BTN_right2left.Enable(False)
858
861
864
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
883
884
885
887
888 self.debug = None
889
890 try:
891 kwargs['style'] = kwargs['style'] | wx.LC_REPORT
892 except KeyError:
893 kwargs['style'] = wx.LC_REPORT
894
895 self.__is_single_selection = ((kwargs['style'] & wx.LC_SINGLE_SEL) == wx.LC_SINGLE_SEL)
896
897 wx.ListCtrl.__init__(self, *args, **kwargs)
898 listmixins.ListCtrlAutoWidthMixin.__init__(self)
899
900 self.__widths = None
901 self.__data = None
902 self.__activate_callback = None
903 self.__rightclick_callback = None
904
905 self.Bind(wx.EVT_MOTION, self._on_mouse_motion)
906 self.__item_tooltip_callback = None
907 self.__tt_last_item = None
908 self.__tt_static_part = _("""Select the items you want to work on.
909
910 A discontinuous selection may depend on your holding down a platform-dependent modifier key (<ctrl>, <alt>, etc) or key combination (eg. <ctrl-shift> or <ctrl-alt>) while clicking.""")
911
912
913
915 """(Re)define the columns.
916
917 Note that this will (have to) delete the items.
918 """
919 self.ClearAll()
920 self.__tt_last_item = None
921 if columns is None:
922 return
923 for idx in range(len(columns)):
924 self.InsertColumn(idx, columns[idx])
925
927 """Set the column width policy.
928
929 widths = None:
930 use previous policy if any or default policy
931 widths != None:
932 use this policy and remember it for later calls
933
934 This means there is no way to *revert* to the default policy :-(
935 """
936
937 if widths is not None:
938 self.__widths = widths
939 for idx in range(len(self.__widths)):
940 self.SetColumnWidth(col = idx, width = self.__widths[idx])
941 return
942
943
944 if self.__widths is not None:
945 for idx in range(len(self.__widths)):
946 self.SetColumnWidth(col = idx, width = self.__widths[idx])
947 return
948
949
950 if self.GetItemCount() == 0:
951 width_type = wx.LIST_AUTOSIZE_USEHEADER
952 else:
953 width_type = wx.LIST_AUTOSIZE
954 for idx in range(self.GetColumnCount()):
955 self.SetColumnWidth(col = idx, width = width_type)
956
958 """All item members must be unicode()able or None."""
959
960 wx.BeginBusyCursor()
961
962 loop = 0
963 while True:
964 if loop > 3:
965 _log.debug('unable to delete list items after looping 3 times, continuing and hoping for the best')
966 break
967 loop += 1
968 if self.debug is not None:
969 _log.debug('[round %s] GetItemCount() before DeleteAllItems(): %s (%s, thread [%s])', loop, self.GetItemCount(), self.debug, thread.get_ident())
970 if not self.DeleteAllItems():
971 _log.debug('DeleteAllItems() failed (%s)', self.debug)
972 item_count = self.GetItemCount()
973 if self.debug is not None:
974 _log.debug('GetItemCount() after DeleteAllItems(): %s (%s)', item_count, self.debug)
975 if item_count == 0:
976 break
977 wx.SafeYield(None, True)
978 _log.debug('GetItemCount() not 0 after DeleteAllItems() (%s)', self.debug)
979 time.sleep(0.3)
980 wx.SafeYield(None, True)
981
982 if items is None:
983 self.data = None
984 wx.EndBusyCursor()
985 return
986
987 for item in items:
988 try:
989 item[0]
990 if not isinstance(item, basestring):
991 is_numerically_iterable = True
992
993 else:
994 is_numerically_iterable = False
995 except TypeError:
996 is_numerically_iterable = False
997
998 if is_numerically_iterable:
999
1000
1001 col_val = unicode(item[0])
1002 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
1003 for col_num in range(1, min(self.GetColumnCount(), len(item))):
1004 col_val = unicode(item[col_num])
1005 self.SetStringItem(index = row_num, col = col_num, label = col_val)
1006 else:
1007
1008 col_val = unicode(item)
1009 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
1010
1011 self.data = items
1012
1013 wx.EndBusyCursor()
1014
1016 """<data must be a list corresponding to the item indices>"""
1017 if data is not None:
1018 item_count = self.GetItemCount()
1019 if len(data) != item_count:
1020 _log.debug('<data> length (%s) must be equal to number of list items (%s) (%s, thread [%s])', len(data), item_count, self.debug, thread.get_ident())
1021 self.__data = data
1022 self.__tt_last_item = None
1023 return
1024
1027
1028 data = property(_get_data, set_data)
1029
1036
1037
1039 if self.__is_single_selection:
1040 return [self.GetFirstSelected()]
1041 selections = []
1042 idx = self.GetFirstSelected()
1043 while idx != -1:
1044 selections.append(idx)
1045 idx = self.GetNextSelected(idx)
1046 return selections
1047
1048 selections = property(__get_selections, set_selections)
1049
1050
1051
1053 labels = []
1054 for col_idx in self.GetColumnCount():
1055 col = self.GetColumn(col = col_idx)
1056 labels.append(col.GetText())
1057 return labels
1058
1060 if item_idx is not None:
1061 return self.GetItem(item_idx)
1062
1064 return [ self.GetItem(item_idx) for item_idx in range(self.GetItemCount()) ]
1065
1067 return [ self.GetItemText(item_idx) for item_idx in range(self.GetItemCount()) ]
1068
1070
1071 if self.__is_single_selection or only_one:
1072 return self.GetFirstSelected()
1073
1074 items = []
1075 idx = self.GetFirstSelected()
1076 while idx != -1:
1077 items.append(idx)
1078 idx = self.GetNextSelected(idx)
1079
1080 return items
1081
1083
1084 if self.__is_single_selection or only_one:
1085 return self.GetItemText(self.GetFirstSelected())
1086
1087 items = []
1088 idx = self.GetFirstSelected()
1089 while idx != -1:
1090 items.append(self.GetItemText(idx))
1091 idx = self.GetNextSelected(idx)
1092
1093 return items
1094
1096 if self.__data is None:
1097 return None
1098
1099 if item_idx is not None:
1100 return self.__data[item_idx]
1101
1102 return [ self.__data[item_idx] for item_idx in range(self.GetItemCount()) ]
1103
1105
1106 if self.__is_single_selection or only_one:
1107 if self.__data is None:
1108 return None
1109 idx = self.GetFirstSelected()
1110 if idx == -1:
1111 return None
1112 return self.__data[idx]
1113
1114 data = []
1115 if self.__data is None:
1116 return data
1117 idx = self.GetFirstSelected()
1118 while idx != -1:
1119 data.append(self.__data[idx])
1120 idx = self.GetNextSelected(idx)
1121
1122 return data
1123
1125 self.Select(idx = self.GetFirstSelected(), on = 0)
1126
1128 self.DeleteItem(item_idx)
1129 if self.__data is not None:
1130 del self.__data[item_idx]
1131 self.__tt_last_item = None
1132
1133
1134
1136 event.Skip()
1137 if self.__activate_callback is not None:
1138 self.__activate_callback(event)
1139
1141 event.Skip()
1142 if self.__rightclick_callback is not None:
1143 self.__rightclick_callback(event)
1144
1146 """Update tooltip on mouse motion.
1147
1148 for s in dir(wx):
1149 if s.startswith('LIST_HITTEST'):
1150 print s, getattr(wx, s)
1151
1152 LIST_HITTEST_ABOVE 1
1153 LIST_HITTEST_BELOW 2
1154 LIST_HITTEST_NOWHERE 4
1155 LIST_HITTEST_ONITEM 672
1156 LIST_HITTEST_ONITEMICON 32
1157 LIST_HITTEST_ONITEMLABEL 128
1158 LIST_HITTEST_ONITEMRIGHT 256
1159 LIST_HITTEST_ONITEMSTATEICON 512
1160 LIST_HITTEST_TOLEFT 1024
1161 LIST_HITTEST_TORIGHT 2048
1162 """
1163 item_idx, where_flag = self.HitTest(wx.Point(event.X, event.Y))
1164
1165
1166 if where_flag not in [
1167 wx.LIST_HITTEST_ONITEMLABEL,
1168 wx.LIST_HITTEST_ONITEMICON,
1169 wx.LIST_HITTEST_ONITEMSTATEICON,
1170 wx.LIST_HITTEST_ONITEMRIGHT,
1171 wx.LIST_HITTEST_ONITEM
1172 ]:
1173 self.__tt_last_item = None
1174 self.SetToolTipString(self.__tt_static_part)
1175 return
1176
1177
1178 if self.__tt_last_item == item_idx:
1179 return
1180
1181
1182 self.__tt_last_item = item_idx
1183
1184
1185
1186 if item_idx == wx.NOT_FOUND:
1187 self.SetToolTipString(self.__tt_static_part)
1188 return
1189
1190
1191 if self.__data is None:
1192 self.SetToolTipString(self.__tt_static_part)
1193 return
1194
1195
1196
1197
1198
1199 if (
1200 (item_idx > (len(self.__data) - 1))
1201 or
1202 (item_idx < -1)
1203 ):
1204 self.SetToolTipString(self.__tt_static_part)
1205 print "*************************************************************"
1206 print "GNUmed has detected an inconsistency with list item tooltips."
1207 print ""
1208 print "This is not a big problem and you can keep working."
1209 print ""
1210 print "However, please send us the following so we can fix GNUmed:"
1211 print ""
1212 print "item idx: %s" % item_idx
1213 print 'where flag: %s' % where_flag
1214 print 'data list length: %s' % len(self.__data)
1215 print "*************************************************************"
1216 return
1217
1218 dyna_tt = None
1219 if self.__item_tooltip_callback is not None:
1220 dyna_tt = self.__item_tooltip_callback(self.__data[item_idx])
1221
1222 if dyna_tt is None:
1223 self.SetToolTipString(self.__tt_static_part)
1224 return
1225
1226 self.SetToolTipString(dyna_tt)
1227
1228
1229
1231 return self.__activate_callback
1232
1234 if callback is None:
1235 self.Unbind(wx.EVT_LIST_ITEM_ACTIVATED)
1236 else:
1237 if not callable(callback):
1238 raise ValueError('<activate> callback is not a callable: %s' % callback)
1239 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_list_item_activated)
1240 self.__activate_callback = callback
1241
1242 activate_callback = property(_get_activate_callback, _set_activate_callback)
1243
1245 return self.__rightclick_callback
1246
1248 if callback is None:
1249 self.Unbind(wx.EVT_LIST_ITEM_RIGHT_CLICK)
1250 else:
1251 if not callable(callback):
1252 raise ValueError('<rightclick> callback is not a callable: %s' % callback)
1253 self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_list_item_rightclicked)
1254 self.__rightclick_callback = callback
1255
1256 rightclick_callback = property(_get_rightclick_callback, _set_rightclick_callback)
1257
1263
1264
1265
1266
1267
1268 item_tooltip_callback = property(lambda x:x, _set_item_tooltip_callback)
1269
1270
1271
1272 if __name__ == '__main__':
1273
1274 if len(sys.argv) < 2:
1275 sys.exit()
1276
1277 if sys.argv[1] != 'test':
1278 sys.exit()
1279
1280 sys.path.insert(0, '../../')
1281
1282 from Gnumed.pycommon import gmI18N
1283 gmI18N.activate_locale()
1284 gmI18N.install_domain()
1285
1286
1288 app = wx.PyWidgetTester(size = (400, 500))
1289 dlg = wx.MultiChoiceDialog (
1290 parent = None,
1291 message = 'test message',
1292 caption = 'test caption',
1293 choices = ['a', 'b', 'c', 'd', 'e']
1294 )
1295 dlg.ShowModal()
1296 sels = dlg.GetSelections()
1297 print "selected:"
1298 for sel in sels:
1299 print sel
1300
1302
1303 def edit(argument):
1304 print "editor called with:"
1305 print argument
1306
1307 def refresh(lctrl):
1308 choices = ['a', 'b', 'c']
1309 lctrl.set_string_items(choices)
1310
1311 app = wx.PyWidgetTester(size = (200, 50))
1312 chosen = get_choices_from_list (
1313
1314 caption = 'select health issues',
1315
1316
1317 columns = ['issue'],
1318 refresh_callback = refresh
1319
1320 )
1321 print "chosen:"
1322 print chosen
1323
1325 app = wx.PyWidgetTester(size = (200, 50))
1326 dlg = cItemPickerDlg(None, -1, msg = 'Pick a few items:')
1327 dlg.set_columns(['Plugins'], ['Load in workplace', 'dummy'])
1328
1329 dlg.set_string_items(['patient', 'emr', 'docs'])
1330 result = dlg.ShowModal()
1331 print result
1332 print dlg.get_picks()
1333
1334
1335
1336 test_item_picker_dlg()
1337
1338
1339
1340