def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True)
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._current_obj = None self._parent = parent self._on_run_editor = run_editor self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.SEARCH_SPEC) self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS) self._last_operation = None self._source_id = None self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor) self._setup() self.set_value(initial_value, force=True)
def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns)
def __init__(self, columns=None, tree=False, restore_name=None, chars=25, store=None, search_spec=None, fast_iter=False, result_view_class=None): """ Create a new SearchContainer object. :param columns: a list of :class:`kiwi.ui.objectlist.Column` :param tree: if we should list the results as a tree :param restore_name: :param chars: maximum number of chars used by the search entry :param store: a database store :param search_spec: a search spec for store to find on """ if tree: self.result_view_class = SearchResultTreeView if result_view_class: self.result_view_class = result_view_class self._auto_search = True self._lazy_search = False self._last_results = None self._model = None self._query_executer = None self._restore_name = restore_name self._search_filters = [] self._selected_item = None self._summary_label = None self._search_spec = search_spec self._fast_iter = fast_iter self.store = store self.menu = None self.result_view = None self._settings_key = 'search-columns-%s' % (api.get_current_user( self.store).username, ) self.columns = self.restore_columns(columns) self.vbox = gtk.VBox() SlaveDelegate.__init__(self, toplevel=self.vbox) self.vbox.show() search_filter = StringSearchFilter(_('Search:'), chars=chars, container=self) search_filter.connect('changed', self._on_search_filter__changed) self._search_filters.append(search_filter) self._primary_filter = search_filter self._create_ui() self.focus_search_entry()
def __init__(self, columns=None, tree=False, restore_name=None, chars=25, store=None, search_spec=None, fast_iter=False, result_view_class=None): """ Create a new SearchContainer object. :param columns: a list of :class:`kiwi.ui.objectlist.Column` :param tree: if we should list the results as a tree :param restore_name: :param chars: maximum number of chars used by the search entry :param store: a database store :param search_spec: a search spec for store to find on """ if tree: self.result_view_class = SearchResultTreeView if result_view_class: self.result_view_class = result_view_class self._auto_search = True self._lazy_search = False self._last_results = None self._model = None self._query_executer = None self._restore_name = restore_name self._search_filters = [] self._selected_item = None self._summary_label = None self._search_spec = search_spec self._fast_iter = fast_iter self.store = store self.menu = None self.result_view = None self._settings_key = 'search-columns-%s' % ( api.get_current_user(self.store).username, ) self.columns = self.restore_columns(columns) self.vbox = gtk.VBox() SlaveDelegate.__init__(self, toplevel=self.vbox) self.vbox.show() search_filter = StringSearchFilter(_('Search:'), chars=chars, container=self) search_filter.connect('changed', self._on_search_filter__changed) self._search_filters.append(search_filter) self._primary_filter = search_filter self._create_ui() self.focus_search_entry()
def add_filter_by_attribute(self, attr, title, data_type, valid_values=None, callback=None, use_having=False): """Add a filter accordingly to the attributes specified :param attr: attribute that will be filtered. This can be either the name of the attribute or the attribute itself. :param title: the title of the filter that will be visible in the interface :param data_type: the attribute type (str, bool, decimal, etc) :param callback: the callback function that will be triggered :param use_having: use having expression in the query """ if data_type is not bool: title += ':' if data_type == datetime.date: filter = DateSearchFilter(title) if valid_values: filter.clear_options() filter.add_custom_options() for opt in valid_values: filter.add_option(opt) filter.select(valid_values[0]) elif (data_type == decimal.Decimal or data_type == int or data_type == currency): filter = NumberSearchFilter(title) if data_type != int: filter.set_digits(2) elif data_type == str: if valid_values: filter = ComboSearchFilter(title, valid_values) else: filter = StringSearchFilter(title) filter.enable_advanced() elif data_type == bool: filter = BoolSearchFilter(title) else: raise NotImplementedError(title, data_type) filter.set_removable() self.add_filter(filter, columns=[attr], callback=callback, use_having=use_having) if data_type is not bool: label = filter.get_title_label() label.set_alignment(1.0, 0.5) self.label_group.add_widget(label) combo = filter.get_mode_combo() if combo: self.combo_group.add_widget(combo) return filter
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._current_obj = None self._parent = parent self._on_run_editor = run_editor self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.SEARCH_SPEC) self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS) self._last_operation = None self._source_id = None self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor) self._setup() self.set_value(initial_value, force=True)
def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns)
def add_filter_by_column(self, column): """Add a filter accordingly to the column specification :param column: a SearchColumn instance """ title = column.get_search_label() if column.data_type is not bool: title += ':' if column.data_type == datetime.date: filter = DateSearchFilter(title) if column.valid_values: filter.clear_options() filter.add_custom_options() for opt in column.valid_values: filter.add_option(opt) filter.select(column.valid_values[0]) elif (column.data_type == decimal.Decimal or column.data_type == int or column.data_type == currency): filter = NumberSearchFilter(title) if column.data_type != int: filter.set_digits(2) elif column.data_type == str: if column.valid_values: filter = ComboSearchFilter(title, column.valid_values) else: filter = StringSearchFilter(title) filter.enable_advanced() elif column.data_type == bool: filter = BoolSearchFilter(title) else: raise NotImplementedError(title, column.data_type) filter.set_removable() attr = column.search_attribute or column.attribute self.add_filter(filter, columns=[attr], callback=column.search_func, use_having=column.use_having) if column.data_type is not bool: label = filter.get_title_label() label.set_alignment(1.0, 0.5) self.label_group.add_widget(label) combo = filter.get_mode_combo() if combo: self.combo_group.add_widget(combo) return filter
def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._executer.set_order_by(self.order_by) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True)
class QueryEntryGadget(object): """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a Gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have a button to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) """ MIN_KEY_LENGTH = 1 LOADING_ITEMS_TEXT = _("Loading items...") NEW_ITEM_TEXT = _("Create a new item with that name") NEW_ITEM_TOOLTIP = _("Create a new item") EDIT_ITEM_TOOLTIP = _("Edit the selected item") INFO_ITEM_TOOLTIP = _("See info about the selected item") NO_ITEMS_FOUND_TEXT = _("No items found") advanced_search = True selection_only = False item_editor = None item_info_dialog = ClientEditor search_class = None search_spec = None search_columns = None def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None, edit_button=None, info_button=None, search_clause=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._parent = parent self._on_run_editor = run_editor self._can_edit = False self._search_clause = search_clause self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.edit_button = edit_button self.info_button = info_button self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.search_spec) self._executer.set_filter_columns(self._filter, self.search_columns) self._last_operation = None self._source_id = None self._setup() self.set_value(initial_value, force=True) # # Public API # def set_value(self, obj, force=False): if not force and obj == self._current_obj: return obj = self.store.fetch(obj) if obj is not None: if hasattr(obj, 'description'): value = obj.description else: value = obj.get_description() self.entry.prefill([(value, obj)]) self.update_edit_button(Gtk.STOCK_EDIT, self.EDIT_ITEM_TOOLTIP) else: value = '' self.entry.prefill([]) self.update_edit_button(Gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP) self._current_obj = obj self.entry.update(obj) self.entry.set_text(value) self._update_widgets() def set_editable(self, can_edit): self.entry.set_property('editable', can_edit) self._update_widgets() def update_edit_button(self, stock, tooltip=None): image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU) self.edit_button.set_image(image) if tooltip is not None: self.edit_button.set_tooltip_text(tooltip) def get_object_from_item(self, item): return item def describe_item(self, item): raise NotImplementedError # # Private # def _setup(self): if not self.selection_only: if self.edit_button is None or self.info_button is None: self._replace_widget() if self.edit_button is None: self.edit_button = self._add_button(Gtk.STOCK_NEW) self.edit_button.connect('clicked', self._on_edit_button__clicked) if self.info_button is None: self.info_button = self._add_button(Gtk.STOCK_INFO) self.info_button.connect('clicked', self._on_info_button__clicked) self.entry.connect('activate', self._on_entry__activate) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('notify::sensitive', self._on_entry_sensitive) self.entry.connect('key-press-event', self._on_entry__key_press_event) self._popup = _QueryEntryPopup( self, has_new_item=not self.selection_only) self._popup.connect('item-selected', self._on_popup__item_selected) self._popup.connect('create-item', self._on_popup__create_item) def _update_widgets(self): self._can_edit = self.entry.get_editable() and self.entry.get_sensitive() if self.edit_button is not None: self.edit_button.set_sensitive(bool(self._can_edit or self._current_obj)) if self.info_button is not None: self.info_button.set_sensitive(bool(self._current_obj)) def _add_button(self, stock): image = Gtk.Image.new_from_stock(stock, Gtk.IconSize.MENU) button = Gtk.Button() button.set_relief(Gtk.ReliefStyle.NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False, 0) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self.entry.props.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in container.__class__.list_child_properties(): props[pspec.name] = container.child_get_property(self.entry, pspec.name) self.box = Gtk.HBox() self.box.show() self.entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _find_items(self, text): self._filter.set_state(text) state = self._filter.get_state() resultset = self._executer.search([state]) if self._search_clause: resultset = resultset.find(self._search_clause) return self._executer.search_async(resultset=resultset, limit=10) def _dispatch(self, value): self._source_id = None if self._last_operation is not None: self._last_operation.cancel() self._last_operation = self._find_items(value) self._last_operation.connect( 'finish', lambda o: self._popup.add_items(o.get_result())) def _run_search(self): if not self.advanced_search: return text = self.entry.get_text() item = run_dialog(self.search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if item: self.set_value(self.get_object_from_item(item)) def _run_editor(self, model=None, description=None): with api.new_store() as store: model = store.fetch(model) if self._on_run_editor is not None: retval = self._on_run_editor(store, model, description=description, visual_mode=not self._can_edit) else: if issubclass(self.item_editor, BasePersonRoleEditor): rd = run_person_role_dialog else: rd = run_dialog retval = rd(self.item_editor, self._parent, store, model, description=description, visual_mode=not self._can_edit) if store.committed: return self.store.fetch(retval) # # Callbacks # def _on_entry__key_press_event(self, window, event): keyval = event.keyval if keyval == Gdk.KEY_Up or keyval == Gdk.KEY_KP_Up: self._popup.scroll(relative=-1) return True elif keyval == Gdk.KEY_Down or keyval == Gdk.KEY_KP_Down: self._popup.scroll(relative=+1) return True elif keyval == Gdk.KEY_Page_Up: self._popup.scroll(relative=-14) return True elif keyval == Gdk.KEY_Page_Down: self._popup.scroll(relative=+14) return True elif keyval == Gdk.KEY_Escape: self._popup.popdown() return True return False def _on_entry__changed(self, entry): value = unicode(entry.get_text()) self.set_value(None) if len(value) >= self.MIN_KEY_LENGTH: if self._source_id is not None: GLib.source_remove(self._source_id) self._source_id = GLib.timeout_add(150, self._dispatch, value) if not self._popup.visible: self._popup.popup() self._popup.set_loading(True) elif self._popup.visible: # In this case, the user has deleted text to less than the # min key length, so pop it down if self._source_id is not None: GLib.source_remove(self._source_id) self._source_id = None self._popup.popdown() def _on_entry__activate(self, entry): if self._popup.visible: self._popup.popdown() self._popup.confirm() else: self._run_search() def _on_entry_sensitive(self, entry, pspec): self._update_widgets() def _on_popup__item_selected(self, popup, item, fallback_to_search): self.set_value(self.get_object_from_item(item)) popup.popdown() self.entry.grab_focus() GLib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1) if item is None and fallback_to_search: self._run_search() def _on_popup__create_item(self, popup): obj = self._run_editor(description=unicode(self.entry.get_text())) self.set_value(obj) def _on_edit_button__clicked(self, entry): current_obj = self.entry.read() obj = self._run_editor(current_obj) if obj: self.set_value(obj, force=True) def _on_info_button__clicked(self, entry): obj = self.entry.read() with api.new_store() as store: run_dialog(self.item_info_dialog, self._parent, store, store.fetch(obj))
class SearchEntryGadget(object): find_tooltip = _('Search') edit_tooltip = _('Edit') new_tooltip = _('Create') def __init__(self, entry, store, model, model_property, search_columns, search_class, parent, run_editor=None): """ This gadget modifies a ProxyEntry turning it into a replacement for ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have two buttons: One for showing the related search dialog (or search editor), and another one to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) :param entry: The entry that we should modify :param store: The store that will be used for database queries :param model: The model that we are updating :param model_property: Property name of the model that should be updated :param search_columns: Columns that will be queried when the user activates the entry :param search_class: Class of the search editor/dialog that will be displayed when more than one object is found :param parent: The parent that should be respected when running other dialogs :param find_tooltip: the tooltip to use for the search button :param edit_tooltip: the tooltip to use for the edit button :param new_tooltip: the tooltip to use for the new button """ self.store = store self._entry = entry self._model = model # TODO: Maybe this two variables shoulb be a list of properties of the # table instead of strings self._model_property = model_property self._search_columns = search_columns self._search_class = search_class self._parent = parent self._on_run_editor = run_editor # TODO: Respect permission manager self._editor_class = search_class.editor_class # If the search is for a person, the editor is called with a special # function if issubclass(search_class, BasePersonSearch): self._is_person = True else: self._is_person = False self._setup_widgets() self._setup_callbacks() # # Private API # def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns) def _create_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self._entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property( self._entry, pspec.name) self.box = gtk.HBox() self.box.show() self._entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _setup_callbacks(self): self._entry.connect('activate', self._on_entry_activate) self._entry.connect('changed', self._on_entry_changed) self._entry.connect('notify::sensitive', self._on_entry_sensitive) self.find_button.connect('clicked', self._on_find_button__clicked) self.edit_button.connect('clicked', self._on_edit_button__clicked) def _run_search(self): text = self._entry.get_text() value = run_dialog(self._search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if value: self.set_value(self.get_model_obj(value)) def _run_editor(self): with api.new_store() as store: model = getattr(self._model, self._model_property) model = store.fetch(model) if self._on_run_editor: value = self._on_run_editor(store, model) elif self._is_person: value = run_person_role_dialog(self._editor_class, self._parent, store, model) else: value = run_dialog(self._editor_class, self._parent, store, model) if value: value = self.store.fetch(self.get_model_obj(value)) self.set_value(value) # # Public API # def set_value(self, obj): if obj: display_value = obj.get_description() self._entry.prefill([(display_value, obj)]) self.update_edit_button(gtk.STOCK_INFO, self.edit_tooltip) else: display_value = '' self._entry.prefill([]) self.update_edit_button(gtk.STOCK_NEW, self.new_tooltip) self._value = obj self._entry.update(obj) self._entry.set_text(display_value) def get_model_obj(self, obj): return obj def update_edit_button(self, stock, tooltip): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) self.edit_button.set_tooltip_text(tooltip) # # Callbacks # def _on_entry_activate(self, entry): if not self._entry.get_property('editable'): return text = entry.get_text() self._filter.set_state(text) state = self._filter.get_state() results = list(self._executer.search([state])[:2]) if len(results) != 1: # XXX: If nothing is found in the query above, runing the search # will cause the query to be executed a second time. Refactor the # search to allow us to send the initial results avoiding this # second query. return self._run_search() # This means the search returned only one result. self.set_value(self.get_model_obj(results[0])) def _on_entry_changed(self, entry): # If the user edits the value in the entry, it invalidates the value. if self._value: self.set_value(None) def _on_entry_sensitive(self, entry, pspec): can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.edit_button.set_sensitive(can_edit) def _on_edit_button__clicked(self, entry): self._run_editor() def _on_find_button__clicked(self, entry): self._run_search()
class QueryEntryGadget(object): """This gadget modifies a ProxyEntry to behave like a ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have a button to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) """ MIN_KEY_LENGTH = 1 LOADING_ITEMS_TEXT = _("Loading items...") NEW_ITEM_TEXT = _("Create a new item with that name") NEW_ITEM_TOOLTIP = _("Create a new item") EDIT_ITEM_TOOLTIP = _("Edit the selected item") ITEM_EDITOR = None SEARCH_CLASS = None SEARCH_SPEC = None SEARCH_COLUMNS = None def __init__(self, entry, store, initial_value=None, parent=None, run_editor=None): """ :param entry: The entry that we should modify :param store: The store that will be used for database queries :param initial_value: Initial value for the entry :param parent: The parent that should be respected when running other dialogs """ super(QueryEntryGadget, self).__init__() self._current_obj = None self._parent = parent self._on_run_editor = run_editor self.entry = entry self.entry.set_mode(ENTRY_MODE_DATA) self.store = store # The filter that will be used. This is not really in the interface. # We will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self.SEARCH_SPEC) self._executer.set_filter_columns(self._filter, self.SEARCH_COLUMNS) self._last_operation = None self._source_id = None self._is_person = issubclass(self.ITEM_EDITOR, BasePersonRoleEditor) self._setup() self.set_value(initial_value, force=True) # # Public API # def set_value(self, obj, force=False): if not force and obj == self._current_obj: return obj = self.store.fetch(obj) if obj is not None: value = obj.get_description() self.entry.prefill([(value, obj)]) self.update_edit_button(gtk.STOCK_INFO, self.EDIT_ITEM_TOOLTIP) else: value = '' self.entry.prefill([]) self.update_edit_button(gtk.STOCK_NEW, self.NEW_ITEM_TOOLTIP) self._current_obj = obj self.entry.update(obj) self.entry.set_text(value) def set_editable(self, can_edit): self.edit_button.set_sensitive(can_edit) self.entry.set_property('editable', can_edit) def update_edit_button(self, stock, tooltip): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) self.edit_button.set_tooltip_text(tooltip) def get_object_from_item(self, item): return item def describe_item(self, item): raise NotImplementedError # # Private # def _setup(self): self._replace_widget() self.edit_button = self._add_button(gtk.STOCK_NEW) self.edit_button.connect('clicked', self._on_edit_button__clicked) self.entry.connect('activate', self._on_entry__activate) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('notify::sensitive', self._on_entry_sensitive) self.entry.connect('key-press-event', self._on_entry__key_press_event) self._popup = _QueryEntryPopup(self) self._popup.connect('item-selected', self._on_popup__item_selected) self._popup.connect('create-item', self._on_popup__create_item) def _add_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self.entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property(self.entry, pspec.name) self.box = gtk.HBox() self.box.show() self.entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _find_items(self, text): self._filter.set_state(text) state = self._filter.get_state() return self._executer.search_async([state], limit=10) def _dispatch(self, value): self._source_id = None if self._last_operation is not None: self._last_operation.cancel() self._last_operation = self._find_items(value) self._last_operation.connect( 'finish', lambda o: self._popup.add_items(o.get_result())) def _run_search(self): text = self.entry.get_text() if not text: return item = run_dialog(self.SEARCH_CLASS, self._parent, self.store, double_click_confirm=True, initial_string=text) if item: self.set_value(self.get_object_from_item(item)) def _run_editor(self, model=None, description=None): with api.new_store() as store: model = store.fetch(model) if self._on_run_editor is not None: retval = self._on_run_editor(store, model, description=description) else: rd = run_person_role_dialog if self._is_person else run_dialog retval = rd(self.ITEM_EDITOR, self._parent, store, model, description=description) if store.committed: return self.store.fetch(retval) # # Callbacks # def _on_entry__key_press_event(self, window, event): keyval = event.keyval if keyval == gtk.keysyms.Up or keyval == gtk.keysyms.KP_Up: self._popup.scroll(relative=-1) return True elif keyval == gtk.keysyms.Down or keyval == gtk.keysyms.KP_Down: self._popup.scroll(relative=+1) return True elif keyval == gtk.keysyms.Page_Up: self._popup.scroll(relative=-14) return True elif keyval == gtk.keysyms.Page_Down: self._popup.scroll(relative=+14) return True elif keyval == gtk.keysyms.Escape: self._popup.popdown() return True return False def _on_entry__changed(self, entry): value = unicode(entry.get_text()) self.set_value(None) if len(value) >= self.MIN_KEY_LENGTH: if self._source_id is not None: glib.source_remove(self._source_id) self._source_id = glib.timeout_add(150, self._dispatch, value) if not self._popup.visible: self._popup.popup() self._popup.set_loading(True) elif self._popup.visible: # In this case, the user has deleted text to less than the # min key length, so pop it down if self._source_id is not None: glib.source_remove(self._source_id) self._source_id = None self._popup.popdown() def _on_entry__activate(self, entry): self._popup.popdown() self._popup.confirm() def _on_entry_sensitive(self, entry, pspec): can_edit = entry.get_editable() and entry.get_sensitive() self.edit_button.set_sensitive(can_edit) def _on_popup__item_selected(self, popup, item, fallback_to_search): self.set_value(self.get_object_from_item(item)) popup.popdown() self.entry.grab_focus() glib.idle_add(self.entry.select_region, len(self.entry.get_text()), -1) if item is None and fallback_to_search: self._run_search() def _on_popup__create_item(self, popup): obj = self._run_editor(description=unicode(self.entry.get_text())) self.set_value(obj) def _on_edit_button__clicked(self, entry): current_obj = self.entry.read() obj = self._run_editor(current_obj) if obj: self.set_value(obj, force=True)
class SearchEntryGadget(object): find_tooltip = _('Search') edit_tooltip = _('Edit') new_tooltip = _('Create') def __init__(self, entry, store, model, model_property, search_columns, search_class, parent): """ This gadget modifies a ProxyEntry turning it into a replacement for ProxyComboEntry. When instanciated, the gadget will remove the entry from the editor, add a gtk.HBox on its place, and re-attach the entry to the newly created hbox. This hbox will also have two buttons: One for showing the related search dialog (or search editor), and another one to add/edit a new object. There are a few advantages in using this instead of a combo: - There is no need to prefill the combo with all the options, which can be very slow depending on the number of objects. - This allows the user to use a better search mechanism, allowing him to filter using multiple keywords and even candidade keys (like a client document) :param entry: The entry that we should modify :param store: The store that will be used for database queries :param model: The model that we are updating :param model_property: Property name of the model that should be updated :param search_columns: Columns that will be queried when the user activates the entry :param search_class: Class of the search editor/dialog that will be displayed when more than one object is found :param parent: The parent that should be respected when running other dialogs :param find_tooltip: the tooltip to use for the search button :param edit_tooltip: the tooltip to use for the edit button :param new_tooltip: the tooltip to use for the new button """ self.store = store self._entry = entry self._model = model # TODO: Maybe this two variables shoulb be a list of properties of the # table instead of strings self._model_property = model_property self._search_columns = search_columns self._search_class = search_class self._parent = parent # TODO: Respect permission manager self._editor_class = search_class.editor_class # If the search is for a person, the editor is called with a special # function if issubclass(search_class, BasePersonSearch): self._is_person = True else: self._is_person = False self._setup_widgets() self._setup_callbacks() # # Private API # def _setup_widgets(self): self._replace_widget() # Add the two buttons self.find_button = self._create_button(gtk.STOCK_FIND) self.edit_button = self._create_button(gtk.STOCK_NEW) can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.find_button.set_tooltip_text(self.find_tooltip) self.edit_button.set_tooltip_text(self.new_tooltip) # the entry needs a completion to work in MODE_DATA self._completion = gtk.EntryCompletion() self._entry.set_completion(self._completion) self._entry.set_mode(ENTRY_MODE_DATA) initial_value = getattr(self._model, self._model_property) self.set_value(initial_value) # The filter that will be used. This is not really in the interface. We # will just use it to perform the search. self._filter = StringSearchFilter('') self._executer = QueryExecuter(self.store) self._executer.set_search_spec(self._search_class.search_spec) self._executer.set_filter_columns(self._filter, self._search_columns) def _create_button(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.set_image(image) button.show() self.box.pack_start(button, False, False) return button def _replace_widget(self): # This will remove the entry, add a hbox in the entry old position, and # reattach the entry to this box. The box will then be used to add two # new buttons (one for searching, other for editing/adding new objects container = self._entry.parent # stolen from gazpacho code (widgets/base/base.py): props = {} for pspec in gtk.container_class_list_child_properties(container): props[pspec.name] = container.child_get_property(self._entry, pspec.name) self.box = gtk.HBox() self.box.show() self._entry.reparent(self.box) container.add(self.box) for name, value in props.items(): container.child_set_property(self.box, name, value) def _setup_callbacks(self): self._entry.connect('activate', self._on_entry_activate) self._entry.connect('changed', self._on_entry_changed) self._entry.connect('notify::sensitive', self._on_entry_sensitive) self.find_button.connect('clicked', self._on_find_button__clicked) self.edit_button.connect('clicked', self._on_edit_button__clicked) def _run_search(self): text = self._entry.get_text() value = run_dialog(self._search_class, self._parent, self.store, double_click_confirm=True, initial_string=text) if value: self.set_value(value) def _run_editor(self): with api.new_store() as store: model = getattr(self._model, self._model_property) model = store.fetch(model) if self._is_person: value = run_person_role_dialog(self._editor_class, self._parent, store, model) else: value = run_dialog(self._editor_class, self._parent, store, model) if value: value = self.store.fetch(value) self.set_value(value) # # Public API # def set_value(self, obj): if obj: display_value = obj.get_description() obj_id = obj.id self._entry.prefill([(display_value, obj_id)]) self.set_edit_button_stock(gtk.STOCK_INFO) self.edit_button.set_tooltip_text(self.edit_tooltip) else: display_value = '' obj_id = None self._entry.prefill([]) self.set_edit_button_stock(gtk.STOCK_NEW) self.edit_button.set_tooltip_text(self.new_tooltip) self._value = obj self._entry.update(obj_id) self._entry.set_text(display_value) def set_edit_button_stock(self, stock): image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_MENU) self.edit_button.set_image(image) # # Callbacks # def _on_entry_activate(self, entry): if not self._entry.get_property('editable'): return text = entry.get_text() self._filter.set_state(text) state = self._filter.get_state() results = list(self._executer.search([state])[:2]) if len(results) != 1: # XXX: If nothing is found in the query above, runing the search # will cause the query to be executed a second time. Refactor the # search to allow us to send the initial results avoiding this # second query. return self._run_search() # This means the search returned only one result. self.set_value(results[0]) def _on_entry_changed(self, entry): # If the user edits the value in the entry, it invalidates the value. if self._value: self.set_value(None) def _on_entry_sensitive(self, entry, pspec): can_edit = self._entry.get_editable() and self._entry.get_sensitive() self.find_button.set_sensitive(can_edit) self.edit_button.set_sensitive(can_edit) def _on_edit_button__clicked(self, entry): self._run_editor() def _on_find_button__clicked(self, entry): self._run_search()