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 choices=None,
39 selections=None,
40 columns=None,
41 data=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
502 self._LCTRL_items.set_string_items(items = items)
503 self._LCTRL_items.set_column_widths()
504
505 if (items is None) or (len(items) == 0):
506 self._BTN_edit.Enable(False)
507 self._BTN_remove.Enable(False)
508 else:
509 self._LCTRL_items.Select(0)
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 self.__data = items
958 self.__tt_last_item = None
959
960 if items is None:
961 return
962
963 for item in items:
964 try:
965 item[0]
966 if not isinstance(item, basestring):
967 is_numerically_iterable = True
968 else:
969 is_numerically_iterable = False
970 except TypeError:
971 is_numerically_iterable = False
972
973 if is_numerically_iterable:
974
975
976 col_val = unicode(item[0])
977 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
978 for col_idx in range(1, min(self.GetColumnCount(), len(item))):
979 col_val = unicode(item[col_idx])
980 self.SetStringItem(index = row_num, col = col_idx, label = col_val)
981 else:
982
983 col_val = unicode(item)
984 row_num = self.InsertStringItem(index = sys.maxint, label = col_val)
985
987 """<data must be a list corresponding to the item indices>"""
988 self.__data = data
989 self.__tt_last_item = None
990
997
998
1000 if self.__is_single_selection:
1001 return [self.GetFirstSelected()]
1002 selections = []
1003 idx = self.GetFirstSelected()
1004 while idx != -1:
1005 selections.append(idx)
1006 idx = self.GetNextSelected(idx)
1007 return selections
1008
1009 selections = property(__get_selections, set_selections)
1010
1011
1012
1014 labels = []
1015 for col_idx in self.GetColumnCount():
1016 col = self.GetColumn(col = col_idx)
1017 labels.append(col.GetText())
1018 return labels
1019
1021 if item_idx is not None:
1022 return self.GetItem(item_idx)
1023
1025 return [ self.GetItem(item_idx) for item_idx in range(self.GetItemCount()) ]
1026
1028 return [ self.GetItemText(item_idx) for item_idx in range(self.GetItemCount()) ]
1029
1031
1032 if self.__is_single_selection or only_one:
1033 return self.GetFirstSelected()
1034
1035 items = []
1036 idx = self.GetFirstSelected()
1037 while idx != -1:
1038 items.append(idx)
1039 idx = self.GetNextSelected(idx)
1040
1041 return items
1042
1044
1045 if self.__is_single_selection or only_one:
1046 return self.GetItemText(self.GetFirstSelected())
1047
1048 items = []
1049 idx = self.GetFirstSelected()
1050 while idx != -1:
1051 items.append(self.GetItemText(idx))
1052 idx = self.GetNextSelected(idx)
1053
1054 return items
1055
1057 if self.__data is None:
1058 return None
1059
1060 if item_idx is not None:
1061 return self.__data[item_idx]
1062
1063 return [ self.__data[item_idx] for item_idx in range(self.GetItemCount()) ]
1064
1066
1067 if self.__is_single_selection or only_one:
1068 if self.__data is None:
1069 return None
1070 idx = self.GetFirstSelected()
1071 if idx == -1:
1072 return None
1073 return self.__data[idx]
1074
1075 data = []
1076 if self.__data is None:
1077 return data
1078 idx = self.GetFirstSelected()
1079 while idx != -1:
1080 data.append(self.__data[idx])
1081 idx = self.GetNextSelected(idx)
1082
1083 return data
1084
1086 self.Select(idx = self.GetFirstSelected(), on = 0)
1087
1089 self.DeleteItem(item_idx)
1090 if self.__data is not None:
1091 del self.__data[item_idx]
1092 self.__tt_last_item = None
1093
1094
1095
1097 event.Skip()
1098 if self.__activate_callback is not None:
1099 self.__activate_callback(event)
1100
1102 event.Skip()
1103 if self.__rightclick_callback is not None:
1104 self.__rightclick_callback(event)
1105
1107 """Update tooltip on mouse motion.
1108
1109 for s in dir(wx):
1110 if s.startswith('LIST_HITTEST'):
1111 print s, getattr(wx, s)
1112
1113 LIST_HITTEST_ABOVE 1
1114 LIST_HITTEST_BELOW 2
1115 LIST_HITTEST_NOWHERE 4
1116 LIST_HITTEST_ONITEM 672
1117 LIST_HITTEST_ONITEMICON 32
1118 LIST_HITTEST_ONITEMLABEL 128
1119 LIST_HITTEST_ONITEMRIGHT 256
1120 LIST_HITTEST_ONITEMSTATEICON 512
1121 LIST_HITTEST_TOLEFT 1024
1122 LIST_HITTEST_TORIGHT 2048
1123 """
1124 item_idx, where_flag = self.HitTest(wx.Point(event.X, event.Y))
1125
1126
1127 if where_flag not in [
1128 wx.LIST_HITTEST_ONITEMLABEL,
1129 wx.LIST_HITTEST_ONITEMICON,
1130 wx.LIST_HITTEST_ONITEMSTATEICON,
1131 wx.LIST_HITTEST_ONITEMRIGHT,
1132 wx.LIST_HITTEST_ONITEM
1133 ]:
1134 self.__tt_last_item = None
1135 self.SetToolTipString(self.__tt_static_part)
1136 return
1137
1138
1139 if self.__tt_last_item == item_idx:
1140 return
1141
1142
1143 self.__tt_last_item = item_idx
1144
1145
1146
1147 if item_idx == wx.NOT_FOUND:
1148 self.SetToolTipString(self.__tt_static_part)
1149 return
1150
1151
1152 if self.__data is None:
1153 self.SetToolTipString(self.__tt_static_part)
1154 return
1155
1156
1157
1158
1159
1160 if (
1161 (item_idx > (len(self.__data) - 1))
1162 or
1163 (item_idx < -1)
1164 ):
1165 self.SetToolTipString(self.__tt_static_part)
1166 print "*************************************************************"
1167 print "GNUmed has detected an inconsistency with list item tooltips."
1168 print ""
1169 print "This is not a big problem and you can keep working."
1170 print ""
1171 print "However, please send us the following so we can fix GNUmed:"
1172 print ""
1173 print "item idx: %s" % item_idx
1174 print 'where flag: %s' % where_flag
1175 print 'data list length: %s' % len(self.__data)
1176 print "*************************************************************"
1177 return
1178
1179 dyna_tt = None
1180 if self.__item_tooltip_callback is not None:
1181 dyna_tt = self.__item_tooltip_callback(self.__data[item_idx])
1182
1183 if dyna_tt is None:
1184 self.SetToolTipString(self.__tt_static_part)
1185 return
1186
1187 self.SetToolTipString(dyna_tt)
1188
1189
1190
1192 return self.__activate_callback
1193
1195 if callback is None:
1196 self.Unbind(wx.EVT_LIST_ITEM_ACTIVATED)
1197 else:
1198 if not callable(callback):
1199 raise ValueError('<activate> callback is not a callable: %s' % callback)
1200 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_list_item_activated)
1201 self.__activate_callback = callback
1202
1203 activate_callback = property(_get_activate_callback, _set_activate_callback)
1204
1206 return self.__rightclick_callback
1207
1209 if callback is None:
1210 self.Unbind(wx.EVT_LIST_ITEM_RIGHT_CLICK)
1211 else:
1212 if not callable(callback):
1213 raise ValueError('<rightclick> callback is not a callable: %s' % callback)
1214 self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_list_item_rightclicked)
1215 self.__rightclick_callback = callback
1216
1217 rightclick_callback = property(_get_rightclick_callback, _set_rightclick_callback)
1218
1224
1225
1226
1227
1228
1229 item_tooltip_callback = property(lambda x:x, _set_item_tooltip_callback)
1230
1231
1232
1233 if __name__ == '__main__':
1234
1235 if len(sys.argv) < 2:
1236 sys.exit()
1237
1238 if sys.argv[1] != 'test':
1239 sys.exit()
1240
1241 from Gnumed.pycommon import gmI18N
1242 gmI18N.activate_locale()
1243 gmI18N.install_domain()
1244
1245
1247 app = wx.PyWidgetTester(size = (400, 500))
1248 dlg = wx.MultiChoiceDialog (
1249 parent = None,
1250 message = 'test message',
1251 caption = 'test caption',
1252 choices = ['a', 'b', 'c', 'd', 'e']
1253 )
1254 dlg.ShowModal()
1255 sels = dlg.GetSelections()
1256 print "selected:"
1257 for sel in sels:
1258 print sel
1259
1261
1262 def edit(argument):
1263 print "editor called with:"
1264 print argument
1265
1266 def refresh(lctrl):
1267 choices = ['a', 'b', 'c']
1268 lctrl.set_string_items(choices)
1269
1270 app = wx.PyWidgetTester(size = (200, 50))
1271 chosen = get_choices_from_list (
1272
1273 caption = 'select health issues',
1274
1275
1276 columns = ['issue'],
1277 refresh_callback = refresh
1278
1279 )
1280 print "chosen:"
1281 print chosen
1282
1284 app = wx.PyWidgetTester(size = (200, 50))
1285 dlg = cItemPickerDlg(None, -1, msg = 'Pick a few items:')
1286 dlg.set_columns(['Plugins'], ['Load in workplace', 'dummy'])
1287
1288 dlg.set_string_items(['patient', 'emr', 'docs'])
1289 result = dlg.ShowModal()
1290 print result
1291 print dlg.get_picks()
1292
1293
1294
1295 test_item_picker_dlg()
1296
1297
1298
1299