Esempio n. 1
0
    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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio n. 4
0
    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()
Esempio n. 5
0
    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()
Esempio n. 6
0
    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
Esempio n. 7
0
    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)
Esempio n. 8
0
    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)
Esempio n. 9
0
    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
Esempio n. 10
0
    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)
Esempio n. 11
0
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))
Esempio n. 12
0
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()
Esempio n. 13
0
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)
Esempio n. 14
0
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()