Philipp Hörist pushed to branch master at gajim / gajim
Commits: a27599c6 by Philipp Hörist at 2019-04-19T21:57:55Z Rework Search dialog
- Make it a proper Gtk.Assistant
- - - - -
6 changed files:
- gajim/common/modules/dataforms.py - − gajim/data/gui/search_window.ui - gajim/data/style/gajim.css - gajim/gtk/discovery.py - − gajim/gtk/discovery_search.py - + gajim/gtk/search.py
Changes:
===================================== 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):
===================================== gajim/data/gui/search_window.ui deleted ===================================== @@ -1,157 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.3 --> -<interface> - <requires lib="gtk+" version="3.12"/> - <object class="GtkImage" id="image1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="stock">gtk-add</property> - </object> - <object class="GtkImage" id="image2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="stock">gtk-find</property> - </object> - <object class="GtkImage" id="image3"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="stock">gtk-dialog-info</property> - </object> - <object class="GtkWindow" id="search_window"> - <property name="can_focus">False</property> - <property name="border_width">12</property> - <property name="title" translatable="yes">Search</property> - <property name="type_hint">dialog</property> - <signal name="destroy" handler="on_search_window_destroy" swapped="no"/> - <signal name="key-press-event" handler="on_search_window_key_press_event" swapped="no"/> - <child> - <object class="GtkBox" id="vbox1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">6</property> - <child> - <object class="GtkBox" id="search_vbox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <child> - <object class="GtkLabel" id="label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">Please wait while retrieving search form...</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkProgressBar" id="progressbar"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="pulse_step">0.10000000149</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - <child> - <placeholder/> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButtonBox" id="hbuttonbox1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">6</property> - <property name="layout_style">end</property> - <child> - <object class="GtkButton" id="add_contact_button"> - <property name="label" translatable="yes">_Add contact</property> - <property name="sensitive">False</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="no_show_all">True</property> - <property name="image">image1</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_add_contact_button_clicked" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="information_button"> - <property name="label" translatable="yes">_Information</property> - <property name="sensitive">False</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="no_show_all">True</property> - <property name="image">image3</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_information_button_clicked" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkButton" id="search_button"> - <property name="label" translatable="yes">_Search</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="can_default">True</property> - <property name="receives_default">False</property> - <property name="image">image2</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_search_button_clicked" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">2</property> - </packing> - </child> - <child> - <object class="GtkButton" id="close_button"> - <property name="label">gtk-close</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="can_default">True</property> - <property name="receives_default">False</property> - <property name="use_stock">True</property> - <signal name="clicked" handler="on_close_button_clicked" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">3</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - </object> - </child> - </object> -</interface>
===================================== 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; }
===================================== 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)
===================================== gajim/gtk/discovery_search.py deleted ===================================== @@ -1,244 +0,0 @@ -# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de> -# Copyright (C) 2007-2014 Yann Leboulanger <asterix AT lagaule.org> -# -# 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 http://www.gnu.org/licenses/. - -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)
===================================== gajim/gtk/search.py ===================================== @@ -0,0 +1,344 @@ +# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com> +# +# 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 http://www.gnu.org/licenses/. + +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)
View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/a27599c63b5f8cbbd8e6e77097943c6bffb...