From a27599c63b5f8cbbd8e6e77097943c6bffbf1fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Fri, 19 Apr 2019 23:57:55 +0200 Subject: [PATCH] Rework Search dialog - Make it a proper Gtk.Assistant --- gajim/common/modules/dataforms.py | 10 +- gajim/data/gui/search_window.ui | 157 -------------- gajim/data/style/gajim.css | 4 + gajim/gtk/discovery.py | 9 +- gajim/gtk/discovery_search.py | 244 --------------------- gajim/gtk/search.py | 344 ++++++++++++++++++++++++++++++ 6 files changed, 354 insertions(+), 414 deletions(-) delete mode 100644 gajim/data/gui/search_window.ui delete mode 100644 gajim/gtk/discovery_search.py create mode 100644 gajim/gtk/search.py diff --git a/gajim/common/modules/dataforms.py b/gajim/common/modules/dataforms.py index a374c911a..87ed8af76 100644 --- a/gajim/common/modules/dataforms.py +++ b/gajim/common/modules/dataforms.py @@ -665,6 +665,10 @@ class DataForm(ExtendedNode): for value in self.getTags('instructions'): self.delChild(value) + @property + def is_reported(self): + return self.getTag('reported') is not None + class SimpleDataForm(DataForm, DataRecord): def __init__(self, type_=None, title=None, instructions=None, fields=None, @@ -744,12 +748,6 @@ class MultipleDataForm(DataForm): for record in self.getTags('item'): yield record -# @property -# def reported(self): -# """ -# DataRecord that contains descriptions of fields in records -# """ -# return self.getTag('reported') # # @reported.setter # def reported(self, record): diff --git a/gajim/data/gui/search_window.ui b/gajim/data/gui/search_window.ui deleted file mode 100644 index 3b93567d3..000000000 --- a/gajim/data/gui/search_window.ui +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - True - False - gtk-add - - - True - False - gtk-find - - - True - False - gtk-dialog-info - - - False - 12 - Search - dialog - - - - - True - False - vertical - 6 - - - True - False - vertical - - - True - False - Please wait while retrieving search form... - - - False - False - 0 - - - - - True - False - 0.10000000149 - - - False - False - 1 - - - - - - - - False - True - 0 - - - - - True - False - 6 - end - - - _Add contact - False - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - image1 - True - - - - False - False - 0 - - - - - _Information - False - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - image3 - True - - - - False - False - 1 - - - - - _Search - True - True - True - False - image2 - True - - - - False - False - 2 - - - - - gtk-close - True - True - True - False - True - - - - False - False - 3 - - - - - False - True - 1 - - - - - - diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index 4cb9356eb..b19faacfe 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -256,3 +256,7 @@ button.flat.link { padding: 0; border: 0; } /* Treeview */ .adhoc-treeview { padding: 5px; } .adhoc-scrolled { border: 1px solid; border-color:@unfocused_borders; } + +/* Search Dialog */ +.search-treeview { padding: 5px; } +.search-scrolled { border: 1px solid; border-color:@unfocused_borders; } diff --git a/gajim/gtk/discovery.py b/gajim/gtk/discovery.py index c38ed8198..69b711431 100644 --- a/gajim/gtk/discovery.py +++ b/gajim/gtk/discovery.py @@ -58,7 +58,7 @@ from gajim.common.const import StyleAttr from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import InformationDialog from gajim.gtk.service_registration import ServiceRegistration -from gajim.gtk.discovery_search import SearchWindow +from gajim.gtk.search import Search from gajim.gtk.adhoc import AdHocCommand from gajim.gtk.util import icon_exists from gajim.gtk.util import get_builder @@ -1284,12 +1284,7 @@ class ToplevelAgentBrowser(AgentBrowser): if not iter_: return service = model[iter_][0] - if service in app.interface.instances[self.account]['search']: - app.interface.instances[self.account]['search'][service].window.\ - present() - else: - app.interface.instances[self.account]['search'][service] = \ - SearchWindow(self.account, service) + Search(self.account, service, self.window.window) def cleanup(self): AgentBrowser.cleanup(self) diff --git a/gajim/gtk/discovery_search.py b/gajim/gtk/discovery_search.py deleted file mode 100644 index cb8b578a5..000000000 --- a/gajim/gtk/discovery_search.py +++ /dev/null @@ -1,244 +0,0 @@ -# Copyright (C) 2007 Stephan Erb -# Copyright (C) 2007-2014 Yann Leboulanger -# -# This file is part of Gajim. -# -# Gajim is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published -# by the Free Software Foundation; version 3 only. -# -# Gajim is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Gajim. If not, see . - -from gi.repository import GLib -from gi.repository import Gtk -from gi.repository import Gdk - -from gajim.common import app -from gajim.common import ged -from gajim.common.modules import dataforms -from gajim.common.i18n import _ - -from gajim import vcard -from gajim import dataforms_widget - -from gajim.gtk.util import get_builder -from gajim.gtk.add_contact import AddNewContactWindow -from gajim.gtk.dataform import FakeDataFormWidget - - -class SearchWindow: - def __init__(self, account, jid): - self.account = account - self.jid = jid - - self._ui = get_builder('search_window.ui') - self.window = self._ui.search_window - - self._ui.search_button.set_sensitive(False) - - self._ui.connect_signals(self) - self.window.show_all() - self.request_form() - self.pulse_id = GLib.timeout_add(80, self.pulse_callback) - - self.is_form = None - - # Is there a jid column in results ? if -1: no, else column number - self.jid_column = -1 - - app.ged.register_event_handler('search-form-received', - ged.GUI1, - self._nec_search_form_received) - app.ged.register_event_handler('search-result-received', - ged.GUI1, - self._nec_search_result_received) - - def request_form(self): - con = app.connections[self.account] - con.get_module('Search').request_search_fields(self.jid) - - def pulse_callback(self): - self._ui.progressbar.pulse() - return True - - def on_search_window_key_press_event(self, _widget, event): - if event.keyval == Gdk.KEY_Escape: - self.window.destroy() - - def on_search_window_destroy(self, _widget): - if self.pulse_id: - GLib.source_remove(self.pulse_id) - del app.interface.instances[self.account]['search'][self.jid] - app.ged.remove_event_handler('search-form-received', - ged.GUI1, - self._nec_search_form_received) - app.ged.remove_event_handler('search-result-received', - ged.GUI1, - self._nec_search_result_received) - - def on_close_button_clicked(self, _button): - self.window.destroy() - - def on_search_button_clicked(self, _button): - con = app.connections[self.account] - if self.is_form: - self.data_form_widget.data_form.type_ = 'submit' - con.get_module('Search').send_search_form( - self.jid, self.data_form_widget.data_form.get_purged(), True) - else: - infos = self.data_form_widget.get_submit_form() - if 'instructions' in infos: - del infos['instructions'] - con.get_module('Search').send_search_form(self.jid, infos, False) - - self._ui.search_vbox.remove(self.data_form_widget) - - self._ui.progressbar.show() - self._ui.label.set_text(_('Waiting for results')) - self._ui.label.show() - self.pulse_id = GLib.timeout_add(80, self.pulse_callback) - self._ui.search_button.hide() - - def on_add_contact_button_clicked(self, _widget): - (model, iter_) = self.result_treeview.get_selection().get_selected() - if not iter_: - return - jid = model[iter_][self.jid_column] - AddNewContactWindow(self.account, jid) - - def on_information_button_clicked(self, _widget): - (model, iter_) = self.result_treeview.get_selection().get_selected() - if not iter_: - return - jid = model[iter_][self.jid_column] - if jid in app.interface.instances[self.account]['infos']: - app.interface.instances[self.account]['infos'][jid].window.present() - else: - contact = app.contacts.create_contact(jid=jid, account=self.account) - app.interface.instances[self.account]['infos'][jid] = \ - vcard.VcardWindow(contact, self.account) - - def _nec_search_form_received(self, obj): - if self.pulse_id: - GLib.source_remove(self.pulse_id) - self._ui.progressbar.hide() - self._ui.label.hide() - - if obj.is_dataform: - self.is_form = True - self.data_form_widget = dataforms_widget.DataFormWidget() - self.dataform = dataforms.extend_form(node=obj.data) - self.data_form_widget.set_sensitive(True) - try: - self.data_form_widget.data_form = self.dataform - except dataforms.Error: - self._ui.label.set_text(_('Error in received dataform')) - self._ui.label.show() - return - if self.data_form_widget.title: - self.window.set_title( - '%s - Search - Gajim' % self.data_form_widget.title) - else: - self.is_form = False - self.data_form_widget = FakeDataFormWidget(obj.data) - - self.data_form_widget.show_all() - self._ui.search_vbox.pack_start(self.data_form_widget, True, True, 0) - self._ui.search_button.set_sensitive(True) - - def on_result_treeview_cursor_changed(self, treeview): - if self.jid_column == -1: - return - (model, iter_) = treeview.get_selection().get_selected() - if not iter_: - return - if model[iter_][self.jid_column]: - self._ui.add_contact_button.set_sensitive(True) - self._ui.information_button.set_sensitive(True) - else: - self._ui.add_contact_button.set_sensitive(False) - self._ui.information_button.set_sensitive(False) - - def _nec_search_result_received(self, obj): - if self.pulse_id: - GLib.source_remove(self.pulse_id) - self._ui.progressbar.hide() - self._ui.label.hide() - - if not obj.is_dataform: - if not obj.data: - self._ui.label.set_text(_('No result')) - self._ui.label.show() - return - # We suppose all items have the same fields - sw = Gtk.ScrolledWindow() - sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - self.result_treeview = Gtk.TreeView() - self.result_treeview.connect( - 'cursor-changed', self.on_result_treeview_cursor_changed) - sw.add(self.result_treeview) - # Create model - fieldtypes = [str]*len(obj.data[0]) - model = Gtk.ListStore(*fieldtypes) - # Copy data to model - for item in obj.data: - model.append(item.values()) - # Create columns - counter = 0 - for field in obj.data[0].keys(): - self.result_treeview.append_column( - Gtk.TreeViewColumn(field, - Gtk.CellRendererText(), - text=counter)) - if field == 'jid': - self.jid_column = counter - counter += 1 - self.result_treeview.set_model(model) - sw.show_all() - self._ui.search_vbox.pack_start(sw, True, True, 0) - if self.jid_column > -1: - self._ui.add_contact_button.show() - self._ui.information_button.show() - return - - self.dataform = dataforms.extend_form(node=obj.data) - if not self.dataform.items: - # No result - self._ui.label.set_text(_('No result')) - self._ui.label.show() - return - - self.data_form_widget.set_sensitive(True) - try: - self.data_form_widget.data_form = self.dataform - except dataforms.Error: - self._ui.label.set_text(_('Error in received dataform')) - self._ui.label.show() - return - - self.result_treeview = self.data_form_widget.records_treeview - selection = self.result_treeview.get_selection() - selection.set_mode(Gtk.SelectionMode.SINGLE) - self.result_treeview.connect( - 'cursor-changed', self.on_result_treeview_cursor_changed) - - counter = 0 - for field in self.dataform.reported.iter_fields(): - if field.var == 'jid': - self.jid_column = counter - break - counter += 1 - self._ui.search_vbox.pack_start(self.data_form_widget, True, True, 0) - self.data_form_widget.show() - if self.jid_column > -1: - self._ui.add_contact_button.show() - self._ui.information_button.show() - if self.data_form_widget.title: - self.window.set_title( - '%s - Search - Gajim' % self.data_form_widget.title) diff --git a/gajim/gtk/search.py b/gajim/gtk/search.py new file mode 100644 index 000000000..c88dce355 --- /dev/null +++ b/gajim/gtk/search.py @@ -0,0 +1,344 @@ +# Copyright (C) 2019 Philipp Hörist +# +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +import logging +import itertools +from enum import IntEnum + +from gi.repository import Gtk + +from gajim.common import app +from gajim.common import ged +from gajim.common.i18n import _ + +from gajim.common.modules import dataforms + +from gajim.gtk.dataform import DataFormWidget +from gajim.gtk.util import ensure_not_destroyed +from gajim.gtk.util import find_widget + +log = logging.getLogger('gajim.gtk.search') + + +class Page(IntEnum): + REQUEST_FORM = 0 + FORM = 1 + REQUEST_RESULT = 2 + COMPLETED = 3 + ERROR = 4 + + +class Search(Gtk.Assistant): + def __init__(self, account, jid, transient_for=None): + Gtk.Assistant.__init__(self) + + self._con = app.connections[account] + self._account = account + self._jid = jid + self._destroyed = False + + self.set_application(app.app) + self.set_resizable(True) + self.set_position(Gtk.WindowPosition.CENTER) + if transient_for is not None: + self.set_transient_for(transient_for) + + self.set_size_request(500, 400) + self.get_style_context().add_class('dialog-margin') + + self._add_page(RequestForm()) + self._add_page(Form()) + self._add_page(RequestResult()) + self._add_page(Completed()) + self._add_page(Error()) + + self.connect('prepare', self._on_page_change) + self.connect('cancel', self._on_cancel) + self.connect('close', self._on_cancel) + self.connect('destroy', self._on_destroy) + + self._remove_sidebar() + + self._buttons = {} + self._add_custom_buttons() + + self.show() + app.ged.register_event_handler('search-form-received', + ged.GUI1, + self._search_form_received) + app.ged.register_event_handler('search-result-received', + ged.GUI1, + self._search_result_received) + + self._request_search_fields() + + def _add_custom_buttons(self): + action_area = find_widget('action_area', self) + for button in list(action_area.get_children()): + self.remove_action_widget(button) + + search = Gtk.Button(label=_('Search')) + search.connect('clicked', self._execute_search) + search.get_style_context().add_class('suggested-action') + self._buttons['search'] = search + self.add_action_widget(search) + + new_search = Gtk.Button(label=_('New Search')) + new_search.get_style_context().add_class('suggested-action') + new_search.connect('clicked', + lambda *args: self.set_current_page(Page.FORM)) + self._buttons['new-search'] = new_search + self.add_action_widget(new_search) + + def _set_button_visibility(self, page): + for button in self._buttons.values(): + button.hide() + + if page == Page.FORM: + self._buttons['search'].show() + + elif page in (Page.ERROR, Page.COMPLETED): + self._buttons['new-search'].show() + + def _add_page(self, page): + self.append_page(page) + self.set_page_type(page, page.type_) + self.set_page_title(page, page.title) + self.set_page_complete(page, page.complete) + + def set_stage_complete(self, is_valid): + self._buttons['search'].set_sensitive(is_valid) + + def _request_search_fields(self): + self._con.get_module('Search').request_search_fields(self._jid) + + def _execute_search(self, *args): + self.set_current_page(Page.REQUEST_RESULT) + form = self.get_nth_page(Page.FORM).get_submit_form() + self._con.get_module('Search').send_search_form(self._jid, form, True) + + @ensure_not_destroyed + def _search_form_received(self, event): + if not event.is_dataform: + self.set_current_page(Page.ERROR) + return + + self.get_nth_page(Page.FORM).process_search_form(event.data) + self.set_current_page(Page.FORM) + + @ensure_not_destroyed + def _search_result_received(self, event): + if event.data is None: + self._on_error('') + return + self.get_nth_page(Page.COMPLETED).process_result(event.data) + self.set_current_page(Page.COMPLETED) + + def _remove_sidebar(self): + main_box = self.get_children()[0] + sidebar = main_box.get_children()[0] + main_box.remove(sidebar) + + def _on_page_change(self, _assistant, _page): + self._set_button_visibility(self.get_current_page()) + + def _on_error(self, error_text): + log.info('Show Error page') + page = self.get_nth_page(Page.ERROR) + page.set_text(error_text) + self.set_current_page(Page.ERROR) + + def _on_cancel(self, _widget): + app.ged.remove_event_handler('search-form-received', + ged.GUI1, + self._search_form_received) + app.ged.remove_event_handler('search-result-received', + ged.GUI1, + self._search_result_received) + self.destroy() + + def _on_destroy(self, *args): + self._destroyed = True + + +class RequestForm(Gtk.Box): + + type_ = Gtk.AssistantPageType.CUSTOM + title = _('Request Search Form') + complete = False + + def __init__(self): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(18) + spinner = Gtk.Spinner() + self.pack_start(spinner, True, True, 0) + spinner.start() + self.show_all() + + +class Form(Gtk.Box): + + type_ = Gtk.AssistantPageType.CUSTOM + title = _('Search') + complete = True + + def __init__(self): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(18) + self._dataform_widget = None + self.show_all() + + @property + def search_form(self): + return self._dataform_widget.get_submit_form() + + def clear(self): + self._show_form(None) + + def process_search_form(self, form): + self._show_form(form) + + def _show_form(self, form): + if self._dataform_widget is not None: + self.remove(self._dataform_widget) + self._dataform_widget.destroy() + if form is None: + return + + options = {'form-width': 350} + + form = dataforms.extend_form(node=form) + self._dataform_widget = DataFormWidget(form, options=options) + self._dataform_widget.connect('is-valid', self._on_is_valid) + self._dataform_widget.validate() + self._dataform_widget.show_all() + self.add(self._dataform_widget) + + def _on_is_valid(self, _widget, is_valid): + self.get_toplevel().set_stage_complete(is_valid) + + def get_submit_form(self): + return self._dataform_widget.get_submit_form() + + +class RequestResult(RequestForm): + + type_ = Gtk.AssistantPageType.CUSTOM + title = _('Search…') + complete = False + + +class Completed(Gtk.Box): + + type_ = Gtk.AssistantPageType.CUSTOM + title = _('Search Result') + complete = True + + def __init__(self): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(12) + self.show_all() + self._label = Gtk.Label(label=_('No results found')) + self._label.get_style_context().add_class('bold16') + self._label.set_no_show_all(True) + self._label.set_halign(Gtk.Align.CENTER) + self._scrolled = Gtk.ScrolledWindow() + self._scrolled.get_style_context().add_class('search-scrolled') + self._scrolled.set_no_show_all(True) + self._treeview = None + self.add(self._label) + self.add(self._scrolled) + self.show_all() + + def process_result(self, form): + if self._treeview is not None: + self._scrolled.remove(self._treeview) + self._treeview.destroy() + self._treeview = None + self._label.hide() + self._scrolled.hide() + + if not form: + self._label.show() + return + + form = dataforms.extend_form(node=form) + + fieldtypes = [] + fieldvars = [] + for field in form.reported.iter_fields(): + if field.type_ == 'boolean': + fieldtypes.append(bool) + elif field.type_ in ('jid-single', 'text-single'): + fieldtypes.append(str) + else: + log.warning('Not supported field received: %s', field.type_) + continue + fieldvars.append(field.var) + + liststore = Gtk.ListStore(*fieldtypes) + + for item in form.iter_records(): + iter_ = liststore.append() + for field in item.iter_fields(): + if field.var in fieldvars: + liststore.set_value(iter_, + fieldvars.index(field.var), + field.value) + + self._treeview = Gtk.TreeView() + self._treeview.set_hexpand(True) + self._treeview.set_vexpand(True) + self._treeview.get_style_context().add_class('search-treeview') + + for field, counter in zip(form.reported.iter_fields(), + itertools.count()): + self._treeview.append_column( + Gtk.TreeViewColumn(field.label, + Gtk.CellRendererText(), + text=counter)) + + self._treeview.set_model(liststore) + self._treeview.show() + self._scrolled.add(self._treeview) + self._scrolled.show() + + +class Error(Gtk.Box): + + type_ = Gtk.AssistantPageType.CUSTOM + title = _('Error') + complete = True + + def __init__(self): + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.set_spacing(12) + self.set_homogeneous(True) + + icon = Gtk.Image.new_from_icon_name('dialog-error-symbolic', + Gtk.IconSize.DIALOG) + icon.get_style_context().add_class('error-color') + icon.set_valign(Gtk.Align.END) + self._label = Gtk.Label() + self._label.get_style_context().add_class('bold16') + self._label.set_valign(Gtk.Align.START) + + self.add(icon) + self.add(self._label) + self.show_all() + + def set_text(self, text): + self._label.set_text(text)