Rework Search dialog

- Make it a proper Gtk.Assistant
This commit is contained in:
Philipp Hörist 2019-04-19 23:57:55 +02:00
parent 8bfe90c5fe
commit a27599c63b
6 changed files with 354 additions and 414 deletions

View File

@ -665,6 +665,10 @@ class DataForm(ExtendedNode):
for value in self.getTags('instructions'): for value in self.getTags('instructions'):
self.delChild(value) self.delChild(value)
@property
def is_reported(self):
return self.getTag('reported') is not None
class SimpleDataForm(DataForm, DataRecord): class SimpleDataForm(DataForm, DataRecord):
def __init__(self, type_=None, title=None, instructions=None, fields=None, def __init__(self, type_=None, title=None, instructions=None, fields=None,
@ -744,12 +748,6 @@ class MultipleDataForm(DataForm):
for record in self.getTags('item'): for record in self.getTags('item'):
yield record yield record
# @property
# def reported(self):
# """
# DataRecord that contains descriptions of fields in records
# """
# return self.getTag('reported')
# #
# @reported.setter # @reported.setter
# def reported(self, record): # def reported(self, record):

View File

@ -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>

View File

@ -256,3 +256,7 @@ button.flat.link { padding: 0; border: 0; }
/* Treeview */ /* Treeview */
.adhoc-treeview { padding: 5px; } .adhoc-treeview { padding: 5px; }
.adhoc-scrolled { border: 1px solid; border-color:@unfocused_borders; } .adhoc-scrolled { border: 1px solid; border-color:@unfocused_borders; }
/* Search Dialog */
.search-treeview { padding: 5px; }
.search-scrolled { border: 1px solid; border-color:@unfocused_borders; }

View File

@ -58,7 +58,7 @@ from gajim.common.const import StyleAttr
from gajim.gtk.dialogs import ErrorDialog from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InformationDialog from gajim.gtk.dialogs import InformationDialog
from gajim.gtk.service_registration import ServiceRegistration 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.adhoc import AdHocCommand
from gajim.gtk.util import icon_exists from gajim.gtk.util import icon_exists
from gajim.gtk.util import get_builder from gajim.gtk.util import get_builder
@ -1284,12 +1284,7 @@ class ToplevelAgentBrowser(AgentBrowser):
if not iter_: if not iter_:
return return
service = model[iter_][0] service = model[iter_][0]
if service in app.interface.instances[self.account]['search']: Search(self.account, service, self.window.window)
app.interface.instances[self.account]['search'][service].window.\
present()
else:
app.interface.instances[self.account]['search'][service] = \
SearchWindow(self.account, service)
def cleanup(self): def cleanup(self):
AgentBrowser.cleanup(self) AgentBrowser.cleanup(self)

View File

@ -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)

344
gajim/gtk/search.py Normal file
View File

@ -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)