__doc__ = """GNUmed list controls and widgets. TODO: From: Rob McMullen To: address@hidden Subject: Re: [wxPython-users] ANN: ColumnSizer mixin for ListCtrl Thanks for all the suggestions, on and off line. There's an update with a new name (ColumnAutoSizeMixin) and better sizing algorithm at: http://trac.flipturn.org/browser/trunk/peppy/lib/column_autosize.py sorting: http://code.activestate.com/recipes/426407/ """ #================================================================ __author__ = "Karsten Hilbert " __license__ = "GPL v2 or later" import sys import types import logging import thread import time import locale import re as regex #import io #import csv import wx import wx.lib.mixins.listctrl as listmixins from Gnumed.pycommon import gmTools _log = logging.getLogger('gm.list_ui') #================================================================ # FIXME: configurable callback on double-click action def get_choices_from_list ( parent=None, msg=None, caption=None, columns=None, choices=None, data=None, selections=None, edit_callback=None, new_callback=None, delete_callback=None, refresh_callback=None, single_selection=False, can_return_empty=False, ignore_OK_button=False, left_extra_button=None, middle_extra_button=None, right_extra_button=None, list_tooltip_callback=None): """Let user select item(s) from a list. - new_callback: () - edit_callback: (item data) - delete_callback: (item data) - refresh_callback: (listctrl) - list_tooltip_callback: (item data) - left/middle/right_extra_button: (label, tooltip, [, wants_list_ctrl]) wants_list_ctrl is optional is called with item_data (or listctrl) as the only argument returns: on [CANCEL]: None on [OK]: if any items selected: if single_selection: the data of the selected item else: list of data of selected items else: if can_return_empty is True AND [OK] button was pressed: empty list else: None """ if caption is None: caption = _('generic multi choice dialog') if single_selection: dlg = cGenericListSelectorDlg(parent, -1, title = caption, msg = msg, style = wx.LC_SINGLE_SEL) else: dlg = cGenericListSelectorDlg(parent, -1, title = caption, msg = msg) dlg.refresh_callback = refresh_callback dlg.edit_callback = edit_callback dlg.new_callback = new_callback dlg.delete_callback = delete_callback dlg.list_tooltip_callback = list_tooltip_callback dlg.ignore_OK_button = ignore_OK_button dlg.left_extra_button = left_extra_button dlg.middle_extra_button = middle_extra_button dlg.right_extra_button = right_extra_button dlg.set_columns(columns = columns) if refresh_callback is None: dlg.set_string_items(items = choices) # list ctrl will refresh anyway if possible dlg.set_column_widths() if data is not None: dlg.set_data(data = data) # can override data set if refresh_callback is not None if selections is not None: dlg.set_selections(selections = selections) dlg.can_return_empty = can_return_empty btn_pressed = dlg.ShowModal() sels = dlg.get_selected_item_data(only_one = single_selection) dlg.Destroy() if btn_pressed == wx.ID_OK: if can_return_empty and (sels is None): return [] return sels return None #---------------------------------------------------------------- from Gnumed.wxGladeWidgets import wxgGenericListSelectorDlg class cGenericListSelectorDlg(wxgGenericListSelectorDlg.wxgGenericListSelectorDlg): """A dialog holding a list and a few buttons to act on the items.""" def __init__(self, *args, **kwargs): try: msg = kwargs['msg'] del kwargs['msg'] except KeyError: msg = None wxgGenericListSelectorDlg.wxgGenericListSelectorDlg.__init__(self, *args, **kwargs) self.message = msg self.left_extra_button = None self.middle_extra_button = None self.right_extra_button = None self.refresh_callback = None # called when new/edit/delete callbacks return True (IOW were not cancelled) self.new_callback = None # called when NEW button pressed, no argument passed in self.edit_callback = None # called when EDIT button pressed, data of topmost selected item passed in self.delete_callback = None # called when DELETE button pressed, data of topmost selected item passed in self.can_return_empty = False self.ignore_OK_button = False # by default do show/use the OK button self.select_callback = None # called when an item is selected, data of topmost selected item passed in self._LCTRL_items.select_callback = self._on_list_item_selected_in_listctrl #------------------------------------------------------------ def set_columns(self, columns=None): self._LCTRL_items.set_columns(columns = columns) #------------------------------------------------------------ def set_column_widths(self, widths=None): self._LCTRL_items.set_column_widths(widths = widths) #------------------------------------------------------------ def set_string_items(self, items=None, reshow=True): self._LCTRL_items.set_string_items(items = items, reshow = reshow) self._LCTRL_items.set_column_widths() #self._LCTRL_items.Select(0) #------------------------------------------------------------ def set_selections(self, selections = None): self._LCTRL_items.set_selections(selections = selections) if selections is None: return if len(selections) == 0: return if self.ignore_OK_button: return self._BTN_ok.Enable(True) self._BTN_ok.SetDefault() #------------------------------------------------------------ def set_data(self, data = None): self._LCTRL_items.set_data(data = data) #------------------------------------------------------------ def get_selected_item_data(self, only_one=False): return self._LCTRL_items.get_selected_item_data(only_one=only_one) #------------------------------------------------------------ # event handlers #------------------------------------------------------------ def _on_list_item_deselected(self, event): if self._LCTRL_items.get_selected_items(only_one=True) == -1: if not self.can_return_empty: self._BTN_cancel.SetDefault() self._BTN_ok.Enable(False) self._BTN_edit.Enable(False) self._BTN_delete.Enable(False) event.Skip() #------------------------------------------------------------ def _on_new_button_pressed(self, event): self.__do_insert() event.Skip() #------------------------------------------------------------ def _on_edit_button_pressed(self, event): # if the edit button *can* be pressed there are *supposed* # to be both an item selected and an editor configured self.__do_edit() event.Skip() #------------------------------------------------------------ def _on_delete_button_pressed(self, event): # if the delete button *can* be pressed there are *supposed* # to be both an item selected and a deletor configured no_items = len(self._LCTRL_items.get_selected_items(only_one = False)) if no_items == 0: return if no_items > 1: style = wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION | wx.STAY_ON_TOP title = _('Deleting list items') question = _( 'You have selected %s list items.\n' '\n' 'Really delete all %s items ?' ) % (no_items, no_items) dlg = wx.MessageDialog(None, question, title, style) btn_pressed = dlg.ShowModal() dlg.Destroy() if btn_pressed == wx.ID_NO: self._LCTRL_items.SetFocus() return if btn_pressed == wx.ID_CANCEL: self._LCTRL_items.SetFocus() return self.__do_delete() event.Skip() #------------------------------------------------------------ def _on_left_extra_button_pressed(self, event): if self.__left_extra_button_wants_list: item_data = self._LCTRL_items else: item_data = self._LCTRL_items.get_selected_item_data(only_one=True) if not self.__left_extra_button_callback(item_data): self._LCTRL_items.SetFocus() return if self.__refresh_callback is None: self._LCTRL_items.SetFocus() return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) finally: wx.EndBusyCursor() self._LCTRL_items.set_column_widths() self._LCTRL_items.SetFocus() #------------------------------------------------------------ def _on_middle_extra_button_pressed(self, event): if self.__middle_extra_button_wants_list: item_data = self._LCTRL_items else: item_data = self._LCTRL_items.get_selected_item_data(only_one=True) if not self.__middle_extra_button_callback(item_data): self._LCTRL_items.SetFocus() return if self.__refresh_callback is None: self._LCTRL_items.SetFocus() return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) finally: wx.EndBusyCursor() self._LCTRL_items.set_column_widths() self._LCTRL_items.SetFocus() #------------------------------------------------------------ def _on_right_extra_button_pressed(self, event): if self.__right_extra_button_wants_list: item_data = self._LCTRL_items else: item_data = self._LCTRL_items.get_selected_item_data(only_one=True) if not self.__right_extra_button_callback(item_data): self._LCTRL_items.SetFocus() return if self.__refresh_callback is None: self._LCTRL_items.SetFocus() return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) finally: wx.EndBusyCursor() self._LCTRL_items.set_column_widths() self._LCTRL_items.SetFocus() #------------------------------------------------------------ # internal helpers #------------------------------------------------------------ def _on_list_item_selected_in_listctrl(self, event): event.Skip() if not self.__ignore_OK_button: self._BTN_ok.SetDefault() self._BTN_ok.Enable(True) if self.edit_callback is not None: self._BTN_edit.Enable(True) if self.delete_callback is not None: self._BTN_delete.Enable(True) if self.__select_callback is not None: item = self._LCTRL_items.get_selected_item_data(only_one = True) self.__select_callback(item) #------------------------------------------------------------ def _on_delete_key_pressed_in_listctrl(self): self.__do_delete() #------------------------------------------------------------ def __do_delete(self): any_deleted = False for item_data in self._LCTRL_items.get_selected_item_data(only_one = False): if item_data is None: continue if self.__delete_callback(item_data): any_deleted = True self._LCTRL_items.SetFocus() if any_deleted is False: return if self.__refresh_callback is None: return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) self._LCTRL_items.set_column_widths() finally: wx.EndBusyCursor() #------------------------------------------------------------ def _on_edit_invoked_in_listctrl(self): self.__do_edit() #------------------------------------------------------------ def __do_edit(self): if not self.__edit_callback(self._LCTRL_items.get_selected_item_data(only_one = True)): self._LCTRL_items.SetFocus() return if self.__refresh_callback is None: self._LCTRL_items.SetFocus() return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) self._LCTRL_items.set_column_widths() self._LCTRL_items.SetFocus() finally: wx.EndBusyCursor() #------------------------------------------------------------ def _on_insert_key_pressed_in_listctrl(self): self.__do_insert() #------------------------------------------------------------ def __do_insert(self): if not self.__new_callback(): self._LCTRL_items.SetFocus() return if self.__refresh_callback is None: self._LCTRL_items.SetFocus() return wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) self._LCTRL_items.set_column_widths() self._LCTRL_items.SetFocus() finally: wx.EndBusyCursor() #------------------------------------------------------------ # properties #------------------------------------------------------------ def _set_ignore_OK_button(self, ignored): self.__ignore_OK_button = ignored if self.__ignore_OK_button: self._BTN_ok.Hide() self._BTN_ok.Enable(False) else: self._BTN_ok.Show() if self._LCTRL_items.get_selected_items(only_one=True) == -1: if self.can_return_empty: self._BTN_ok.Enable(True) else: self._BTN_ok.Enable(False) self._BTN_cancel.SetDefault() ignore_OK_button = property(lambda x:x, _set_ignore_OK_button) #------------------------------------------------------------ def _set_left_extra_button(self, definition): if definition is None: self._BTN_extra_left.Enable(False) self._BTN_extra_left.Hide() self.__left_extra_button_callback = None self.__left_extra_button_wants_list = False return if len(definition) == 3: (label, tooltip, callback) = definition wants_list = False else: (label, tooltip, callback, wants_list) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__left_extra_button_callback = callback self.__left_extra_button_wants_list = wants_list self._BTN_extra_left.SetLabel(label) self._BTN_extra_left.SetToolTipString(tooltip) self._BTN_extra_left.Enable(True) self._BTN_extra_left.Show() left_extra_button = property(lambda x:x, _set_left_extra_button) #------------------------------------------------------------ def _set_middle_extra_button(self, definition): if definition is None: self._BTN_extra_middle.Enable(False) self._BTN_extra_middle.Hide() self.__middle_extra_button_callback = None self.__middle_extra_button_wants_list = False return if len(definition) == 3: (label, tooltip, callback) = definition wants_list = False else: (label, tooltip, callback, wants_list) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__middle_extra_button_callback = callback self.__middle_extra_button_wants_list = wants_list self._BTN_extra_middle.SetLabel(label) self._BTN_extra_middle.SetToolTipString(tooltip) self._BTN_extra_middle.Enable(True) self._BTN_extra_middle.Show() middle_extra_button = property(lambda x:x, _set_middle_extra_button) #------------------------------------------------------------ def _set_right_extra_button(self, definition): if definition is None: self._BTN_extra_right.Enable(False) self._BTN_extra_right.Hide() self.__right_extra_button_callback = None self.__right_extra_button_wants_list = False return if len(definition) == 3: (label, tooltip, callback) = definition wants_list = False else: (label, tooltip, callback, wants_list) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__right_extra_button_callback = callback self.__right_extra_button_wants_list = wants_list self._BTN_extra_right.SetLabel(label) self._BTN_extra_right.SetToolTipString(tooltip) self._BTN_extra_right.Enable(True) self._BTN_extra_right.Show() right_extra_button = property(lambda x:x, _set_right_extra_button) #------------------------------------------------------------ def _get_new_callback(self): return self.__new_callback def _set_new_callback(self, callback): if callback is not None: if self.__refresh_callback is None: raise ValueError('refresh callback must be set before new callback can be set') if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__new_callback = callback if callback is None: self._BTN_new.Enable(False) self._BTN_new.Hide() self._LCTRL_items.new_callback = None else: self._BTN_new.Enable(True) self._BTN_new.Show() self._LCTRL_items.new_callback = self._on_insert_key_pressed_in_listctrl new_callback = property(_get_new_callback, _set_new_callback) #------------------------------------------------------------ def _get_edit_callback(self): return self.__edit_callback def _set_edit_callback(self, callback): if callback is not None: if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__edit_callback = callback if callback is None: self._BTN_edit.Enable(False) self._BTN_edit.Hide() self._LCTRL_items.edit_callback = None else: self._BTN_edit.Enable(True) self._BTN_edit.Show() self._LCTRL_items.edit_callback = self._on_edit_invoked_in_listctrl edit_callback = property(_get_edit_callback, _set_edit_callback) #------------------------------------------------------------ def _get_delete_callback(self): return self.__delete_callback def _set_delete_callback(self, callback): if callback is not None: if self.__refresh_callback is None: raise ValueError('refresh callback must be set before delete callback can be set') if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__delete_callback = callback if callback is None: self._BTN_delete.Enable(False) self._BTN_delete.Hide() self._LCTRL_items.delete_callback = None else: self._BTN_delete.Enable(True) self._BTN_delete.Show() self._LCTRL_items.delete_callback = self._on_delete_key_pressed_in_listctrl delete_callback = property(_get_delete_callback, _set_delete_callback) #------------------------------------------------------------ def _get_refresh_callback(self): return self.__refresh_callback def _set_refresh_callback_helper(self): wx.BeginBusyCursor() try: self.__refresh_callback(lctrl = self._LCTRL_items) finally: wx.EndBusyCursor() self._LCTRL_items.set_column_widths() def _set_refresh_callback(self, callback): if callback is not None: if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__refresh_callback = callback if callback is not None: wx.CallAfter(self._set_refresh_callback_helper) refresh_callback = property(_get_refresh_callback, _set_refresh_callback) #------------------------------------------------------------ def _get_select_callback(self): return self.__select_callback def _set_select_callback(self, callback): if callback is not None: if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__select_callback = callback select_callback = property(_get_select_callback, _set_select_callback) #------------------------------------------------------------ def _get_message(self): return self._LBL_message.GetLabel() def _set_message(self, msg): if msg is None: self._LBL_message.Hide() self._LBL_message.SetLabel(u'') else: self._LBL_message.SetLabel(msg) self._LBL_message.Show() self.Layout() message = property(_get_message, _set_message) #------------------------------------------------------------ def _set_left_extra_button(self, definition): if definition is None: self._BTN_extra_left.Enable(False) self._BTN_extra_left.Hide() self.__left_extra_button_callback = None return (label, tooltip, callback) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__left_extra_button_callback = callback self._BTN_extra_left.SetLabel(label) self._BTN_extra_left.SetToolTipString(tooltip) self._BTN_extra_left.Enable(True) self._BTN_extra_left.Show() left_extra_button = property(lambda x:x, _set_left_extra_button) #------------------------------------------------------------ def _set_middle_extra_button(self, definition): if definition is None: self._BTN_extra_middle.Enable(False) self._BTN_extra_middle.Hide() self.__middle_extra_button_callback = None return (label, tooltip, callback) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__middle_extra_button_callback = callback self._BTN_extra_middle.SetLabel(label) self._BTN_extra_middle.SetToolTipString(tooltip) self._BTN_extra_middle.Enable(True) self._BTN_extra_middle.Show() middle_extra_button = property(lambda x:x, _set_middle_extra_button) #------------------------------------------------------------ def _set_right_extra_button(self, definition): if definition is None: self._BTN_extra_right.Enable(False) self._BTN_extra_right.Hide() self.__right_extra_button_callback = None return (label, tooltip, callback) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__right_extra_button_callback = callback self._BTN_extra_right.SetLabel(label) self._BTN_extra_right.SetToolTipString(tooltip) self._BTN_extra_right.Enable(True) self._BTN_extra_right.Show() right_extra_button = property(lambda x:x, _set_right_extra_button) #================================================================ from Gnumed.wxGladeWidgets import wxgItemPickerDlg class cItemPickerDlg(wxgItemPickerDlg.wxgItemPickerDlg): def __init__(self, *args, **kwargs): try: msg = kwargs['msg'] del kwargs['msg'] except KeyError: msg = None wxgItemPickerDlg.wxgItemPickerDlg.__init__(self, *args, **kwargs) if msg is None: self._LBL_msg.Hide() else: self._LBL_msg.SetLabel(msg) self.ignore_dupes_on_picking = True self._LCTRL_left.activate_callback = self.__pick_selected self.__extra_button_callback = None self._LCTRL_left.SetFocus() #------------------------------------------------------------ # external API #------------------------------------------------------------ def set_columns(self, columns=None, columns_right=None): self._LCTRL_left.set_columns(columns = columns) if columns_right is None: self._LCTRL_right.set_columns(columns = columns) else: if len(columns_right) < len(columns): cols = columns else: cols = columns_right[:len(columns)] self._LCTRL_right.set_columns(columns = cols) #------------------------------------------------------------ def set_string_items(self, items=None, reshow=True): self._LCTRL_left.set_string_items(items = items, reshow = reshow) self._LCTRL_left.set_column_widths() self._LCTRL_right.set_string_items(reshow = False) self._BTN_left2right.Enable(False) self._BTN_right2left.Enable(False) #------------------------------------------------------------ def set_selections(self, selections = None): self._LCTRL_left.set_selections(selections = selections) #------------------------------------------------------------ def set_choices(self, choices=None, data=None, reshow=True): self.set_string_items(items = choices, reshow = reshow) if data is not None: self.set_data(data = data) #------------------------------------------------------------ def set_picks(self, picks=None, data=None, reshow=True): self._LCTRL_right.set_string_items(picks, reshow = reshow) self._LCTRL_right.set_column_widths() if data is not None: self._LCTRL_right.set_data(data = data) #------------------------------------------------------------ def set_data(self, data = None): self._LCTRL_left.set_data(data = data) #------------------------------------------------------------ def get_picks(self): return self._LCTRL_right.get_item_data() picks = property(get_picks, lambda x:x) #------------------------------------------------------------ def _set_extra_button(self, definition): if definition is None: self._BTN_extra.Enable(False) self._BTN_extra.Hide() self.__extra_button_callback = None return (label, tooltip, callback) = definition if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__extra_button_callback = callback self._BTN_extra.SetLabel(label) self._BTN_extra.SetToolTipString(tooltip) self._BTN_extra.Enable(True) self._BTN_extra.Show() extra_button = property(lambda x:x, _set_extra_button) #------------------------------------------------------------ # internal helpers #------------------------------------------------------------ def __pick_selected(self, event=None): if self._LCTRL_left.get_selected_items(only_one = True) == -1: return right_items = self._LCTRL_right.get_string_items() right_data = self._LCTRL_right.get_item_data() if right_data is None: right_data = [] selected_items = self._LCTRL_left.get_selected_string_items(only_one = False) selected_data = self._LCTRL_left.get_selected_item_data(only_one = False) if self.ignore_dupes_on_picking is False: right_items.extend(selected_items) right_data.extend(selected_data) self._LCTRL_right.set_string_items(items = right_items, reshow = True) self._LCTRL_right.set_data(data = right_data) self._LCTRL_right.set_column_widths() # print u'%s <-> %s (items)' % (self._LCTRL_left.ItemCount, self._LCTRL_right.ItemCount) # print u'%s <-> %s (data)' % (len(self._LCTRL_left.data), len(self._LCTRL_right.data)) return for sel_item, sel_data in zip(selected_items, selected_data): if sel_item in right_items: continue right_items.append(sel_item) right_data.append(sel_data) self._LCTRL_right.set_string_items(items = right_items, reshow = True) self._LCTRL_right.set_data(data = right_data) self._LCTRL_right.set_column_widths() # print u'%s <-> %s (items)' % (self._LCTRL_left.ItemCount, self._LCTRL_right.ItemCount) # print u'%s <-> %s (data)' % (len(self._LCTRL_left.data), len(self._LCTRL_right.data)) #------------------------------------------------------------ def __remove_selected_picks(self): if self._LCTRL_right.get_selected_items(only_one = True) == -1: return for item_idx in self._LCTRL_right.get_selected_items(only_one = False): self._LCTRL_right.remove_item(item_idx) if self._LCTRL_right.GetItemCount() == 0: self._BTN_right2left.Enable(False) # print u'%s <-> %s (items)' % (self._LCTRL_left.ItemCount, self._LCTRL_right.ItemCount) # print u'%s <-> %s (data)' % (len(self._LCTRL_left.data), len(self._LCTRL_right.data)) #------------------------------------------------------------ # event handlers #------------------------------------------------------------ def _on_left_list_item_selected(self, event): self._BTN_left2right.Enable(True) #------------------------------------------------------------ def _on_left_list_item_deselected(self, event): if self._LCTRL_left.get_selected_items(only_one = True) == -1: self._BTN_left2right.Enable(False) #------------------------------------------------------------ def _on_right_list_item_selected(self, event): self._BTN_right2left.Enable(True) #------------------------------------------------------------ def _on_right_list_item_deselected(self, event): if self._LCTRL_right.get_selected_items(only_one = True) == -1: self._BTN_right2left.Enable(False) #------------------------------------------------------------ def _on_button_left2right_pressed(self, event): self.__pick_selected() #------------------------------------------------------------ def _on_button_right2left_pressed(self, event): self.__remove_selected_picks() #------------------------------------------------------------ def _on_extra_button_pressed(self, event): self.__extra_button_callback() #------------------------------------------------------------ def _set_left_item_tooltip_callback(self, callback): self._LCTRL_left.item_tooltip_callback = callback left_item_tooltip_callback = property(lambda x:x, _set_left_item_tooltip_callback) #------------------------------------------------------------ def _set_right_item_tooltip_callback(self, callback): self._LCTRL_right.item_tooltip_callback = callback right_item_tooltip_callback = property(lambda x:x, _set_right_item_tooltip_callback) #================================================================ class cReportListCtrl(listmixins.ListCtrlAutoWidthMixin, listmixins.ColumnSorterMixin, wx.ListCtrl): # sorting: at set_string_items() time all items will be # adorned with their initial row number as wxPython data, # this is used later for a) sorting and b) to access # GNUmed data objects associated with rows, # the latter are ordered in initial row number order # at set_data() time map_item_idx2data_idx = wx.ListCtrl.GetItemData sort_order_tags = { True: u' [\u03b1\u0391 \u2192 \u03c9\u03A9]', False: u' [\u03c9\u03A9 \u2192 \u03b1\u0391]' } def __init__(self, *args, **kwargs): self.debug = None try: kwargs['style'] = kwargs['style'] | wx.LC_REPORT except KeyError: kwargs['style'] = wx.LC_REPORT self.__is_single_selection = ((kwargs['style'] & wx.LC_SINGLE_SEL) == wx.LC_SINGLE_SEL) wx.ListCtrl.__init__(self, *args, **kwargs) listmixins.ListCtrlAutoWidthMixin.__init__(self) # required for column sorting self._invalidate_sorting_metadata() # must be called after each (external/direct) list item update listmixins.ColumnSorterMixin.__init__(self, 0) # must be called again after adding columns (why ?) self.__secondary_sort_col = None # for debugging sorting: #self.Bind(wx.EVT_LIST_COL_CLICK, self._on_col_click, self) # cols/rows self.__widths = None self.__data = None # event callbacks self.__select_callback = None self.__activate_callback = None self.__new_callback = None self.__edit_callback = None self.__delete_callback = None # context menu self.__extend_popup_menu_callback = None self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_list_item_rightclicked) # (also handled by MENU key on EVT_LIST_KEY_DOWN) # row tooltips self.__item_tooltip_callback = None self.__tt_last_item = None self.__tt_static_part_base = _( u'Select the items you want to work on.\n' u'\n' u'A discontinuous selection may depend on your holding ' u'down a platform-dependent modifier key (, , ' u'etc) or key combination (eg. or ) ' u'while clicking.' ) self.__tt_static_part = self.__tt_static_part_base self.Bind(wx.EVT_MOTION, self._on_mouse_motion) # search related: self.__search_term = None self.__next_line_to_search = 0 self.__searchable_cols = None # general event handling # self.Bind(wx.EVT_KILL_FOCUS, self._on_lost_focus) self.Bind(wx.EVT_CHAR, self._on_char) # CTRL-F / CTRL-N (LIST_KEY_DOWN does not support modifiers) self.Bind(wx.EVT_LIST_KEY_DOWN, self._on_list_key_down) # context menu key -> context menu / DEL / INS #------------------------------------------------------------ # setters #------------------------------------------------------------ def set_columns(self, columns=None): """(Re)define the columns. Note that this will (have to) delete the items. """ self.ClearAll() self.__tt_last_item = None if columns is None: return for idx in range(len(columns)): self.InsertColumn(idx, columns[idx]) self._invalidate_sorting_metadata() #------------------------------------------------------------ def set_column_widths(self, widths=None): """Set the column width policy. widths = None: use previous policy if any or default policy widths != None: use this policy and remember it for later calls This means there is no way to *revert* to the default policy :-( """ # explicit policy ? if widths is not None: self.__widths = widths for idx in range(len(self.__widths)): self.SetColumnWidth(col = idx, width = self.__widths[idx]) return # previous policy ? if self.__widths is not None: for idx in range(len(self.__widths)): self.SetColumnWidth(col = idx, width = self.__widths[idx]) return # default policy ! if self.GetItemCount() == 0: width_type = wx.LIST_AUTOSIZE_USEHEADER else: width_type = wx.LIST_AUTOSIZE for idx in range(self.GetColumnCount()): self.SetColumnWidth(col = idx, width = width_type) #------------------------------------------------------------ def set_resize_column(self, column='LAST'): if column != 'LAST': if column > self.ColumnCount: return # this column will take up all remaining space courtesy of the width mixin self.setResizeColumn(column) #------------------------------------------------------------ def remove_items_safely(self, max_tries=3): tries = 0 while tries < max_tries: if self.debug is not None: _log.debug('[round %s] <%s>.GetItemCount() before DeleteAllItems(): %s (thread [%s])', tries, self.debug, self.GetItemCount(), thread.get_ident()) if not self.DeleteAllItems(): _log.error('<%s>.DeleteAllItems() failed', self.debug) item_count = self.GetItemCount() if item_count == 0: return True wx.SafeYield(None, True) _log.error('<%s>.GetItemCount() not 0 (rather: %s) after DeleteAllItems()', self.debug, item_count) time.sleep(0.3) wx.SafeYield(None, True) tries += 1 _log.error('<%s>: unable to delete list items after looping %s times', self.debug, max_tries) return False #------------------------------------------------------------ def set_string_items(self, items=None, reshow=True): """All item members must be unicode()able or None.""" wx.BeginBusyCursor() self._invalidate_sorting_metadata() if self.ItemCount == 0: topmost_visible = 0 else: topmost_visible = self.GetFirstSelected() if topmost_visible == -1: topmost_visible = self.GetFocusedItem() if topmost_visible == -1: topmost_visible = self.TopItem if not self.remove_items_safely(max_tries = 3): _log.error("cannot remove items (!?), continuing and hoping for the best") if items is None: self.data = None wx.EndBusyCursor() return # insert new items for item in items: try: item[0] if not isinstance(item, basestring): is_numerically_iterable = True # do not iterate over individual chars in a string, however else: is_numerically_iterable = False except TypeError: is_numerically_iterable = False if is_numerically_iterable: # cannot use errors='replace' since then # None/ints/unicode strings fail to get encoded col_val = unicode(item[0]) row_num = self.InsertStringItem(index = sys.maxint, label = col_val) for col_num in range(1, min(self.GetColumnCount(), len(item))): col_val = unicode(item[col_num]) self.SetStringItem(index = row_num, col = col_num, label = col_val) else: # cannot use errors='replace' since then None/ints/unicode strings fails to get encoded col_val = unicode(item) row_num = self.InsertStringItem(index = sys.maxint, label = col_val) if reshow: if self.ItemCount > 0: if topmost_visible < self.ItemCount: self.EnsureVisible(topmost_visible) self.Focus(topmost_visible) else: self.EnsureVisible(self.ItemCount - 1) self.Focus(self.ItemCount - 1) # set data to be a copy of items self.data = items wx.EndBusyCursor() #-------------------------- def get_string_items(self): rows = [] for row_idx in range(self.ItemCount): row = [] for col_idx in range(self.ColumnCount): row.append(self.GetItem(row_idx, col_idx).GetText()) rows.append(row) return rows # old: only returned first column #return [ self.GetItemText(item_idx) for item_idx in range(self.GetItemCount()) ] string_items = property(get_string_items, set_string_items) #------------------------------------------------------------ def set_data(self, data=None): """ assumed to be a list corresponding to the item indices""" if data is not None: item_count = self.GetItemCount() if len(data) != item_count: _log.debug(' length (%s) must be equal to number of list items (%s) (%s, thread [%s])', len(data), item_count, self.debug, thread.get_ident()) for item_idx in range(len(data)): self.SetItemData(item_idx, item_idx) self.__data = data self.__tt_last_item = None # string data (rows/visible list items) not modified, # so no need to call _update_sorting_metadata return def _get_data(self): # slower than "return self.__data" but helps with detecting # problems with len(__data) != self.GetItemCount(), # also takes care of returning data in the order corresponding # to the order get_string_items returns rows return self.get_item_data() # returns all data since item_idx is None data = property(_get_data, set_data) #------------------------------------------------------------ def set_selections(self, selections=None): # not sure why this is done: if self.GetItemCount() > 0: self.Select(0, on = 0) if selections is None: return for idx in selections: self.Select(idx = idx, on = 1) def __get_selections(self): if self.__is_single_selection: return [self.GetFirstSelected()] selections = [] idx = self.GetFirstSelected() while idx != -1: selections.append(idx) idx = self.GetNextSelected(idx) return selections selections = property(__get_selections, set_selections) #------------------------------------------------------------ # getters #------------------------------------------------------------ def get_column_labels(self): labels = [] for col_idx in range(self.ColumnCount): col = self.GetColumn(col = col_idx) labels.append(col.GetText()) return labels column_labels = property(get_column_labels, lambda x:x) #------------------------------------------------------------ def get_item(self, item_idx=None): if item_idx is not None: return self.GetItem(item_idx) #------------------------------------------------------------ def get_items(self): return [ self.GetItem(item_idx) for item_idx in range(self.GetItemCount()) ] items = property(get_items, lambda x:x) #------------------------------------------------------------ def get_selected_items(self, only_one=False): if self.__is_single_selection or only_one: return self.GetFirstSelected() items = [] idx = self.GetFirstSelected() while idx != -1: items.append(idx) idx = self.GetNextSelected(idx) return items selected_items = property(get_selected_items, lambda x:x) #------------------------------------------------------------ def get_selected_string_items(self, only_one=False): if self.__is_single_selection or only_one: return self.GetItemText(self.GetFirstSelected()) items = [] idx = self.GetFirstSelected() while idx != -1: items.append(self.GetItemText(idx)) idx = self.GetNextSelected(idx) return items selected_string_items = property(get_selected_string_items, lambda x:x) #------------------------------------------------------------ def get_item_data(self, item_idx=None): if self.__data is None: # this isn't entirely clean return None if item_idx is not None: return self.__data[self.map_item_idx2data_idx(item_idx)] # if is None return all data up to item_count, # in case of len(__data) != self.GetItemCount() this # gives the chance to figure out what is going on return [ self.__data[self.map_item_idx2data_idx(item_idx)] for item_idx in range(self.GetItemCount()) ] item_data = property(get_item_data, lambda x:x) #------------------------------------------------------------ def get_selected_item_data(self, only_one=False): if self.__is_single_selection or only_one: if self.__data is None: return None idx = self.GetFirstSelected() if idx == -1: return None return self.__data[self.map_item_idx2data_idx(idx)] data = [] if self.__data is None: return data idx = self.GetFirstSelected() while idx != -1: data.append(self.__data[self.map_item_idx2data_idx(idx)]) idx = self.GetNextSelected(idx) return data selected_item_data = property(get_selected_item_data, lambda x:x) #------------------------------------------------------------ def deselect_selected_item(self): self.Select(idx = self.GetFirstSelected(), on = 0) #------------------------------------------------------------ def remove_item(self, item_idx=None): # do NOT remove the corresponding data because even if # the item pointing to this data instance is gone all # other items will still point to their corresponding # *initial* row numbers #if self.__data is not None: # del self.__data[self.map_item_idx2data_idx(item_idx)] self.DeleteItem(item_idx) self.__tt_last_item = None self._invalidate_sorting_metadata() #------------------------------------------------------------ # internal helpers #------------------------------------------------------------ def __show_context_menu(self, item_idx): items = self.selected_items if self.__is_single_selection: if items is None: no_of_selected_items = 0 else: no_of_selected_items = 1 else: no_of_selected_items = len(items) if no_of_selected_items == 0: title = _('List Item Actions') elif no_of_selected_items == 1: title = _('List Item Actions (selected: 1 entry)') else: title = _('List Item Actions (selected: %s entries)') % no_of_selected_items # build context menu self._context_menu = wx.Menu(title = title) needs_separator = False if self.__new_callback is not None: menu_item = self._context_menu.Append(-1, _('Add ()')) self.Bind(wx.EVT_MENU, self._on_add_row, menu_item) needs_separator = True if self.__edit_callback is not None: menu_item = self._context_menu.Append(-1, _('&Edit')) self.Bind(wx.EVT_MENU, self._on_edit_row, menu_item) needs_separator = True if self.__delete_callback is not None: menu_item = self._context_menu.Append(-1, _('Delete ()')) self.Bind(wx.EVT_MENU, self._on_delete_row, menu_item) needs_separator = True if needs_separator: self._context_menu.AppendSeparator() menu_item = self._context_menu.Append(-1, _('Find ()')) self.Bind(wx.EVT_MENU, self._on_show_search_dialog, menu_item) if self.__search_term is not None: if self.__search_term.strip() != u'': menu_item = self._context_menu.Append(-1, _('Find next [%s] ()') % self.__search_term) self.Bind(wx.EVT_MENU, self._on_search_match, menu_item) self._context_menu.AppendSeparator() col_headers = [] self._rclicked_row_idx = item_idx self._rclicked_row_data = self.get_item_data(item_idx = self._rclicked_row_idx) self._rclicked_row_cells = [] self._rclicked_row_cells_w_hdr = [] for col_idx in range(self.ColumnCount): cell_content = self.GetItem(self._rclicked_row_idx, col_idx).Text.strip() col_header = self.GetColumn(col_idx).m_text.strip() col_headers.append(col_header) self._rclicked_row_cells.append(cell_content) self._rclicked_row_cells_w_hdr.append(u'%s: %s' % (col_header, cell_content)) # # export area # exp_menu = wx.Menu() # menu_item = exp_menu.Append(-1, _('&All rows as CSV')) # self.Bind(wx.EVT_MENU, self._all_rows2export_area, menu_item) # menu_item = exp_menu.Append(-1, _('&Selected rows as CSV')) # self.Bind(wx.EVT_MENU, self._selected_rows2export_area, menu_item) # 1) set clipboard to item clip_menu = wx.Menu() # items for all selected rows if > 1 if no_of_selected_items > 1: # row tooltips menu_item = clip_menu.Append(-1, _('Tooltips of selected rows')) self.Bind(wx.EVT_MENU, self._tooltips2clipboard, menu_item) # row data as formatted text if available menu_item = clip_menu.Append(-1, _('Data (formatted as text) of selected rows')) self.Bind(wx.EVT_MENU, self._datas2clipboard, menu_item) # all fields of the list row as one line of text menu_item = clip_menu.Append(-1, _('Content (as one line each) of selected rows')) self.Bind(wx.EVT_MENU, self._rows2clipboard, menu_item) clip_menu.AppendSeparator() # items for the right-clicked row # row tooltip menu_item = clip_menu.Append(-1, _('Tooltip of current row')) self.Bind(wx.EVT_MENU, self._tooltip2clipboard, menu_item) # row data as formatted text if available if hasattr(self._rclicked_row_data, 'format'): menu_item = clip_menu.Append(-1, _('Data (formatted as text) of current row')) self.Bind(wx.EVT_MENU, self._data2clipboard, menu_item) # all fields of the list row as one line of text menu_item = clip_menu.Append(-1, _('Content (as one line) of current row')) self.Bind(wx.EVT_MENU, self._row2clipboard, menu_item) # all fields of the list row as multiple lines of text menu_item = clip_menu.Append(-1, _('Content (one line per column) of current row')) self.Bind(wx.EVT_MENU, self._row_list2clipboard, menu_item) # each field of the list row as text with and without header clip_menu.AppendSeparator() for col_idx in range(self.ColumnCount): col_content = self._rclicked_row_cells[col_idx].strip() # skip empty field if col_content == u'': continue col_header = col_headers[col_idx] if col_header == u'': # skip one-character fields without header, # actually, no, because in ideographic languages # one character may mean a lot #if len(col_content) == 1: # continue # without column header menu_item = clip_menu.Append(-1, _(u'Column &%s (current row): "%s" [#%s]') % (col_idx+1, shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._col2clipboard, menu_item) else: col_menu = wx.Menu() # with full column header menu_item = col_menu.Append(-1, u'"%s: %s" [#%s]' % (shorten_text(col_header, 8), shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._col_w_hdr2clipboard, menu_item) # without column header menu_item = col_menu.Append(-1, u'"%s" [#%s]' % (shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._col2clipboard, menu_item) clip_menu.AppendMenu(-1, _(u'Column &%s (current row): %s') % (col_idx+1, col_header), col_menu) # 2) append item to current clipboard item clip_add_menu = wx.Menu() # items for all selected rows if > 1 if no_of_selected_items > 1: # row tooltips menu_item = clip_add_menu.Append(-1, _('Tooltips of selected rows')) self.Bind(wx.EVT_MENU, self._add_tooltips2clipboard, menu_item) # row data as formatted text if available menu_item = clip_add_menu.Append(-1, _('Data (formatted as text) of selected rows')) self.Bind(wx.EVT_MENU, self._add_datas2clipboard, menu_item) # all fields of the list row as one line of text menu_item = clip_add_menu.Append(-1, _('Content (as one line each) of selected rows')) self.Bind(wx.EVT_MENU, self._add_rows2clipboard, menu_item) clip_add_menu.AppendSeparator() # items for the right-clicked row # row tooltip menu_item = clip_add_menu.Append(-1, _('Tooltip of current row')) self.Bind(wx.EVT_MENU, self._add_tooltip2clipboard, menu_item) # row data as formatted text if available if hasattr(self._rclicked_row_data, 'format'): menu_item = clip_add_menu.Append(-1, _('Data (formatted as text) of current row')) self.Bind(wx.EVT_MENU, self._add_data2clipboard, menu_item) # all fields of the list row as one line of text menu_item = clip_add_menu.Append(-1, _('Content (as one line) of current row')) self.Bind(wx.EVT_MENU, self._add_row2clipboard, menu_item) # all fields of the list row as multiple lines of text menu_item = clip_add_menu.Append(-1, _('Content (one line per column) of current row')) self.Bind(wx.EVT_MENU, self._add_row_list2clipboard, menu_item) # each field of the list row as text with and without header clip_add_menu.AppendSeparator() for col_idx in range(self.ColumnCount): col_content = self._rclicked_row_cells[col_idx].strip() # skip empty field if col_content == u'': continue col_header = col_headers[col_idx] if col_header == u'': # without column header menu_item = clip_add_menu.Append(-1, _(u'Column &%s (current row): "%s" [#%s]') % (col_idx+1, shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._add_col2clipboard, menu_item) else: col_add_menu = wx.Menu() # with full column header menu_item = col_add_menu.Append(-1, u'"%s: %s" [#%s]' % (shorten_text(col_header, 8), shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._add_col_w_hdr2clipboard, menu_item) # without column header menu_item = col_add_menu.Append(-1, u'"%s" [#%s]' % (shorten_text(col_content, 35), col_idx)) self.Bind(wx.EVT_MENU, self._add_col2clipboard, menu_item) clip_add_menu.AppendMenu(-1, _(u'Column &%s (current row): %s') % (col_idx+1, col_header), col_add_menu) # 3) copy item to export area #export_area_menu = wx.Menu() # put into export area # current row # - fields as one line # - fields as list # - data formatted # - tooltip # selected rows # - fields as lines each # - all data formatted # - all tooltips # - as CSV # send signal # show menu #self._context_menu.AppendMenu(-1, _('Copy to e&xport area...'), exp_menu) self._context_menu.AppendMenu(-1, _('&Copy to clipboard...'), clip_menu) self._context_menu.AppendMenu(-1, _('Append (&+) to clipboard...'), clip_add_menu) if self.__extend_popup_menu_callback is not None: self.__extend_popup_menu_callback(menu = self._context_menu) # show menu self.PopupMenu(self._context_menu, wx.DefaultPosition) self._context_menu.Destroy() return #------------------------------------------------------------ def __handle_delete(self): if self.__delete_callback is None: return no_items = len(self.get_selected_items(only_one = False)) if no_items == 0: return if no_items > 1: question = _( '%s list items are selected.\n' '\n' 'Really delete all %s items ?' ) % (no_items, no_items) title = _('Deleting list items') style = wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION | wx.STAY_ON_TOP dlg = wx.MessageDialog(None, question, title, style) btn_pressed = dlg.ShowModal() dlg.Destroy() if btn_pressed == wx.ID_NO: self.SetFocus() return if btn_pressed == wx.ID_CANCEL: self.SetFocus() return self.__delete_callback() return #------------------------------------------------------------ def __handle_insert(self): if self.__new_callback is None: return self.__new_callback() #------------------------------------------------------------ def __handle_edit(self): if self.__edit_callback is None: return self.__edit_callback() #------------------------------------------------------------ def __show_search_dialog(self): #print "showing search dlg" if self.__search_term is None: #print "no prev search term" default = u'' else: #print "prev search term:", self.__search_term default = self.__search_term search_term = wx.GetTextFromUser ( _(u'Enter the search term:'), _(u'List search'), default_value = default ) if search_term.strip() == u'': #print "empty search term" self.__search_term = None self.__tt_static_part = self.__tt_static_part_base return #print "search term:", search_term self.__search_term = search_term self.__tt_static_part = _( u'Current search term: [[%s]]\n' u'\n' u'%s' ) % ( search_term, self.__tt_static_part_base ) self.__search_match() #------------------------------------------------------------ # event handlers #------------------------------------------------------------ def _on_list_item_activated(self, event): event.Skip() if self.__activate_callback is not None: self.__activate_callback(event) return # default double-click / ENTER action: edit self.__handle_edit() #------------------------------------------------------------ def _on_list_item_selected(self, event): if self.__select_callback is not None: self.__select_callback(event) else: event.Skip() #------------------------------------------------------------ def _on_list_item_rightclicked(self, event): event.Skip() self.__show_context_menu(event.Index) #------------------------------------------------------------ def _on_list_key_down(self, evt): evt.Skip() if evt.KeyCode == wx.WXK_DELETE: self.__handle_delete() return if evt.KeyCode == wx.WXK_INSERT: self.__handle_insert() return if evt.KeyCode == wx.WXK_MENU: self.__show_context_menu(evt.Index) return #------------------------------------------------------------ def _on_char(self, evt): if unichr(evt.GetRawKeyCode()) == u'f': if evt.GetModifiers() == wx.MOD_CMD: #print "search dialog invoked" self.__show_search_dialog() return if unichr(evt.GetRawKeyCode()) == u'n': if evt.GetModifiers() == wx.MOD_CMD: #print "search-next key invoked" self.__search_match() return evt.Skip() return #------------------------------------------------------------ def _on_mouse_motion(self, event): """Update tooltip on mouse motion. for s in dir(wx): if s.startswith('LIST_HITTEST'): print s, getattr(wx, s) LIST_HITTEST_ABOVE 1 LIST_HITTEST_BELOW 2 LIST_HITTEST_NOWHERE 4 LIST_HITTEST_ONITEM 672 LIST_HITTEST_ONITEMICON 32 LIST_HITTEST_ONITEMLABEL 128 LIST_HITTEST_ONITEMRIGHT 256 LIST_HITTEST_ONITEMSTATEICON 512 LIST_HITTEST_TOLEFT 1024 LIST_HITTEST_TORIGHT 2048 """ item_idx, where_flag = self.HitTest(wx.Point(event.X, event.Y)) # pointer on item related area at all ? if where_flag not in [ wx.LIST_HITTEST_ONITEMLABEL, wx.LIST_HITTEST_ONITEMICON, wx.LIST_HITTEST_ONITEMSTATEICON, wx.LIST_HITTEST_ONITEMRIGHT, wx.LIST_HITTEST_ONITEM ]: self.__tt_last_item = None # not on any item self.SetToolTipString(self.__tt_static_part) return # same item as last time around ? if self.__tt_last_item == item_idx: return # remeber the new item we are on self.__tt_last_item = item_idx # HitTest() can return -1 if it so pleases, meaning that no item # was hit or else that maybe there aren't any items (empty list) if item_idx == wx.NOT_FOUND: self.SetToolTipString(self.__tt_static_part) return # do we *have* item data ? if self.__data is None: self.SetToolTipString(self.__tt_static_part) return # under some circumstances the item_idx returned # by HitTest() may be out of bounds with respect to # self.__data, this hints at a sync problem between # setting display items and associated data if ( (item_idx > (len(self.__data) - 1)) or (item_idx < -1) ): self.SetToolTipString(self.__tt_static_part) print "*************************************************************" print "GNUmed has detected an inconsistency with list item tooltips." print "" print "This is not a big problem and you can keep working." print "" print "However, please send us the following so we can fix GNUmed:" print "" print "item idx: %s" % item_idx print 'where flag: %s' % where_flag print 'data list length: %s' % len(self.__data) print "*************************************************************" return dyna_tt = None if self.__item_tooltip_callback is not None: dyna_tt = self.__item_tooltip_callback(self.__data[self.map_item_idx2data_idx(item_idx)]) if dyna_tt is None: self.SetToolTipString(self.__tt_static_part) return self.SetToolTipString(dyna_tt) #------------------------------------------------------------ # context menu event hendlers #------------------------------------------------------------ def _on_add_row(self, evt): evt.Skip() self.__handle_insert() #------------------------------------------------------------ def _on_edit_row(self, evt): evt.Skip() self.__handle_edit() #------------------------------------------------------------ def _on_delete_row(self, evt): evt.Skip() self.__handle_delete() #------------------------------------------------------------ def _on_show_search_dialog(self, evt): evt.Skip() self.__show_search_dialog() #------------------------------------------------------------ def _on_search_match(self, evt): evt.Skip() self.__search_match() # #------------------------------------------------------------ # def _all_rows2export_area(self, evt): # col_labels = self.column_labels # # csv_name = gmTools.get_unique_filename(suffix = '.csv') # csv_file = io.open(csv_name, mode = 'wt', encoding = 'utf8') # csv_writer = csv.DictWriter(csv_file, col_labels) # csv_writer.writeheader() # # for item_idx in range(self.ItemCount): # row_dict = {} # for col_idx in range(self.ColumnCount): # row_dict[col_labels[col_idx]] = self.GetItem(item_idx, col_idx).Text # csv_writer.writerow(row_dict) # # csv_file.close() # # # signal export area # # #------------------------------------------------------------ # def _selected_rows2export_area(self, evt): # col_labels = self.column_labels # # csv_name = gmTools.get_unique_filename(suffix = '.csv') # csv_file = io.open(csv_name, mode = 'wb', encoding = 'utf8') # csv_writer = csv.DictWriter(csv_file, col_labels) # csv_writer.writeheader() # # for item_idx in self.selected_items: # row_dict = {} # for col_idx in range(self.ColumnCount): # row_dict[col_labels[col_idx]] = self.GetItem(item_idx, col_idx).Text # csv_writer.writerow(row_dict) # # csv_file.close() # # # signal export area # #------------------------------------------------------------ def _tooltip2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() if (self.__data is None) or (self.__item_tooltip_callback is None): txt = self.__tt_static_part else: txt = self.__item_tooltip_callback(self.__data[self.map_item_idx2data_idx(self._rclicked_row_idx)]) if txt is None: txt = self.__tt_static_part data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _tooltips2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return if (self.__data is None) or (self.__item_tooltip_callback is None): return tts = [] for data in self.selected_item_data: tt = self.__item_tooltip_callback(data) if tt is None: continue tts.append(tt) if len(tts) == 0: return data_obj = wx.TextDataObject() data_obj.SetText(u'\n\n'.join(tts)) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_tooltip2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text if (self.__data is None) or (self.__item_tooltip_callback is None): txt += self.__tt_static_part else: tmp = self.__item_tooltip_callback(self.__data[self.map_item_idx2data_idx(self._rclicked_row_idx)]) if tmp is None: txt += self.__tt_static_part else: txt += tmp # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_tooltips2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return if (self.__data is None) or (self.__item_tooltip_callback is None): return tts = [] for data in self.selected_item_data: tt = self.__item_tooltip_callback(data) if tt is None: continue tts.append(tt) if len(tts) == 0: return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n\n' txt += u'\n\n'.join(tts) data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _row2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() data_obj.SetText(u' // '.join(self._rclicked_row_cells)) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _rows2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return rows = [] for item_idx in self.selected_items: rows.append(u' // '.join([ self.GetItem(item_idx, col_idx).Text.strip() for col_idx in range(self.ColumnCount) ])) data_obj = wx.TextDataObject() data_obj.SetText(u'\n\n'.join(rows)) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_row2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text txt += u' // '.join(self._rclicked_row_cells) # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_rows2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return rows = [] for item_idx in self.selected_items: rows.append(u' // '.join([ self.GetItem(item_idx, col_idx).Text.strip() for col_idx in range(self.ColumnCount) ])) data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' txt += u'\n\n'.join(rows) data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _row_list2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() data_obj.SetText(u'\n'.join(self._rclicked_row_cells_w_hdr)) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_row_list2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text txt += u'\n'.join(self._rclicked_row_cells_w_hdr) # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _data2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = self._rclicked_row_data.format() if type(txt) == type([]): txt = u'\n'.join(txt) data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _datas2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_as_txt = [] for data in self.selected_item_data: if hasattr(data, 'format'): txt = data.format() if type(txt) is list: txt = u'\n'.join(txt) else: txt = u'%s' % data data_as_txt.append(txt) data_obj = wx.TextDataObject() data_obj.SetText(u'\n\n'.join(data_as_txt)) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_data2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text tmp = self._rclicked_row_data.format() if type(tmp) == type([]): txt += u'\n'.join(tmp) else: txt += tmp # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_datas2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_as_txt = [] for data in self.selected_item_data: if hasattr(data, 'format'): txt = data.format() if type(txt) is list: txt = u'\n'.join(txt) else: txt = u'%s' % data data_as_txt.append(txt) data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' txt += u'\n'.join(data_as_txt) # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _col2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() #col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.split(u':', 1)[0].rstrip(u':')) - 1 col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.rsplit(u'#', 1)[1].rstrip(u']')) txt = self._rclicked_row_cells[col_idx] data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_col2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text #col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.split(u':', 1)[0].rstrip(u':')) - 1 col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.rsplit(u'#', 1)[1].rstrip(u']')) txt += self._rclicked_row_cells[col_idx] # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _col_w_hdr2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() #col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.split(u':', 1)[0].rstrip(u':')) - 1 col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.rsplit(u'#', 1)[1].rstrip(u']')) txt = self._rclicked_row_cells_w_hdr[col_idx] data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ def _add_col_w_hdr2clipboard(self, evt): if wx.TheClipboard.IsOpened(): _log.debug('clipboard already open') return if not wx.TheClipboard.Open(): _log.debug('cannot open clipboard') return data_obj = wx.TextDataObject() txt = u'' # get previous text got_it = wx.TheClipboard.GetData(data_obj) if got_it: txt = data_obj.Text + u'\n' # add text #col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.split(u':', 1)[0].rstrip(u':')) - 1 col_idx = int(self._context_menu.FindItemById(evt.Id).ItemLabel.rsplit(u'#', 1)[1].rstrip(u']')) txt += self._rclicked_row_cells_w_hdr[col_idx] # set text data_obj.SetText(txt) wx.TheClipboard.SetData(data_obj) wx.TheClipboard.Close() #------------------------------------------------------------ # search related methods #------------------------------------------------------------ # def _on_lost_focus(self, evt): # evt.Skip() # if self.__search_dlg is None: # return ## print self.FindFocus() ## print self.__search_dlg # #self.__search_dlg.Close() #------------------------------------------------------------ def __search_match(self): #print "search_match" if self.__search_term is None: #print "no search term" return False if self.__search_term.strip() == u'': #print "empty search term" return False if self.__searchable_cols is None: #print "searchable cols not initialized, now setting" self.searchable_columns = None if len(self.__searchable_cols) == 0: #print "no cols to search" return False #print "on searching for match on:", self.__search_term for row_idx in range(self.__next_line_to_search, self.ItemCount): for col_idx in range(self.ColumnCount): if col_idx not in self.__searchable_cols: continue col_val = self.GetItem(row_idx, col_idx).GetText() if regex.search(self.__search_term, col_val, regex.U | regex.I) is not None: self.Select(row_idx) self.EnsureVisible(row_idx) if row_idx == self.ItemCount - 1: # wrap around self.__next_line_to_search = 0 else: self.__next_line_to_search = row_idx + 1 return True # wrap around self.__next_line_to_search = 0 return False #------------------------------------------------------------ def _set_searchable_cols(self, cols): #print "setting searchable cols to:", cols # zero-based list of which columns to search if cols is None: #print "setting searchable cols to:", range(self.ColumnCount) self.__searchable_cols = range(self.ColumnCount) return # weed out columns to be searched which # don't exist and uniquify them new_cols = {} for col in cols: if col < self.ColumnCount: new_cols[col] = True #print "actually setting searchable cols to:", new_cols.keys() self.__searchable_cols = new_cols.keys() searchable_columns = property(lambda x:x, _set_searchable_cols) #------------------------------------------------------------ # properties #------------------------------------------------------------ def _get_activate_callback(self): return self.__activate_callback def _set_activate_callback(self, callback): if callback is None: self.Unbind(wx.EVT_LIST_ITEM_ACTIVATED) self.__activate_callback = None return if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_list_item_activated) self.__activate_callback = callback activate_callback = property(_get_activate_callback, _set_activate_callback) #------------------------------------------------------------ def _get_select_callback(self): return self.__select_callback def _set_select_callback(self, callback): if callback is None: self.Unbind(wx.EVT_LIST_ITEM_SELECTED) self.__select_callback = None return if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._on_list_item_selected) self.__select_callback = callback select_callback = property(_get_select_callback, _set_select_callback) #------------------------------------------------------------ def _get_delete_callback(self): return self.__delete_callback def _set_delete_callback(self, callback): if callback is None: #self.Unbind(wx.EVT_LIST_ITEM_SELECTED) self.__delete_callback = None return if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) #self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._on_list_item_selected) self.__delete_callback = callback delete_callback = property(_get_delete_callback, _set_delete_callback) #------------------------------------------------------------ def _get_new_callback(self): return self.__new_callback def _set_new_callback(self, callback): if callback is None: self.__new_callback = None return if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__new_callback = callback new_callback = property(_get_new_callback, _set_new_callback) #------------------------------------------------------------ def _get_edit_callback(self): return self.__edit_callback def _set_edit_callback(self, callback): if callback is None: self.__edit_callback = None return if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__edit_callback = callback edit_callback = property(_get_edit_callback, _set_edit_callback) #------------------------------------------------------------ def _set_item_tooltip_callback(self, callback): if callback is not None: if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__item_tooltip_callback = callback # the callback must be a function which takes a single argument # the argument is the data for the item the tooltip is on # the callback must return None if no item tooltip is to be shown # otherwise it must return a string (possibly with \n) item_tooltip_callback = property(lambda x:x, _set_item_tooltip_callback) #------------------------------------------------------------ def _set_extend_popup_menu_callback(self, callback): if callback is not None: if not callable(callback): raise ValueError(' callback is not a callable: %s' % callback) self.__extend_popup_menu_callback = callback extend_popup_menu_callback = property(lambda x:x, _set_extend_popup_menu_callback) #------------------------------------------------------------ # ColumnSorterMixin API #------------------------------------------------------------ def GetListCtrl(self): if self.itemDataMap is None: self._update_sorting_metadata() return self # required #------------------------------------------------------------ def OnSortOrderChanged(self): col_idx, is_ascending = self.GetSortState() if col_idx == -1: _log.debug(u'outside any column (idx: -1) clicked, ignoring') return self._remove_sorting_indicators_from_column_headers() col_state = self.GetColumn(col_idx) col_state.m_text += self.sort_order_tags[is_ascending] self.SetColumn(col_idx, col_state) #------------------------------------------------------------ def GetSecondarySortValues(self, primary_sort_col, primary_item1_idx, primary_item2_idx): return (primary_item1_idx, primary_item2_idx) if self.__secondary_sort_col is None: return (primary_item1_idx, primary_item2_idx) if self.__secondary_sort_col == primary_sort_col: return (primary_item1_idx, primary_item2_idx) secondary_val1 = self.itemDataMap[primary_item1_idx][self.__secondary_sort_col] secondary_val2 = self.itemDataMap[primary_item2_idx][self.__secondary_sort_col] if type(secondary_val1) == type(u'') and type(secondary_val2) == type(u''): secondary_cmp_result = locale.strcoll(secondary_val1, secondary_val2) elif type(secondary_val1) == type('') or type(secondary_val2) == type(''): secondary_cmp_result = locale.strcoll(str(secondary_val1), str(secondary_val2)) else: secondary_cmp_result = cmp(secondary_val1, secondary_val2) if secondary_cmp_result == 0: return (primary_item1_idx, primary_item2_idx) # make the secondary column always sort ascending currently_ascending = self._colSortFlag[primary_sort_col] if currently_ascending: secondary_val1, secondary_val2 = min(secondary_val1, secondary_val2), max(secondary_val1, secondary_val2) else: secondary_val1, secondary_val2 = max(secondary_val1, secondary_val2), min(secondary_val1, secondary_val2) return (secondary_val1, secondary_val2) #------------------------------------------------------------ def _unicode_aware_column_sorter(self, item1, item2): # http://jtauber.com/blog/2006/01/27/python_unicode_collation_algorithm/ # http://stackoverflow.com/questions/1097908/how-do-i-sort-unicode-strings-alphabetically-in-python # PyICU sort_col, is_ascending = self.GetSortState() data1 = self.itemDataMap[item1][sort_col] data2 = self.itemDataMap[item2][sort_col] if type(data1) == type(u'') and type(data2) == type(u''): cmp_result = locale.strcoll(data1, data2) elif type(data1) == type('') or type(data2) == type(''): cmp_result = locale.strcoll(str(data1), str(data2)) else: cmp_result = cmp(data1, data2) #direction = u'ASC' if not is_ascending: cmp_result = -1 * cmp_result #direction = u'DESC' # debug: # if cmp_result < 0: # op1 = u'\u2191 ' # up # op2 = u'\u2193' # down # elif cmp_result > 0: # op2 = u'\u2191 ' # up # op1 = u'\u2193' # down # else: # op1 = u' = ' # op2 = u'' # print u'%s: [%s]%s[%s]%s (%s)' % (direction, data1, op1, data2, op2, cmp_result) return cmp_result # defining our own column sorter does not seem to make # a difference over the default one until we resort to # something other than locale.strcoll/strxform/cmp for # actual sorting #def GetColumnSorter(self): # return self._unicode_aware_column_sorter #------------------------------------------------------------ def _generate_map_for_sorting(self): dict2sort = {} item_count = self.GetItemCount() if item_count == 0: return dict2sort col_count = self.GetColumnCount() for item_idx in range(item_count): dict2sort[item_idx] = () if col_count == 0: continue for col_idx in range(col_count): dict2sort[item_idx] += (self.GetItem(item_idx, col_idx).GetText(), ) # debugging: #print u'[%s:%s] ' % (item_idx, col_idx), self.GetItem(item_idx, col_idx).GetText() return dict2sort #------------------------------------------------------------ def _remove_sorting_indicators_from_column_headers(self): for col_idx in range(self.ColumnCount): col_state = self.GetColumn(col_idx) if col_state.m_text.endswith(self.sort_order_tags[True]): col_state.m_text = col_state.m_text[:-len(self.sort_order_tags[True])] if col_state.m_text.endswith(self.sort_order_tags[False]): col_state.m_text = col_state.m_text[:-len(self.sort_order_tags[False])] self.SetColumn(col_idx, col_state) #------------------------------------------------------------ def _invalidate_sorting_metadata(self): self.itemDataMap = None self.SetColumnCount(self.GetColumnCount()) self._remove_sorting_indicators_from_column_headers() #------------------------------------------------------------ def _update_sorting_metadata(self): # MUST have this name self.itemDataMap = self._generate_map_for_sorting() #------------------------------------------------------------ # for debugging: def _on_col_click(self, event): sort_col, order = self.GetSortState() print u'col clicked: %s / sort col: %s / sort direction: %s / sort flags: %s' % (event.GetColumn(), sort_col, order, self._colSortFlag) # if self.itemDataMap is not None: # print u'sort items data map:' # for key, item in self.itemDataMap.items(): # print key, u' -- ', item event.Skip() #------------------------------------------------------------ def __get_secondary_sort_col(self): return self.__secondary_sort_col def __set_secondary_sort_col(self, col): if col is None: self.__secondary_sort_col = None return if col > self.GetColumnCount(): raise ValueError('cannot secondary-sort on col [%s], there are only [%s] columns', col, self.GetColumnCount()) self.__secondary_sort_col = col secondary_sort_column = property(__get_secondary_sort_col, __set_secondary_sort_col) #================================================================ def shorten_text(text=None, max_length=None): if len(text) <= max_length: return text return text[:max_length-1] + u'\u2026' #================================================================ # main #---------------------------------------------------------------- if __name__ == '__main__': if len(sys.argv) < 2: sys.exit() if sys.argv[1] != 'test': sys.exit() sys.path.insert(0, '../../') from Gnumed.pycommon import gmI18N gmI18N.activate_locale() gmI18N.install_domain() #------------------------------------------------------------ def test_wxMultiChoiceDialog(): app = wx.PyWidgetTester(size = (400, 500)) dlg = wx.MultiChoiceDialog ( parent = None, message = 'test message', caption = 'test caption', choices = ['a', 'b', 'c', 'd', 'e'] ) dlg.ShowModal() sels = dlg.GetSelections() print "selected:" for sel in sels: print sel #------------------------------------------------------------ def test_get_choices_from_list(): def edit(argument): print "editor called with:" print argument def refresh(lctrl): choices = ['a', 'b', 'c'] lctrl.set_string_items(choices) app = wx.PyWidgetTester(size = (200, 50)) chosen = get_choices_from_list ( # msg = 'select a health issue\nfrom the list below\n', caption = 'select health issues', #choices = [['D.M.II', '4'], ['MS', '3'], ['Fraktur', '2']], #columns = ['issue', 'no of episodes'] columns = ['issue'], refresh_callback = refresh #, edit_callback = edit ) print "chosen:" print chosen #------------------------------------------------------------ def test_item_picker_dlg(): app = wx.PyWidgetTester(size = (200, 50)) dlg = cItemPickerDlg(None, -1, msg = 'Pick a few items:') dlg.set_columns(['Plugins'], ['Load in workplace', 'dummy']) #dlg.set_columns(['Plugins'], []) dlg.set_string_items(['patient', 'emr', 'docs']) result = dlg.ShowModal() print result print dlg.get_picks() #------------------------------------------------------------ #test_get_choices_from_list() #test_wxMultiChoiceDialog() test_item_picker_dlg() #================================================================ #