StartChatDialog: Add Muclumbus search

This commit is contained in:
Philipp Hörist 2019-03-01 21:45:50 +01:00
parent 6c06d2c497
commit 553436332b
3 changed files with 494 additions and 4 deletions

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 --> <!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="box"> <object class="GtkBox" id="box">
@ -25,7 +25,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow" id="scrolledwindow">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property> <property name="hscrollbar_policy">never</property>
@ -53,4 +53,218 @@
</packing> </packing>
</child> </child>
</object> </object>
<object class="GtkGrid" id="search_result_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">50</property>
<property name="xalign">0</property>
<style>
<class name="bold"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="jid_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">50</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">system-users-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="user_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Users</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<object class="GtkGrid" id="tooltip">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="tooltip_name">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="halign">start</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">50</property>
<property name="xalign">0</property>
<style>
<class name="bold"/>
</style>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tooltip_jid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">50</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tooltip_description">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="halign">start</property>
<property name="wrap">True</property>
<property name="max_width_chars">65</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tooltip_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Name:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tooltip_description_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Description:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Address:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tooltip_user">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="icon_name">system-users-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="anonymous_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Semi-Anonymous
&lt;span size="small"&gt;Only moderators can see your XMPP-Address)&lt;/span&gt;</property>
<property name="use_markup">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="anonymous_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="icon_name">security-medium-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
</interface> </interface>

View file

@ -134,6 +134,15 @@ list.settings > row > box {
#StartChatListBox > row { padding: 10px 20px 10px 10px; } #StartChatListBox > row { padding: 10px 20px 10px 10px; }
#StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color } #StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color }
/* StartChatListBox */
.start-chat-row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; }
.start-chat-row:last-child { border-bottom: 0px}
.start-chat-row.activatable:active { box-shadow: none; }
.start-chat-row { padding: 10px 20px 10px 10px; }
.start-chat-row:not(.activatable) label { color: @insensitive_fg_color }
.start-chat-row:focus { outline: none; }
/* GroupchatConfig */ /* GroupchatConfig */
#GroupchatConfig > box > buttonbox { margin: 0px 12px 12px 12px; } #GroupchatConfig > box > buttonbox { margin: 0px 12px 12px 12px; }
#GroupchatConfig stack { border-bottom: 1px solid; border-color: @borders;} #GroupchatConfig stack { border-bottom: 1px solid; border-color: @borders;}

View file

@ -13,12 +13,16 @@
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import locale import locale
from enum import IntEnum
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Pango from gi.repository import Pango
from nbxmpp.util import is_error_result
from nbxmpp.const import AnonymityMode
from gajim.common import app from gajim.common import app
from gajim.common import helpers from gajim.common import helpers
from gajim.common.i18n import _ from gajim.common.i18n import _
@ -26,6 +30,15 @@ from gajim.common.const import AvatarSize
from gajim.gtk.util import get_icon_name from gajim.gtk.util import get_icon_name
from gajim.gtk.util import get_builder from gajim.gtk.util import get_builder
from gajim.gtk.util import ensure_not_destroyed
SERVICE_JID = 'rodrigo.de.mucobedo@dreckshal.de'
class Search(IntEnum):
CONTACT = 0
MUC = 1
class StartChatDialog(Gtk.ApplicationWindow): class StartChatDialog(Gtk.ApplicationWindow):
@ -38,6 +51,9 @@ class StartChatDialog(Gtk.ApplicationWindow):
self.set_title(_('Start new Conversation')) self.set_title(_('Start new Conversation'))
self.set_default_size(-1, 400) self.set_default_size(-1, 400)
self.ready_to_destroy = False self.ready_to_destroy = False
self._parameter_form = None
self._destroyed = False
self._search_stopped = False
self.builder = get_builder('start_chat_dialog.ui') self.builder = get_builder('start_chat_dialog.ui')
self.listbox = self.builder.get_object('listbox') self.listbox = self.builder.get_object('listbox')
@ -66,6 +82,9 @@ class StartChatDialog(Gtk.ApplicationWindow):
self.listbox.set_sort_func(self._sort_func, None) self.listbox.set_sort_func(self._sort_func, None)
self.listbox.connect('row-activated', self._on_row_activated) self.listbox.connect('row-activated', self._on_row_activated)
self._muc_search_listbox = MUCSearch()
self._current_listbox = self.listbox
self.connect('key-press-event', self._on_key_press) self.connect('key-press-event', self._on_key_press)
self.connect('destroy', self._destroy) self.connect('destroy', self._destroy)
@ -118,7 +137,7 @@ class StartChatDialog(Gtk.ApplicationWindow):
return True return True
if (event.state == Gdk.ModifierType.SHIFT_MASK and if (event.state == Gdk.ModifierType.SHIFT_MASK and
event.keyval == Gdk.KEY_ISO_Left_Tab): event.keyval == Gdk.KEY_ISO_Left_Tab):
self.search_entry.emit('previous-match') self.search_entry.emit('previous-match')
return True return True
@ -127,6 +146,8 @@ class StartChatDialog(Gtk.ApplicationWindow):
return True return True
if event.keyval == Gdk.KEY_Escape: if event.keyval == Gdk.KEY_Escape:
self._search_stopped = True
self._muc_search_listbox.remove_all()
if self.search_entry.get_text() != '': if self.search_entry.get_text() != '':
self.search_entry.emit('stop-search') self.search_entry.emit('stop-search')
else: else:
@ -134,6 +155,11 @@ class StartChatDialog(Gtk.ApplicationWindow):
return True return True
if event.keyval == Gdk.KEY_Return: if event.keyval == Gdk.KEY_Return:
if self._current_listbox_is(Search.MUC):
self._muc_search_listbox.remove_all()
self._start_search()
return True
row = self.listbox.get_selected_row() row = self.listbox.get_selected_row()
if row is not None: if row is not None:
row.emit('activate') row.emit('activate')
@ -159,8 +185,27 @@ class StartChatDialog(Gtk.ApplicationWindow):
self.ready_to_destroy = True self.ready_to_destroy = True
def _set_listbox(self, listbox):
if self._current_listbox == listbox:
return
viewport = self.builder.scrolledwindow.get_child()
viewport.remove(viewport.get_child())
self.builder.scrolledwindow.remove(viewport)
self.builder.scrolledwindow.add(listbox)
self._current_listbox = listbox
def _current_listbox_is(self, box):
if self._current_listbox == self.listbox:
return box == Search.CONTACT
return box == Search.MUC
def _on_search_changed(self, entry): def _on_search_changed(self, entry):
search_text = entry.get_text() search_text = entry.get_text()
if search_text.startswith('g:'):
self._set_listbox(self._muc_search_listbox)
return
self._set_listbox(self.listbox)
if '@' in search_text: if '@' in search_text:
self._add_new_jid_row() self._add_new_jid_row()
self._update_new_jid_rows(search_text) self._update_new_jid_rows(search_text)
@ -256,7 +301,83 @@ class StartChatDialog(Gtk.ApplicationWindow):
return locale.strcoll(name1.lower(), name2.lower()) return locale.strcoll(name1.lower(), name2.lower())
@staticmethod @staticmethod
def _destroy(*args): def _get_connection():
accounts = app.get_connected_accounts()
if not accounts:
raise NotConnected
return app.connections[accounts.pop()].connection
def _start_search(self):
self._search_stopped = False
try:
con = self._get_connection()
except NotConnected:
return
text = self.search_entry.get_text()
text = text[2:].strip()
self._muc_search_listbox.start_search()
if self._parameter_form is None:
con.get_module('Muclumbus').request_parameters(
SERVICE_JID,
callback=self._parameters_received,
user_data=(con, text))
else:
self._parameter_form.vars['q'].value = text
con.get_module('Muclumbus').set_search(
SERVICE_JID,
self._parameter_form,
callback=self._on_search_result,
user_data=con)
@ensure_not_destroyed
def _parameters_received(self, result, user_data):
if is_error_result(result):
self._on_error(result)
return
con, text = user_data
self._parameter_form = result
self._parameter_form.type_ = 'submit'
self._parameter_form.vars['q'].value = text
con.get_module('Muclumbus').set_search(
SERVICE_JID,
self._parameter_form,
callback=self._on_search_result,
user_data=con)
@ensure_not_destroyed
def _on_search_result(self, result, con):
if self._search_stopped:
return
if is_error_result(result):
self._on_error(result)
return
for item in result.items:
self._muc_search_listbox.add(ResultRow(item))
if result.end:
self._muc_search_listbox.end_search()
return
con.get_module('Muclumbus').set_search(
SERVICE_JID,
self._parameter_form,
items_per_page=result.max,
after=result.last,
callback=self._on_search_result,
user_data=con)
def _on_error(self, result):
self._muc_search_listbox.show_error(result)
def _destroy(self, *args):
self._destroyed = True
del app.interface.instances['start_chat'] del app.interface.instances['start_chat']
@ -345,3 +466,149 @@ class ContactRow(Gtk.Grid):
if self.show_account: if self.show_account:
return '%s %s %s' % (self.name, self.jid, self.account_label) return '%s %s %s' % (self.name, self.jid, self.account_label)
return '%s %s' % (self.name, self.jid) return '%s %s' % (self.name, self.jid)
class MUCSearch(Gtk.ListBox):
def __init__(self):
Gtk.ListBox.__init__(self)
self.set_has_tooltip(True)
self.set_activate_on_single_click(False)
self._progress = None
self._ui = get_builder('start_chat_dialog.ui', ['tooltip'])
self.connect('query-tooltip', self._query_tooltip)
self.connect('row-activated', self._on_row_activated)
self.show_all()
def remove_all(self):
def remove(row):
self.remove(row)
row.destroy()
self.foreach(remove)
def show_error(self, error):
self.insert(ErrorRow(error), 0)
self.remove(self._progress)
self._progress.destroy()
def start_search(self):
self._progress = ProgressRow()
super().add(self._progress)
def end_search(self):
self._progress.stop()
def add(self, row):
super().add(row)
self._progress.update()
def _query_tooltip(self, _widget, _x_pos, y_pos, _keyboard_mode, tooltip):
row = self.get_row_at_y(y_pos)
if row is None or not isinstance(row, ResultRow):
self._clear_tooltip()
return False
if row.item.anonymity_mode == AnonymityMode.SEMI:
self._ui.anonymous_icon.show()
self._ui.anonymous_label.show()
self._ui.tooltip_user.set_text(row.item.nusers)
self._ui.tooltip_jid.set_text(row.item.jid)
if row.item.description:
self._ui.tooltip_description.set_text(row.item.description)
self._ui.tooltip_description.show()
self._ui.tooltip_description_label.show()
if row.item.name:
self._ui.tooltip_name.set_text(row.item.name)
self._ui.tooltip_name.show()
self._ui.tooltip_name_label.show()
tooltip.set_custom(self._ui.tooltip)
return True
def _clear_tooltip(self):
self._ui.anonymous_icon.hide()
self._ui.anonymous_label.hide()
self._ui.tooltip_description.hide()
self._ui.tooltip_description_label.hide()
self._ui.tooltip_name.hide()
self._ui.tooltip_name_label.hide()
def _on_row_activated(self, _listbox, row):
app.interface.join_gc_minimal(None, row.item.jid)
class ResultRow(Gtk.ListBoxRow):
def __init__(self, item):
Gtk.ListBoxRow.__init__(self)
self.set_activatable(True)
self.get_style_context().add_class('start-chat-row')
self.item = item
self._ui = get_builder('start_chat_dialog.ui', ['search_result_grid'])
self._ui.jid_label.set_text(item.jid)
self._ui.name_label.set_text(item.name)
self._ui.user_label.set_text(item.nusers)
self.add(self._ui.search_result_grid)
self.show_all()
class ErrorRow(Gtk.ListBoxRow):
def __init__(self, error):
Gtk.ListBoxRow.__init__(self)
self.set_selectable(False)
self.set_activatable(False)
self.get_style_context().add_class('start-chat-row')
self._error = Gtk.Label(label=str(error))
self._error.set_line_wrap(True)
self._error.set_line_wrap_mode(Pango.WrapMode.WORD)
self._error.set_xalign(0)
self._error_image = Gtk.Image.new_from_icon_name(
'dialog-error', Gtk.IconSize.MENU)
self._error_image.get_style_context().add_class('error-color')
self._error_image.set_valign(Gtk.Align.CENTER)
box = Gtk.Box()
box.set_spacing(6)
box.add(self._error_image)
box.add(self._error)
self.add(box)
self.show_all()
class ProgressRow(Gtk.ListBoxRow):
def __init__(self):
Gtk.ListBoxRow.__init__(self)
self.set_selectable(False)
self.set_activatable(False)
self.get_style_context().add_class('start-chat-row')
self._text = _('%s Group chats found')
self._count = 0
self._spinner = Gtk.Spinner()
self._spinner.start()
self._count_label = Gtk.Label(label=self._text % 0)
self._count_label.get_style_context().add_class('bold')
self._finished_image = Gtk.Image.new_from_icon_name(
'emblem-ok-symbolic', Gtk.IconSize.MENU)
self._finished_image.get_style_context().add_class('success-color')
self._finished_image.set_no_show_all(True)
box = Gtk.Box()
box.set_spacing(6)
box.add(self._finished_image)
box.add(self._spinner)
box.add(self._count_label)
self.add(box)
self.show_all()
def update(self):
self._count += 1
self._count_label.set_text(self._text % self._count)
def stop(self):
self._spinner.stop()
self._spinner.hide()
self._finished_image.show()
class NotConnected(Exception):
pass