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, types
22
23
24 import wx
25 import wx.lib.mixins.listctrl as listmixins
26
27
28 if __name__ == '__main__':
29 sys.path.insert(0, '../../')
30
31
32
33
34 -def get_choices_from_list (
35 parent=None,
36 msg=None,
37 caption=None,
38 columns=None,
39 choices=None,
40 data=None,
41 selections=None,
42 edit_callback=None,
43 new_callback=None,
44 delete_callback=None,
45 refresh_callback=None,
46 single_selection=False,
47 can_return_empty=False,
48 ignore_OK_button=False,
49 left_extra_button=None,
50 middle_extra_button=None,
51 right_extra_button=None,
52 list_tooltip_callback=None):
117
118 from Gnumed.wxGladeWidgets import wxgGenericListSelectorDlg
119
121 """A dialog holding a list and a few buttons to act on the items."""
122
123
124
147
150
153
158
169
172
175
176
177
179 if not self.__ignore_OK_button:
180 self._BTN_ok.SetDefault()
181 self._BTN_ok.Enable(True)
182
183 if self.edit_callback is not None:
184 self._BTN_edit.Enable(True)
185
186 if self.delete_callback is not None:
187 self._BTN_delete.Enable(True)
188
190 if self._LCTRL_items.get_selected_items(only_one=True) == -1:
191 if not self.can_return_empty:
192 self._BTN_cancel.SetDefault()
193 self._BTN_ok.Enable(False)
194 self._BTN_edit.Enable(False)
195 self._BTN_delete.Enable(False)
196
211
228
249
265
281
297
298
299
313
314 ignore_OK_button = property(lambda x:x, _set_ignore_OK_button)
315
331
332 left_extra_button = property(lambda x:x, _set_left_extra_button)
333
349
350 middle_extra_button = property(lambda x:x, _set_middle_extra_button)
351
367
368 right_extra_button = property(lambda x:x, _set_right_extra_button)
369
371 return self.__new_callback
372
374 if callback is not None:
375 if self.refresh_callback is None:
376 raise ValueError('refresh callback must be set before new callback can be set')
377 if not callable(callback):
378 raise ValueError('<new> callback is not a callable: %s' % callback)
379 self.__new_callback = callback
380
381 if callback is None:
382 self._BTN_new.Enable(False)
383 self._BTN_new.Hide()
384 else:
385 self._BTN_new.Enable(True)
386 self._BTN_new.Show()
387
388 new_callback = property(_get_new_callback, _set_new_callback)
389
391 return self.__edit_callback
392
394 if callback is not None:
395 if not callable(callback):
396 raise ValueError('<edit> callback is not a callable: %s' % callback)
397 self.__edit_callback = callback
398
399 if callback is None:
400 self._BTN_edit.Enable(False)
401 self._BTN_edit.Hide()
402 else:
403 self._BTN_edit.Enable(True)
404 self._BTN_edit.Show()
405
406 edit_callback = property(_get_edit_callback, _set_edit_callback)
407
409 return self.__delete_callback
410
412 if callback is not None:
413 if self.refresh_callback is None:
414 raise ValueError('refresh callback must be set before delete callback can be set')
415 if not callable(callback):
416 raise ValueError('<delete> callback is not a callable: %s' % callback)
417 self.__delete_callback = callback
418
419 if callback is None:
420 self._BTN_delete.Enable(False)
421 self._BTN_delete.Hide()
422 else:
423 self._BTN_delete.Enable(True)
424 self._BTN_delete.Show()
425
426 delete_callback = property(_get_delete_callback, _set_delete_callback)
427
429 return self.__refresh_callback
430
438
440 if callback is not None:
441 if not callable(callback):
442 raise ValueError('<refresh> callback is not a callable: %s' % callback)
443 self.__refresh_callback = callback
444 if callback is not None:
445 wx.CallAfter(self._set_refresh_callback_helper)
446
447 refresh_callback = property(_get_refresh_callback, _set_refresh_callback)
448
451
452 list_tooltip_callback = property(lambda x:x, _set_list_tooltip_callback)
453
454
455
457 if message is None:
458 self._LBL_message.Hide()
459 return
460 self._LBL_message.SetLabel(message)
461 self._LBL_message.Show()
462
463 message = property(lambda x:x, _set_message)
464
465 from Gnumed.wxGladeWidgets import wxgGenericListManagerPnl
466
468 """A panel holding a generic multi-column list and action buttions."""
469
495
496
497
500
510
513
516
519
520
521
523 if self.edit_callback is not None:
524 self._BTN_edit.Enable(True)
525 if self.delete_callback is not None:
526 self._BTN_remove.Enable(True)
527 if self.__select_callback is not None:
528 item = self._LCTRL_items.get_selected_item_data(only_one=True)
529 self.__select_callback(item)
530
532 if self._LCTRL_items.get_selected_items(only_one=True) == -1:
533 self._BTN_edit.Enable(False)
534 self._BTN_remove.Enable(False)
535 if self.__select_callback is not None:
536 self.__select_callback(None)
537
548
550 if self.edit_callback is None:
551 return
552 self._on_edit_button_pressed(event)
553
567
581
597
613
629
630
631
633 return self.__new_callback
634
636 if callback is not None:
637 if not callable(callback):
638 raise ValueError('<new> callback is not a callable: %s' % callback)
639 self.__new_callback = callback
640 self._BTN_add.Enable(callback is not None)
641
642 new_callback = property(_get_new_callback, _set_new_callback)
643
645 return self.__select_callback
646
648 if callback is not None:
649 if not callable(callback):
650 raise ValueError('<select> callback is not a callable: %s' % callback)
651 self.__select_callback = callback
652
653 select_callback = property(_get_select_callback, _set_select_callback)
654
656 return self._LBL_message.GetLabel()
657
659 if msg is None:
660 self._LBL_message.Hide()
661 self._LBL_message.SetLabel(u'')
662 else:
663 self._LBL_message.SetLabel(msg)
664 self._LBL_message.Show()
665 self.Layout()
666
667 message = property(_get_message, _set_message)
668
684
685 left_extra_button = property(lambda x:x, _set_left_extra_button)
686
702
703 middle_extra_button = property(lambda x:x, _set_middle_extra_button)
704
720
721 right_extra_button = property(lambda x:x, _set_right_extra_button)
722
723 from Gnumed.wxGladeWidgets import wxgItemPickerDlg
724
726
728
729 try:
730 msg = kwargs['msg']
731 del kwargs['msg']
732 except KeyError:
733 msg = None
734
735 wxgItemPickerDlg.wxgItemPickerDlg.__init__(self, *args, **kwargs)
736
737 if msg is None:
738 self._LBL_msg.Hide()
739 else:
740 self._LBL_msg.SetLabel(msg)
741
742 self._LCTRL_left.activate_callback = self.__pick_selected
743
744 self.__extra_button_callback = None
745
746 self._LCTRL_left.SetFocus()
747
748
749
750 - def set_columns(self, columns=None, columns_right=None):
751 self._LCTRL_left.set_columns(columns = columns)
752 if columns_right is None:
753 self._LCTRL_right.set_columns(columns = columns)
754 else:
755 if len(columns_right) < len(columns):
756 cols = columns
757 else:
758 cols = columns_right[:len(columns)]
759 self._LCTRL_right.set_columns(columns = cols)
760
768
771
776
782
785
788
789 picks = property(get_picks, lambda x:x)
790
806
807 extra_button = property(lambda x:x, _set_extra_button)
808
809
810
830
832 if self._LCTRL_right.get_selected_items(only_one = True) == -1:
833 return
834
835 for item_idx in self._LCTRL_right.get_selected_items(only_one = False):
836 self._LCTRL_right.remove_item(item_idx)
837
838 if self._LCTRL_right.GetItemCount() == 0:
839 self._BTN_right2left.Enable(False)
840
841
842
844 self._BTN_left2right.Enable(True)
845
847 if self._LCTRL_left.get_selected_items(only_one = True) == -1:
848 self._BTN_left2right.Enable(False)
849
851 self._BTN_right2left.Enable(True)
852
854 if self._LCTRL_right.get_selected_items(only_one = True) == -1:
855 self._BTN_right2left.Enable(False)
856
859
862
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
881
882
883
885
886 try:
887 kwargs['style'] = kwargs['style'] | wx.LC_REPORT
888 except KeyError:
889 kwargs['style'] = wx.LC_REPORT
890
891 self.__is_single_selection = ((kwargs['style'] & wx.LC_SINGLE_SEL) == wx.LC_SINGLE_SEL)
892
893 wx.ListCtrl.__init__(self, *args, **kwargs)
894 listmixins.ListCtrlAutoWidthMixin.__init__(self)
895
896 self.__widths = None
897 self.__data = None
898 self.__activate_callback = None
899 self.__rightclick_callback = None
900
901 self.Bind(wx.EVT_MOTION, self._on_mouse_motion)
902 self.__item_tooltip_callback = None
903 self.__tt_last_item = None
904 self.__tt_static_part = _("""Select the items you want to work on.
905
906 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.""")
907
908
909
911 """(Re)define the columns.
912
913 Note that this will (have to) delete the items.
914 """
915 self.ClearAll()
916 self.__tt_last_item = None
917 if columns is None:
918 return
919 for idx in range(len(columns)):
920 self.InsertColumn(idx, columns[idx])
921
923 """Set the column width policy.
924
925 widths = None:
926 use previous policy if any or default policy
927 widths != None:
928 use this policy and remember it for later calls
929
930 This means there is no way to *revert* to the default policy :-(
931 """
932
933 if widths is not None:
934 self.__widths = widths
935 for idx in range(len(self.__widths)):
936 self.SetColumnWidth(col = idx, width = self.__widths[idx])
937 return
938
939
940 if self.__widths is not None:
941 for idx in range(len(self.__widths)):
942 self.SetColumnWidth(col = idx, width = self.__widths[idx])
943 return
944
945
946 if self.GetItemCount() == 0:
947 width_type = wx.LIST_AUTOSIZE_USEHEADER
948 else:
949 width_type = wx.LIST_AUTOSIZE
950 for idx in range(self.GetColumnCount()):
951 self.SetColumnWidth(col = idx, width = width_type)
952
954 """All item members must be unicode()able or None."""
955
956 self.DeleteAllItems()
957 if self.ItemCount != 0:
958 raise ValueError('.ItemCount not 0 after .DeleteAllItems()')
959
960 if items is None:
961 self.data = None
962 return
963
964 for item in items:
965 try:
966 item[0]
967 if not isinstance(item, basestring):
968 is_numerically_iterable = True
969
970 else:
971 is_numerically_iterable = False
972 except TypeError:
973 is_numerically_iterable = False
974
975 if is_numerically_iterable:
976
977
978 col_val = unicode(item[0])
979 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
980 for col_num in range(1, min(self.GetColumnCount(), len(item))):
981 col_val = unicode(item[col_num])
982 self.SetStringItem(index = row_num, col = col_num, label = col_val)
983 else:
984
985 col_val = unicode(item)
986 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
987
988 self.data = items
989
991 """<data must be a list corresponding to the item indices>"""
992 if data is not None:
993 if len(data) != (self.ItemCount):
994 raise ValueError('<data> length (%s) must be equal to number of list items (%s)' % (len(data), self.ItemCount))
995 self.__data = data
996 self.__tt_last_item = None
997 return
998
1001
1002 data = property(_get_data, set_data)
1003
1010
1011
1013 if self.__is_single_selection:
1014 return [self.GetFirstSelected()]
1015 selections = []
1016 idx = self.GetFirstSelected()
1017 while idx != -1:
1018 selections.append(idx)
1019 idx = self.GetNextSelected(idx)
1020 return selections
1021
1022 selections = property(__get_selections, set_selections)
1023
1024
1025
1027 labels = []
1028 for col_idx in self.GetColumnCount():
1029 col = self.GetColumn(col = col_idx)
1030 labels.append(col.GetText())
1031 return labels
1032
1034 if item_idx is not None:
1035 return self.GetItem(item_idx)
1036
1038 return [ self.GetItem(item_idx) for item_idx in range(self.GetItemCount()) ]
1039
1041 return [ self.GetItemText(item_idx) for item_idx in range(self.GetItemCount()) ]
1042
1044
1045 if self.__is_single_selection or only_one:
1046 return self.GetFirstSelected()
1047
1048 items = []
1049 idx = self.GetFirstSelected()
1050 while idx != -1:
1051 items.append(idx)
1052 idx = self.GetNextSelected(idx)
1053
1054 return items
1055
1057
1058 if self.__is_single_selection or only_one:
1059 return self.GetItemText(self.GetFirstSelected())
1060
1061 items = []
1062 idx = self.GetFirstSelected()
1063 while idx != -1:
1064 items.append(self.GetItemText(idx))
1065 idx = self.GetNextSelected(idx)
1066
1067 return items
1068
1070 if self.__data is None:
1071 return None
1072
1073 if item_idx is not None:
1074 return self.__data[item_idx]
1075
1076 return [ self.__data[item_idx] for item_idx in range(self.GetItemCount()) ]
1077
1079
1080 if self.__is_single_selection or only_one:
1081 if self.__data is None:
1082 return None
1083 idx = self.GetFirstSelected()
1084 if idx == -1:
1085 return None
1086 return self.__data[idx]
1087
1088 data = []
1089 if self.__data is None:
1090 return data
1091 idx = self.GetFirstSelected()
1092 while idx != -1:
1093 data.append(self.__data[idx])
1094 idx = self.GetNextSelected(idx)
1095
1096 return data
1097
1099 self.Select(idx = self.GetFirstSelected(), on = 0)
1100
1102 self.DeleteItem(item_idx)
1103 if self.__data is not None:
1104 del self.__data[item_idx]
1105 self.__tt_last_item = None
1106
1107
1108
1110 event.Skip()
1111 if self.__activate_callback is not None:
1112 self.__activate_callback(event)
1113
1115 event.Skip()
1116 if self.__rightclick_callback is not None:
1117 self.__rightclick_callback(event)
1118
1120 """Update tooltip on mouse motion.
1121
1122 for s in dir(wx):
1123 if s.startswith('LIST_HITTEST'):
1124 print s, getattr(wx, s)
1125
1126 LIST_HITTEST_ABOVE 1
1127 LIST_HITTEST_BELOW 2
1128 LIST_HITTEST_NOWHERE 4
1129 LIST_HITTEST_ONITEM 672
1130 LIST_HITTEST_ONITEMICON 32
1131 LIST_HITTEST_ONITEMLABEL 128
1132 LIST_HITTEST_ONITEMRIGHT 256
1133 LIST_HITTEST_ONITEMSTATEICON 512
1134 LIST_HITTEST_TOLEFT 1024
1135 LIST_HITTEST_TORIGHT 2048
1136 """
1137 item_idx, where_flag = self.HitTest(wx.Point(event.X, event.Y))
1138
1139
1140 if where_flag not in [
1141 wx.LIST_HITTEST_ONITEMLABEL,
1142 wx.LIST_HITTEST_ONITEMICON,
1143 wx.LIST_HITTEST_ONITEMSTATEICON,
1144 wx.LIST_HITTEST_ONITEMRIGHT,
1145 wx.LIST_HITTEST_ONITEM
1146 ]:
1147 self.__tt_last_item = None
1148 self.SetToolTipString(self.__tt_static_part)
1149 return
1150
1151
1152 if self.__tt_last_item == item_idx:
1153 return
1154
1155
1156 self.__tt_last_item = item_idx
1157
1158
1159
1160 if item_idx == wx.NOT_FOUND:
1161 self.SetToolTipString(self.__tt_static_part)
1162 return
1163
1164
1165 if self.__data is None:
1166 self.SetToolTipString(self.__tt_static_part)
1167 return
1168
1169
1170
1171
1172
1173 if (
1174 (item_idx > (len(self.__data) - 1))
1175 or
1176 (item_idx < -1)
1177 ):
1178 self.SetToolTipString(self.__tt_static_part)
1179 print "*************************************************************"
1180 print "GNUmed has detected an inconsistency with list item tooltips."
1181 print ""
1182 print "This is not a big problem and you can keep working."
1183 print ""
1184 print "However, please send us the following so we can fix GNUmed:"
1185 print ""
1186 print "item idx: %s" % item_idx
1187 print 'where flag: %s' % where_flag
1188 print 'data list length: %s' % len(self.__data)
1189 print "*************************************************************"
1190 return
1191
1192 dyna_tt = None
1193 if self.__item_tooltip_callback is not None:
1194 dyna_tt = self.__item_tooltip_callback(self.__data[item_idx])
1195
1196 if dyna_tt is None:
1197 self.SetToolTipString(self.__tt_static_part)
1198 return
1199
1200 self.SetToolTipString(dyna_tt)
1201
1202
1203
1205 return self.__activate_callback
1206
1208 if callback is None:
1209 self.Unbind(wx.EVT_LIST_ITEM_ACTIVATED)
1210 else:
1211 if not callable(callback):
1212 raise ValueError('<activate> callback is not a callable: %s' % callback)
1213 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_list_item_activated)
1214 self.__activate_callback = callback
1215
1216 activate_callback = property(_get_activate_callback, _set_activate_callback)
1217
1219 return self.__rightclick_callback
1220
1222 if callback is None:
1223 self.Unbind(wx.EVT_LIST_ITEM_RIGHT_CLICK)
1224 else:
1225 if not callable(callback):
1226 raise ValueError('<rightclick> callback is not a callable: %s' % callback)
1227 self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_list_item_rightclicked)
1228 self.__rightclick_callback = callback
1229
1230 rightclick_callback = property(_get_rightclick_callback, _set_rightclick_callback)
1231
1237
1238
1239
1240
1241
1242 item_tooltip_callback = property(lambda x:x, _set_item_tooltip_callback)
1243
1244
1245
1246 if __name__ == '__main__':
1247
1248 if len(sys.argv) < 2:
1249 sys.exit()
1250
1251 if sys.argv[1] != 'test':
1252 sys.exit()
1253
1254 from Gnumed.pycommon import gmI18N
1255 gmI18N.activate_locale()
1256 gmI18N.install_domain()
1257
1258
1260 app = wx.PyWidgetTester(size = (400, 500))
1261 dlg = wx.MultiChoiceDialog (
1262 parent = None,
1263 message = 'test message',
1264 caption = 'test caption',
1265 choices = ['a', 'b', 'c', 'd', 'e']
1266 )
1267 dlg.ShowModal()
1268 sels = dlg.GetSelections()
1269 print "selected:"
1270 for sel in sels:
1271 print sel
1272
1274
1275 def edit(argument):
1276 print "editor called with:"
1277 print argument
1278
1279 def refresh(lctrl):
1280 choices = ['a', 'b', 'c']
1281 lctrl.set_string_items(choices)
1282
1283 app = wx.PyWidgetTester(size = (200, 50))
1284 chosen = get_choices_from_list (
1285
1286 caption = 'select health issues',
1287
1288
1289 columns = ['issue'],
1290 refresh_callback = refresh
1291
1292 )
1293 print "chosen:"
1294 print chosen
1295
1297 app = wx.PyWidgetTester(size = (200, 50))
1298 dlg = cItemPickerDlg(None, -1, msg = 'Pick a few items:')
1299 dlg.set_columns(['Plugins'], ['Load in workplace', 'dummy'])
1300
1301 dlg.set_string_items(['patient', 'emr', 'docs'])
1302 result = dlg.ShowModal()
1303 print result
1304 print dlg.get_picks()
1305
1306
1307
1308 test_item_picker_dlg()
1309
1310
1311
1312