Module Gnumed.wxpython.gmPhraseWheel
GNUmed phrasewheel.
A class, extending wx.TextCtrl, which has a drop-down pick list, automatically filled based on the inital letters typed. Based on the interface of Richard Terry's Visual Basic client.
This is based on seminal work by Ian Haywood ihaywood@gnu.org
Functions
def shutdown()
-
Expand source code
def shutdown(): """It can be useful to call this early from your shutdown code to avoid hangs on Notify().""" global _timers _log.info('shutting down %s pending timers', len(_timers)) for timer in _timers: _log.debug('timer [%s]', timer) timer.Stop() _timers = []
It can be useful to call this early from your shutdown code to avoid hangs on Notify().
Classes
class cMultiPhraseWheel (*args, **kwargs)
-
Expand source code
class cMultiPhraseWheel(cPhraseWheelBase): def __init__(self, *args, **kwargs): super(cMultiPhraseWheel, self).__init__(*args, **kwargs) self.phrase_separators = default_phrase_separators self.left_part = '' self.right_part = '' #--------------------------------------------------------- def GetData(self, can_create=False, as_instance=False, link_obj=None): super().GetData(can_create = can_create, link_obj = link_obj) if len(self._data) > 0: if as_instance: return self._data2instance(link_obj = link_obj) return list(self._data.values()) #--------------------------------------------------------- def list2data_dict(self, data_items=None): data_dict = {} for item in data_items: try: list_label = item['list_label'] except KeyError: list_label = item['label'] try: field_label = item['field_label'] except KeyError: field_label = list_label data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label} return data_dict #--------------------------------------------------------- # internal API #--------------------------------------------------------- def _adjust_data_after_text_update(self): # the textctrl display must already be set properly new_data = {} # this way of looping automatically removes stale # data for labels which are no longer displayed for displayed_label in self.displayed_strings: try: new_data[displayed_label] = self._data[displayed_label] except KeyError: # this removes stale data for which there # is no displayed_label anymore pass self.data = new_data #--------------------------------------------------------- def _extract_fragment_to_match_on(self): cursor_pos = self.GetInsertionPoint() entire_input = self.GetValue() if self.__phrase_separators.search(entire_input) is None: self.left_part = '' self.right_part = '' return self.GetValue().strip() string_left_of_cursor = entire_input[:cursor_pos] string_right_of_cursor = entire_input[cursor_pos:] left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ] if len(left_parts) == 0: self.left_part = '' else: self.left_part = '%s%s ' % ( ('%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]), self.__phrase_separators.pattern[0] ) right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ] self.right_part = '%s %s' % ( self.__phrase_separators.pattern[0], ('%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:]) ) val = (left_parts[-1] + right_parts[0]).strip() return val #-------------------------------------------------------- def _update_display_from_picked_item(self, item): val = ('%s%s%s' % ( self.left_part, self._picklist_item2display_string(item = item), self.right_part )).lstrip().lstrip(';').strip() self.suppress_text_update_smarts = True super(cMultiPhraseWheel, self).SetValue(val) # find item end and move cursor to that place: item_end = val.index(item['field_label']) + len(item['field_label']) self.SetInsertionPoint(item_end) return #-------------------------------------------------------- def _update_data_from_picked_item(self, item): # add item to the data self._data[item['field_label']] = item # the textctrl display must already be set properly field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ] new_data = {} # this way of looping automatically removes stale # data for labels which are no longer displayed for field_label in field_labels: try: new_data[field_label] = self._data[field_label] except KeyError: # this removes stale data for which there # is no displayed_label anymore pass self.data = new_data #--------------------------------------------------------- def _dictify_data(self, data=None, value=None): if type(data) == type([]): # useful because self.GetData() returns just such a list return self.list2data_dict(data_items = data) # else assume new-style already-dictified data return data #-------------------------------------------------------- # properties #-------------------------------------------------------- def _set_phrase_separators(self, phrase_separators): """Set phrase separators. - must be a valid regular expression pattern input is split into phrases at boundaries defined by this regex and matching is performed on the phrase the cursor is in only, after selection from picklist phrase_separators[0] is added to the end of the match in the PRW """ self.__phrase_separators = regex.compile(phrase_separators, flags = regex.UNICODE) def _get_phrase_separators(self): return self.__phrase_separators.pattern phrase_separators = property(_get_phrase_separators, _set_phrase_separators) #-------------------------------------------------------- def _get_displayed_strings(self): return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != '' ] displayed_strings = property(_get_displayed_strings)
Widget for smart guessing of user fields, after Richard Terry's interface.
- VB implementation by Richard Terry
- Python port by Ian Haywood for GNUmed
- enhanced by Karsten Hilbert for GNUmed
- enhanced by Ian Haywood for aumed
- enhanced by Karsten Hilbert for GNUmed
@param matcher: a class used to find matches for the current input @type matcher: a L{match provider
} instance or C{None} @param selection_only: whether free-text can be entered without associated data @type selection_only: boolean
@param capitalisation_mode: how to auto-capitalize input, valid values are found in L{capitalize()
} @type capitalisation_mode: integer @param accepted_chars: a regex pattern defining the characters acceptable in the input string, if None no checking is performed @type accepted_chars: None or a string holding a valid regex pattern
@param final_regex: when the control loses focus the input is checked against this regular expression @type final_regex: a string holding a valid regex pattern
@param navigate_after_selection: whether or not to immediately navigate to the widget next-in-tab-order after selecting an item from the dropdown picklist @type navigate_after_selection: boolean
@param picklist_delay: this much time of user inactivity must have passed before the input related smarts kick in and the drop down pick list is shown @type picklist_delay: integer (milliseconds)
Ancestors
- cPhraseWheelBase
- wx._core.TextCtrl
- wx._core.Control
- wx._core.Window
- wx._core.WindowBase
- wx._core.EvtHandler
- wx._core.Object
- wx._core.Trackable
- wx._core.TextEntry
- sip.wrapper
- sip.simplewrapper
Instance variables
prop displayed_strings
-
Expand source code
def _get_displayed_strings(self): return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != '' ]
prop phrase_separators
-
Expand source code
def _get_phrase_separators(self): return self.__phrase_separators.pattern
Methods
def list2data_dict(self, data_items=None)
-
Expand source code
def list2data_dict(self, data_items=None): data_dict = {} for item in data_items: try: list_label = item['list_label'] except KeyError: list_label = item['label'] try: field_label = item['field_label'] except KeyError: field_label = list_label data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label} return data_dict
Inherited members
class cPhraseWheel (parent=None, id=-1, *args, **kwargs)
-
Expand source code
class cPhraseWheel(cPhraseWheelBase): """Standard single-value Phrasewheel.""" #--------------------------------------------------------- def SetData(self, data=None): """Set the data and thereby set the value, too. if possible. If you call SetData() you better be prepared doing a scan of the entire potential match space. The data value must be found in the match space. Args: data: None -> unset data, otherwise a valid (as per match provider) value from the search space """ if data is None: self._data = {} return True # try getting match candidates self._update_candidates_in_picklist('*') # do we require a match ? if self.selection_only: # yes, but we don't have any candidates if len(self._current_match_candidates) == 0: self.display_as_valid(valid = False) return False # among candidates look for a match with <data> for candidate in self._current_match_candidates: if candidate['data'] == data: super(cPhraseWheel, self).SetText ( value = candidate['field_label'], data = data, suppress_smarts = True ) return True # no match found in candidates (but needed) ... if self.selection_only: self.display_as_valid(valid = False) return False # match providers simply return values, so turn them into the dict internally self._data = self._dictify_data(data = data) self.display_as_valid(valid = True) return True #-------------------------------------------------------- # internal API #-------------------------------------------------------- def _show_picklist(self, input2match): # this helps if the current input was already selected from the # list but still is the substring of another pick list item or # else the picklist will re-open just after selection if len(self._data) > 0: self._picklist_dropdown.Hide() return return super(cPhraseWheel, self)._show_picklist(input2match = input2match) #-------------------------------------------------------- def _set_data_to_first_match(self): # data already set ? if len(self._data) > 0: return True # needed ? val = self.GetValue().strip() if val == '': return True # so try self._update_candidates_in_picklist(val = val) for candidate in self._current_match_candidates: if candidate['field_label'] == val: self._update_data_from_picked_item(candidate) self.MarkDirty() # tell listeners about the user's selection for callback in self._on_selection_callbacks: callback(self._data) return True # no exact match found if self.selection_only: gmDispatcher.send(signal = 'statustext', msg = self.selection_only_error_msg) return False return True #--------------------------------------------------------- def _adjust_data_after_text_update(self): self._data = {} #--------------------------------------------------------- def _extract_fragment_to_match_on(self): return self.GetValue().strip()
Standard single-value Phrasewheel.
Ancestors
- cPhraseWheelBase
- wx._core.TextCtrl
- wx._core.Control
- wx._core.Window
- wx._core.WindowBase
- wx._core.EvtHandler
- wx._core.Object
- wx._core.Trackable
- wx._core.TextEntry
- sip.wrapper
- sip.simplewrapper
Subclasses
- cDateInputPhraseWheel
- cFuzzyTimestampInput
- cIntervalPhraseWheel
- cProductOrSubstancePhraseWheel
- cSubstanceIntakeObjectPhraseWheel
- cSubstanceOrDosePhraseWheel
- cSubstancePRW
- cSubstancePatientNotesPhraseWheel
- cSubstancePreparationPhraseWheel
- cSubstanceSchedulePhraseWheel
Methods
def SetData(self, data=None)
-
Expand source code
def SetData(self, data=None): """Set the data and thereby set the value, too. if possible. If you call SetData() you better be prepared doing a scan of the entire potential match space. The data value must be found in the match space. Args: data: None -> unset data, otherwise a valid (as per match provider) value from the search space """ if data is None: self._data = {} return True # try getting match candidates self._update_candidates_in_picklist('*') # do we require a match ? if self.selection_only: # yes, but we don't have any candidates if len(self._current_match_candidates) == 0: self.display_as_valid(valid = False) return False # among candidates look for a match with <data> for candidate in self._current_match_candidates: if candidate['data'] == data: super(cPhraseWheel, self).SetText ( value = candidate['field_label'], data = data, suppress_smarts = True ) return True # no match found in candidates (but needed) ... if self.selection_only: self.display_as_valid(valid = False) return False # match providers simply return values, so turn them into the dict internally self._data = self._dictify_data(data = data) self.display_as_valid(valid = True) return True
Set the data and thereby set the value, too. if possible.
If you call SetData() you better be prepared doing a scan of the entire potential match space.
The data value must be found in the match space.
Args
data
- None -> unset data, otherwise a valid (as per match provider) value from the search space
Inherited members
class cPhraseWheelBase (parent=None, id=-1, *args, **kwargs)
-
Expand source code
class cPhraseWheelBase(wx.TextCtrl): """Widget for smart guessing of user fields, after Richard Terry's interface. - VB implementation by Richard Terry - Python port by Ian Haywood for GNUmed - enhanced by Karsten Hilbert for GNUmed - enhanced by Ian Haywood for aumed - enhanced by Karsten Hilbert for GNUmed @param matcher: a class used to find matches for the current input @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>} instance or C{None} @param selection_only: whether free-text can be entered without associated data @type selection_only: boolean @param capitalisation_mode: how to auto-capitalize input, valid values are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>} @type capitalisation_mode: integer @param accepted_chars: a regex pattern defining the characters acceptable in the input string, if None no checking is performed @type accepted_chars: None or a string holding a valid regex pattern @param final_regex: when the control loses focus the input is checked against this regular expression @type final_regex: a string holding a valid regex pattern @param navigate_after_selection: whether or not to immediately navigate to the widget next-in-tab-order after selecting an item from the dropdown picklist @type navigate_after_selection: boolean @param picklist_delay: this much time of user inactivity must have passed before the input related smarts kick in and the drop down pick list is shown @type picklist_delay: integer (milliseconds) """ def __init__ (self, parent=None, id=-1, *args, **kwargs): # behaviour self.matcher = None self.selection_only = False self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.') self.capitalisation_mode = gmTools.CAPS_NONE self.accepted_chars = None self.final_regex = '.*' self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__ self.navigate_after_selection = False self.picklist_delay = 150 # milliseconds # state tracking self._has_focus = False self._current_match_candidates = [] self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) self.suppress_text_update_smarts = False self.__static_tt = None self.__static_tt_extra = None # list_label - what's been selected from the dropdown list # field_label - what's being shown in the widget after selection for the selected dropdown list item # data - the underlying data corresponding to the selected item self._data = {} self._on_selection_callbacks = [] self._on_lose_focus_callbacks = [] self._on_set_focus_callbacks = [] self._on_modified_callbacks = [] try: kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB | wx.TE_PROCESS_ENTER except KeyError: kwargs['style'] = wx.TE_PROCESS_TAB | wx.TE_PROCESS_ENTER super(cPhraseWheelBase, self).__init__(parent, id, **kwargs) self.__my_startup_color = self.GetBackgroundColour() self.__background_color_without_focus = self.GetBackgroundColour() global color_prw_valid if color_prw_valid is None: color_prw_valid = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) self.__init_dropdown(parent = parent) self.__register_events() self.__init_timer() #-------------------------------------------------------- # external API #-------------------------------------------------------- def GetData(self, can_create:bool=False, as_instance:bool=False, link_obj=None): """Retrieve the data associated with the displayed string(s). Returns: None, if no data available, and unable to create data. Or a data instance, if requested and data is available or create-able. Or the data itself. """ if not self._data: if can_create: self._create_data(link_obj = link_obj) if not self._data: return None if as_instance: return self._data2instance(link_obj = link_obj) return self._data['data'] #--------------------------------------------------------- def SetText(self, value:str='', data=None, suppress_smarts:bool=False): """Set both value and data of phrasewheel. Args: value: some text to display in the control data: the data value represented by the displayed text suppress_smarts: whether to generate data based on value, always True if data is passed in """ if value is None: value = '' if (value == '') and (data is None): self._data = {} super(cPhraseWheelBase, self).SetValue(value) return self.suppress_text_update_smarts = suppress_smarts if data: self.suppress_text_update_smarts = True self._data = self._dictify_data(data = data, value = value) super(cPhraseWheelBase, self).SetValue(value) self.display_as_valid(valid = True) # if data already available if len(self._data) > 0: return True # empty text value ? if value == '': # valid value not required ? if not self.selection_only: return True if not self._set_data_to_first_match(): # not found if self.selection_only: self.display_as_valid(valid = False) return False return True #-------------------------------------------------------- def set_from_instance(self, instance): raise NotImplementedError('[%s]: set_from_instance()' % self.__class__.__name__) #-------------------------------------------------------- def set_from_pk(self, pk): raise NotImplementedError('[%s]: set_from_pk()' % self.__class__.__name__) #-------------------------------------------------------- def display_as_valid(self, valid:bool=True): """Color input field based on content validity. valid: whether the content ist valid (True) or invalid (False), False = partially invalid """ assert valid in [True, False, None], '<valid> must be True or False or None' if valid is True: color2show = self.__my_startup_color elif valid is False: color2show = color_prw_invalid else: color2show = color_prw_partially_invalid if self.IsEnabled(): self.SetBackgroundColour(color2show) self.Refresh() return self.__previous_enabled_bg_color = color2show #-------------------------------------------------------- def Disable(self): self.Enable(enable = False) #-------------------------------------------------------- def Enable(self, enable=True): if self.IsEnabled() is enable: return if self.IsEnabled(): self.__previous_enabled_bg_color = self.GetBackgroundColour() super(cPhraseWheelBase, self).Enable(enable) if enable is True: #self.SetBackgroundColour(color_prw_valid) self.SetBackgroundColour(self.__previous_enabled_bg_color) elif enable is False: self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)) else: raise ValueError('<enable> must be True or False') self.Refresh() #-------------------------------------------------------- # callback API #-------------------------------------------------------- def add_callback_on_selection(self, callback=None): """Add a callback for invocation when a picklist item is selected. The callback will be invoked whenever an item is selected from the picklist. The associated data is passed in as a single parameter. Callbacks must be able to cope with None as the data parameter as that is sent whenever the user changes a previously selected value. """ if not callable(callback): raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback) self._on_selection_callbacks.append(callback) #--------------------------------------------------------- def add_callback_on_set_focus(self, callback=None): """Add a callback for invocation when getting focus.""" if not callable(callback): raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback) self._on_set_focus_callbacks.append(callback) #--------------------------------------------------------- def add_callback_on_lose_focus(self, callback=None): """Add a callback for invocation when losing focus.""" if not callable(callback): raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback) self._on_lose_focus_callbacks.append(callback) #--------------------------------------------------------- def add_callback_on_modified(self, callback=None): """Add a callback for invocation when the content is modified. This callback will NOT be passed any values. """ if not callable(callback): raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback) self._on_modified_callbacks.append(callback) #-------------------------------------------------------- # match provider proxies #-------------------------------------------------------- def set_context(self, context=None, val=None): if self.matcher is not None: self.matcher.set_context(context=context, val=val) #--------------------------------------------------------- def unset_context(self, context=None): if self.matcher is not None: self.matcher.unset_context(context=context) #-------------------------------------------------------- # internal API #-------------------------------------------------------- # picklist handling #-------------------------------------------------------- def __init_dropdown(self, parent = None): szr_dropdown = None try: #raise NotImplementedError # uncomment for testing self.__dropdown_needs_relative_position = False self._picklist_dropdown = wx.PopupWindow(parent) list_parent = self._picklist_dropdown self.__use_fake_popup = False except NotImplementedError: self.__use_fake_popup = True # on MacOSX wx.PopupWindow is not implemented, so emulate it szr_dropdown = wx.BoxSizer(wx.VERTICAL) # using wx.MiniFrame self.__dropdown_needs_relative_position = False self._picklist_dropdown = wx.MiniFrame ( parent = parent, id = -1, style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW ) scroll_win = wx.ScrolledWindow(parent = self._picklist_dropdown, style = wx.NO_BORDER) scroll_win.SetSizer(szr_dropdown) list_parent = scroll_win # using wx.Window #self.__dropdown_needs_relative_position = True #self._picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER) #self._picklist_dropdown.SetSizer(szr_dropdown) #list_parent = self._picklist_dropdown self.__mac_log('dropdown parent: %s' % self._picklist_dropdown.GetParent()) self._picklist = cPhraseWheelListCtrl ( list_parent, style = wx.LC_NO_HEADER ) self._picklist.InsertColumn(0, '') if szr_dropdown is not None: szr_dropdown.Add(self._picklist, 1, wx.EXPAND) self._picklist_dropdown.Hide() #-------------------------------------------------------- def _show_picklist(self, input2match): """Display the pick list if useful.""" self._picklist_dropdown.Hide() if not self._has_focus: return if len(self._current_match_candidates) == 0: return # if only one match and text == match: do not show # picklist but rather pick that match if len(self._current_match_candidates) == 1: candidate = self._current_match_candidates[0] if candidate['field_label'] == input2match: self._update_data_from_picked_item(candidate) return # recalculate size dropdown_size = self._picklist_dropdown.GetSize() border_width = 4 extra_height = 25 # height rows = len(self._current_match_candidates) if rows < 2: # 2 rows minimum rows = 2 if rows > 20: # 20 rows maximum rows = 20 self.__mac_log('dropdown needs rows: %s' % rows) pw_size = self.GetSize() dropdown_size.SetHeight ( (pw_size.height * rows) + border_width + extra_height ) # width dropdown_size.SetWidth(min ( self.Size.width * 2, self.Parent.Size.width )) # recalculate position (pw_x_abs, pw_y_abs) = self.ClientToScreen(0,0) self.__mac_log('phrasewheel position (on screen): x:%s-%s, y:%s-%s' % (pw_x_abs, (pw_x_abs+pw_size.width), pw_y_abs, (pw_y_abs+pw_size.height))) dropdown_new_x = pw_x_abs dropdown_new_y = pw_y_abs + pw_size.height self.__mac_log('desired dropdown position (on screen): x:%s-%s, y:%s-%s' % (dropdown_new_x, (dropdown_new_x+dropdown_size.width), dropdown_new_y, (dropdown_new_y+dropdown_size.height))) self.__mac_log('desired dropdown size: %s' % dropdown_size) # reaches beyond screen ? if (dropdown_new_y + dropdown_size.height) > self._screenheight: self.__mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight) max_height = self._screenheight - dropdown_new_y - 4 self.__mac_log('max dropdown height would be: %s' % max_height) if max_height > ((pw_size.height * 2) + 4): dropdown_size.SetHeight(max_height) self.__mac_log('possible dropdown position (on screen): x:%s-%s, y:%s-%s' % (dropdown_new_x, (dropdown_new_x+dropdown_size.width), dropdown_new_y, (dropdown_new_y+dropdown_size.height))) self.__mac_log('possible dropdown size: %s' % dropdown_size) # now set dimensions self._picklist_dropdown.SetSize(dropdown_size) self._picklist.SetSize(self._picklist_dropdown.GetClientSize()) self.__mac_log('pick list size set to: %s' % self._picklist_dropdown.GetSize()) if self.__dropdown_needs_relative_position: dropdown_new_x, dropdown_new_y = self._picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y) self._picklist_dropdown.Move(dropdown_new_x, dropdown_new_y) # select first value self._picklist.Select(0) # and show it self._picklist_dropdown.Show(True) # dropdown_top_left = self._picklist_dropdown.ClientToScreen(0,0) # dropdown_size = self._picklist_dropdown.GetSize() # dropdown_bottom_right = self._picklist_dropdown.ClientToScreen(dropdown_size.width, dropdown_size.height) # self.__mac_log('dropdown placement now (on screen): x:%s-%s, y:%s-%s' % ( # dropdown_top_left[0], # dropdown_bottom_right[0], # dropdown_top_left[1], # dropdown_bottom_right[1]) # ) #-------------------------------------------------------- def _hide_picklist(self): """Hide the pick list.""" self._picklist_dropdown.Hide() #-------------------------------------------------------- def _select_picklist_row(self, new_row_idx=None, old_row_idx=None): """Mark the given picklist row as selected.""" if old_row_idx is not None: pass # FIXME: do we need unselect here ? Select() should do it for us self._picklist.Select(new_row_idx) self._picklist.EnsureVisible(new_row_idx) #-------------------------------------------------------- def _picklist_item2display_string(self, item=None): """Get string to display in the field for the given picklist item.""" if item is None: item = self._picklist.get_selected_item() try: return item['field_label'] except KeyError: pass try: return item['list_label'] except KeyError: pass try: return item['label'] except KeyError: return '<no field_*/list_*/label in item>' #return self._picklist.GetItemText(self._picklist.GetFirstSelected()) #-------------------------------------------------------- def _update_display_from_picked_item(self, item): """Update the display to show item strings.""" # default to single phrase display_string = self._picklist_item2display_string(item = item) self.suppress_text_update_smarts = True super(cPhraseWheelBase, self).SetValue(display_string) # in single-phrase phrasewheels always set cursor to end of string self.SetInsertionPoint(self.GetLastPosition()) return #-------------------------------------------------------- # match generation #-------------------------------------------------------- def _extract_fragment_to_match_on(self): raise NotImplementedError('[%s]: fragment extraction not implemented' % self.__class__.__name__) #--------------------------------------------------------- def _update_candidates_in_picklist(self, val): """Get candidates matching the currently typed input.""" # get all currently matching items self._current_match_candidates = [] if self.matcher is not None: matched, self._current_match_candidates = self.matcher.getMatches(val) self._picklist.SetItems(self._current_match_candidates) #-------------------------------------------------------- # tooltip handling #-------------------------------------------------------- def _get_data_tooltip(self): # child classes can override this to provide # per data item dynamic tooltips, # by default do not support dynamic tooltip parts: return None #-------------------------------------------------------- def __recalculate_tooltip(self): """Calculate dynamic tooltip part based on data item. - called via ._set_data() each time property .data (-> .__data) is set - hence also called the first time data is set - the static tooltip can be set any number of ways before that - only when data is first set does the dynamic part become relevant - hence it is sufficient to remember the static part when .data is set for the first time """ if self.__static_tt is None: if self.ToolTip is None: self.__static_tt = '' else: self.__static_tt = self.ToolTip.Tip # need to always calculate static part because # the dynamic part can have *become* None, again, # in which case we want to be able to re-set the # tooltip to the static part static_part = self.__static_tt if (self.__static_tt_extra) is not None and (self.__static_tt_extra.strip() != ''): static_part = '%s\n\n%s' % ( static_part, self.__static_tt_extra ) dynamic_part = self._get_data_tooltip() if dynamic_part is None: self.SetToolTip(static_part) return if static_part == '': tt = dynamic_part else: if dynamic_part.strip() == '': tt = static_part else: tt = '%s\n\n%s\n\n%s' % ( dynamic_part, gmTools.u_box_horiz_single * 32, static_part ) self.SetToolTip(tt) #-------------------------------------------------------- def _get_static_tt_extra(self): return self.__static_tt_extra def _set_static_tt_extra(self, tt): self.__static_tt_extra = tt static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra) #-------------------------------------------------------- # event handling #-------------------------------------------------------- def __register_events(self): self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) self.Bind(wx.EVT_SET_FOCUS, self._on_set_focus) self.Bind(wx.EVT_KILL_FOCUS, self._on_lose_focus) self.Bind(wx.EVT_TEXT, self._on_text_update) self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected) #-------------------------------------------------------- def _on_key_down(self, event): """Is called when a key is pressed.""" keycode = event.GetKeyCode() if keycode == wx.WXK_DOWN: self.__on_cursor_down() return if keycode == wx.WXK_UP: self.__on_cursor_up() return if keycode == wx.WXK_RETURN: self._on_enter() return if keycode == wx.WXK_TAB: if event.ShiftDown(): self.Navigate(flags = wx.NavigationKeyEvent.IsBackward) return self.__on_tab() self.Navigate(flags = wx.NavigationKeyEvent.IsForward) return # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]: pass # need to handle all non-character key presses *before* this check elif not self.__char_is_allowed(char = chr(event.GetUnicodeKey())): wx.Bell() # Richard doesn't show any error message here return event.Skip() return #-------------------------------------------------------- def _on_set_focus(self, event): self._has_focus = True event.Skip() self.__background_color_without_focus = self.GetBackgroundColour() self.SetBackgroundColour(COLOR_BG_PRW_WITH_FOCUS) self.Refresh() # notify interested parties for callback in self._on_set_focus_callbacks: callback() self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) return True #-------------------------------------------------------- def _on_lose_focus(self, event): """Do stuff when leaving the control. The user has had her say, so don't second guess intentions but do report error conditions. """ event.Skip() self._has_focus = False self.__timer.Stop() self._hide_picklist() self.SetBackgroundColour(self.__background_color_without_focus) self.Refresh() wx.CallAfter(self.__on_lost_focus) return True #-------------------------------------------------------- def __on_lost_focus(self): if not self: return self.SetSelection(1,1) is_valid = True # the user may have typed a phrase that is an exact match, # however, just typing it won't associate data from the # picklist, so try do that now self._set_data_to_first_match() # check value against final_regex if any given if self.__final_regex.match(self.GetValue().strip()) is None: gmDispatcher.send(signal = 'statustext', msg = self.final_regex_error_msg % self.final_regex) is_valid = False self.display_as_valid(valid = is_valid) # notify interested parties for callback in self._on_lose_focus_callbacks: callback() #-------------------------------------------------------- def _on_list_item_selected(self, *args, **kwargs): """Gets called when user selected a list item.""" self._hide_picklist() item = self._picklist.get_selected_item() # huh ? if item is None: self.display_as_valid(valid = True) return self._update_display_from_picked_item(item) self._update_data_from_picked_item(item) self.MarkDirty() # and tell the listeners about the user's selection for callback in self._on_selection_callbacks: callback(self._data) if self.navigate_after_selection: self.Navigate() return #-------------------------------------------------------- def _on_text_update (self, event): """Internal handler for wx.EVT_TEXT. Called when text was changed by user or by SetValue(). """ if self.suppress_text_update_smarts: self.suppress_text_update_smarts = False return self._adjust_data_after_text_update() self._current_match_candidates = [] val = self.GetValue().strip() ins_point = self.GetInsertionPoint() # if empty string then hide list dropdown window # we also don't need a timer event then if val == '': self._hide_picklist() self.__timer.Stop() else: new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode) if new_val != val: self.suppress_text_update_smarts = True super(cPhraseWheelBase, self).SetValue(new_val) if ins_point > len(new_val): self.SetInsertionPointEnd() else: self.SetInsertionPoint(ins_point) # FIXME: SetSelection() ? # start timer for delayed match retrieval self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) # notify interested parties for callback in self._on_modified_callbacks: callback() return #-------------------------------------------------------- # keypress handling #-------------------------------------------------------- def _on_enter(self): """Called when the user pressed <ENTER>.""" if self._picklist_dropdown.IsShown(): self._on_list_item_selected() return # FIXME: check for errors before navigation self.Navigate() #-------------------------------------------------------- def __on_cursor_down(self): if self._picklist_dropdown.IsShown(): idx_selected = self._picklist.GetFirstSelected() if idx_selected < (len(self._current_match_candidates) - 1): self._select_picklist_row(idx_selected + 1, idx_selected) return # if we don't yet have a pick list: open new pick list # (this can happen when we TAB into a field pre-filled # with the top-weighted contextual item but want to # select another contextual item) self.__timer.Stop() if self.GetValue().strip() == '': val = '*' else: val = self._extract_fragment_to_match_on() self._update_candidates_in_picklist(val = val) self._show_picklist(input2match = val) #-------------------------------------------------------- def __on_cursor_up(self): if self._picklist_dropdown.IsShown(): selected = self._picklist.GetFirstSelected() if selected > 0: self._select_picklist_row(selected-1, selected) #else: # # FIXME: input history ? #-------------------------------------------------------- def __on_tab(self): """Under certain circumstances take special action on <TAB>. returns: True: <TAB> was handled False: <TAB> was not handled -> can be used to decide whether to do further <TAB> handling outside this class """ # are we seeing the picklist ? if not self._picklist_dropdown.IsShown(): return False # with only one candidate ? if len(self._current_match_candidates) != 1: return False # and do we require the input to be picked from the candidates ? if not self.selection_only: return False # then auto-select that item self._select_picklist_row(new_row_idx = 0) self._on_list_item_selected() return True #-------------------------------------------------------- # timer handling #-------------------------------------------------------- def __init_timer(self): self.__timer = _cPRWTimer() self.__timer.callback = self._on_timer_fired # initially stopped self.__timer.Stop() #-------------------------------------------------------- def _on_timer_fired(self): """Callback for delayed match retrieval timer. if we end up here: - delay has passed without user input - the value in the input field has not changed since the timer started """ # update matches according to current input val = self._extract_fragment_to_match_on() self._update_candidates_in_picklist(val = val) # we now have either: # - all possible items (within reasonable limits) if input was '*' # - all matching items # - an empty match list if no matches were found # also, our picklist is refilled and sorted according to weight wx.CallAfter(self._show_picklist, input2match = val) #---------------------------------------------------- # random helpers and properties #---------------------------------------------------- def __mac_log(self, msg): if self.__use_fake_popup: _log.debug(msg) #-------------------------------------------------------- def __char_is_allowed(self, char=None): # if undefined accept all chars if self.accepted_chars is None: return True return (self.__accepted_chars.match(char) is not None) #-------------------------------------------------------- def _set_accepted_chars(self, accepted_chars=None): if accepted_chars is None: self.__accepted_chars = None else: self.__accepted_chars = regex.compile(accepted_chars) def _get_accepted_chars(self): if self.__accepted_chars is None: return None return self.__accepted_chars.pattern accepted_chars = property(_get_accepted_chars, _set_accepted_chars) #-------------------------------------------------------- def _set_final_regex(self, final_regex=r'.*'): self.__final_regex = regex.compile(final_regex, flags = regex.UNICODE) def _get_final_regex(self): return self.__final_regex.pattern final_regex = property(_get_final_regex, _set_final_regex) #-------------------------------------------------------- def _set_final_regex_error_msg(self, msg): self.__final_regex_error_msg = msg def _get_final_regex_error_msg(self): return self.__final_regex_error_msg final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) #-------------------------------------------------------- # data munging #-------------------------------------------------------- def _set_data_to_first_match(self): return False #-------------------------------------------------------- def _update_data_from_picked_item(self, item): self._data = self._dictify_data(data = item) #--------------------------------------------------------- def _dictify_data(self, data=None, value=None) -> dict: if isinstance(data, dict): # test for dict being well-formed data['data'] data['list_label'] data['field_label'] return data if not value: value = '%s' % data return {'data': data, 'list_label': value, 'field_label': value} # #-------------------------------------------------------- # def _dictify_data(self, data=None, value=None): # raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__) #--------------------------------------------------------- def _adjust_data_after_text_update(self): raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__) #-------------------------------------------------------- def _data2match(self, data): if self.matcher is None: return None return self.matcher.get_match_by_data(data = data) #-------------------------------------------------------- def _create_data(self, link_obj=None): """Must set self._data if possible/successful.""" raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__) #-------------------------------------------------------- def _data2instance(self, link_obj=None): """Turn selected data into class instance. Returns: A class instance, typically a subclass of gmBusinessDBObject.cBusinessDBObject, or None. """ raise NotImplementedError('[%s]: cannot turn data object' % self.__class__.__name__) #-------------------------------------------------------- def _get_raw_data(self): return self._data def _set_raw_data(self, raw_data:dict): """Explicitly set raw data of phrasewheel. Args: raw_data: a dict with the keys 'data' (the actual value), 'list_label' (shown in the picklist when selecting an item), and 'field_label' (shown in the phrasewheel after this idem had been selected) """ self._data = raw_data self.__recalculate_tooltip() raw_data = property(_get_raw_data, _set_raw_data)
Widget for smart guessing of user fields, after Richard Terry's interface.
- VB implementation by Richard Terry
- Python port by Ian Haywood for GNUmed
- enhanced by Karsten Hilbert for GNUmed
- enhanced by Ian Haywood for aumed
- enhanced by Karsten Hilbert for GNUmed
@param matcher: a class used to find matches for the current input @type matcher: a L{match provider
} instance or C{None} @param selection_only: whether free-text can be entered without associated data @type selection_only: boolean
@param capitalisation_mode: how to auto-capitalize input, valid values are found in L{capitalize()
} @type capitalisation_mode: integer @param accepted_chars: a regex pattern defining the characters acceptable in the input string, if None no checking is performed @type accepted_chars: None or a string holding a valid regex pattern
@param final_regex: when the control loses focus the input is checked against this regular expression @type final_regex: a string holding a valid regex pattern
@param navigate_after_selection: whether or not to immediately navigate to the widget next-in-tab-order after selecting an item from the dropdown picklist @type navigate_after_selection: boolean
@param picklist_delay: this much time of user inactivity must have passed before the input related smarts kick in and the drop down pick list is shown @type picklist_delay: integer (milliseconds)
Ancestors
- wx._core.TextCtrl
- wx._core.Control
- wx._core.Window
- wx._core.WindowBase
- wx._core.EvtHandler
- wx._core.Object
- wx._core.Trackable
- wx._core.TextEntry
- sip.wrapper
- sip.simplewrapper
Subclasses
Instance variables
prop accepted_chars
-
Expand source code
def _get_accepted_chars(self): if self.__accepted_chars is None: return None return self.__accepted_chars.pattern
prop final_regex
-
Expand source code
def _get_final_regex(self): return self.__final_regex.pattern
prop final_regex_error_msg
-
Expand source code
def _get_final_regex_error_msg(self): return self.__final_regex_error_msg
prop raw_data
-
Expand source code
def _get_raw_data(self): return self._data
prop static_tooltip_extra
-
Expand source code
def _get_static_tt_extra(self): return self.__static_tt_extra
Methods
def Disable(self)
-
Expand source code
def Disable(self): self.Enable(enable = False)
Disable() -> bool
Disables the window.
def Enable(self, enable=True)
-
Expand source code
def Enable(self, enable=True): if self.IsEnabled() is enable: return if self.IsEnabled(): self.__previous_enabled_bg_color = self.GetBackgroundColour() super(cPhraseWheelBase, self).Enable(enable) if enable is True: #self.SetBackgroundColour(color_prw_valid) self.SetBackgroundColour(self.__previous_enabled_bg_color) elif enable is False: self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)) else: raise ValueError('<enable> must be True or False') self.Refresh()
Enable(enable=True) -> bool
Enable or disable the window for user input.
def GetData(self, can_create: bool = False, as_instance: bool = False, link_obj=None)
-
Expand source code
def GetData(self, can_create:bool=False, as_instance:bool=False, link_obj=None): """Retrieve the data associated with the displayed string(s). Returns: None, if no data available, and unable to create data. Or a data instance, if requested and data is available or create-able. Or the data itself. """ if not self._data: if can_create: self._create_data(link_obj = link_obj) if not self._data: return None if as_instance: return self._data2instance(link_obj = link_obj) return self._data['data']
Retrieve the data associated with the displayed string(s).
Returns
None, if no data available, and unable to create data.
Or a data instance, if requested and data is available or create-able.
Or the data itself.
def SetText(self, value: str = '', data=None, suppress_smarts: bool = False)
-
Expand source code
def SetText(self, value:str='', data=None, suppress_smarts:bool=False): """Set both value and data of phrasewheel. Args: value: some text to display in the control data: the data value represented by the displayed text suppress_smarts: whether to generate data based on value, always True if data is passed in """ if value is None: value = '' if (value == '') and (data is None): self._data = {} super(cPhraseWheelBase, self).SetValue(value) return self.suppress_text_update_smarts = suppress_smarts if data: self.suppress_text_update_smarts = True self._data = self._dictify_data(data = data, value = value) super(cPhraseWheelBase, self).SetValue(value) self.display_as_valid(valid = True) # if data already available if len(self._data) > 0: return True # empty text value ? if value == '': # valid value not required ? if not self.selection_only: return True if not self._set_data_to_first_match(): # not found if self.selection_only: self.display_as_valid(valid = False) return False return True
Set both value and data of phrasewheel.
Args
value
- some text to display in the control
data
- the data value represented by the displayed text
suppress_smarts
- whether to generate data based on value, always True if data is passed in
def add_callback_on_lose_focus(self, callback=None)
-
Expand source code
def add_callback_on_lose_focus(self, callback=None): """Add a callback for invocation when losing focus.""" if not callable(callback): raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback) self._on_lose_focus_callbacks.append(callback)
Add a callback for invocation when losing focus.
def add_callback_on_modified(self, callback=None)
-
Expand source code
def add_callback_on_modified(self, callback=None): """Add a callback for invocation when the content is modified. This callback will NOT be passed any values. """ if not callable(callback): raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback) self._on_modified_callbacks.append(callback)
Add a callback for invocation when the content is modified.
This callback will NOT be passed any values.
def add_callback_on_selection(self, callback=None)
-
Expand source code
def add_callback_on_selection(self, callback=None): """Add a callback for invocation when a picklist item is selected. The callback will be invoked whenever an item is selected from the picklist. The associated data is passed in as a single parameter. Callbacks must be able to cope with None as the data parameter as that is sent whenever the user changes a previously selected value. """ if not callable(callback): raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback) self._on_selection_callbacks.append(callback)
Add a callback for invocation when a picklist item is selected.
The callback will be invoked whenever an item is selected from the picklist. The associated data is passed in as a single parameter. Callbacks must be able to cope with None as the data parameter as that is sent whenever the user changes a previously selected value.
def add_callback_on_set_focus(self, callback=None)
-
Expand source code
def add_callback_on_set_focus(self, callback=None): """Add a callback for invocation when getting focus.""" if not callable(callback): raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback) self._on_set_focus_callbacks.append(callback)
Add a callback for invocation when getting focus.
def display_as_valid(self, valid: bool = True)
-
Expand source code
def display_as_valid(self, valid:bool=True): """Color input field based on content validity. valid: whether the content ist valid (True) or invalid (False), False = partially invalid """ assert valid in [True, False, None], '<valid> must be True or False or None' if valid is True: color2show = self.__my_startup_color elif valid is False: color2show = color_prw_invalid else: color2show = color_prw_partially_invalid if self.IsEnabled(): self.SetBackgroundColour(color2show) self.Refresh() return self.__previous_enabled_bg_color = color2show
Color input field based on content validity.
valid: whether the content ist valid (True) or invalid (False), False = partially invalid
def set_context(self, context=None, val=None)
-
Expand source code
def set_context(self, context=None, val=None): if self.matcher is not None: self.matcher.set_context(context=context, val=val)
def set_from_instance(self, instance)
-
Expand source code
def set_from_instance(self, instance): raise NotImplementedError('[%s]: set_from_instance()' % self.__class__.__name__)
def set_from_pk(self, pk)
-
Expand source code
def set_from_pk(self, pk): raise NotImplementedError('[%s]: set_from_pk()' % self.__class__.__name__)
def unset_context(self, context=None)
-
Expand source code
def unset_context(self, context=None): if self.matcher is not None: self.matcher.unset_context(context=context)
class cPhraseWheelListCtrl (*args, **kwargs)
-
Expand source code
class cPhraseWheelListCtrl(wx.ListCtrl, listmixins.ListCtrlAutoWidthMixin): def __init__(self, *args, **kwargs): try: kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER except KeyError: pass wx.ListCtrl.__init__(self, *args, **kwargs) listmixins.ListCtrlAutoWidthMixin.__init__(self) #-------------------------------------------------------- def SetItems(self, items): self.DeleteAllItems() self.__data = items pos = len(items) + 1 for item in items: #self.InsertItem(pos, label = item['list_label']) self.InsertItem(pos, item['list_label']) #-------------------------------------------------------- def GetSelectedItemData(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx]['data'] #-------------------------------------------------------- def get_selected_item(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx] #-------------------------------------------------------- def get_selected_item_label(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx]['list_label']
ListCtrl() ListCtrl(parent, id=ID_ANY, pos=DefaultPosition, size=DefaultSize, style=LC_ICON, validator=DefaultValidator, name=ListCtrlNameStr)
A list control presents lists in a number of formats: list view, report view, icon view and small icon view.
Ancestors
- wx._core.ListCtrl
- wx._core.Control
- wx._core.Window
- wx._core.WindowBase
- wx._core.EvtHandler
- wx._core.Object
- wx._core.Trackable
- sip.wrapper
- sip.simplewrapper
- wx.lib.mixins.listctrl.ListCtrlAutoWidthMixin
Methods
def GetSelectedItemData(self)
-
Expand source code
def GetSelectedItemData(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx]['data']
def SetItems(self, items)
-
Expand source code
def SetItems(self, items): self.DeleteAllItems() self.__data = items pos = len(items) + 1 for item in items: #self.InsertItem(pos, label = item['list_label']) self.InsertItem(pos, item['list_label'])
def get_selected_item(self)
-
Expand source code
def get_selected_item(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx]
def get_selected_item_label(self)
-
Expand source code
def get_selected_item_label(self): sel_idx = self.GetFirstSelected() if sel_idx == -1: return None return self.__data[sel_idx]['list_label']