From d295472a8e8545f70cae1029e099cbb50942873f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sat, 22 Sep 2018 11:59:07 +0200 Subject: [PATCH] Refactor link context menu - Move hyperlink handling into HtmlTextView - Use actions on the menuitems --- gajim/app_actions.py | 44 ++++++-- gajim/application.py | 13 ++- gajim/conversation_textview.py | 163 ++-------------------------- gajim/data/gui/chat_context_menu.ui | 71 ------------ gajim/gui_menu_builder.py | 76 ++++++++++++- gajim/htmltextview.py | 125 ++++++++------------- gajim/roster_window.py | 2 +- 7 files changed, 176 insertions(+), 318 deletions(-) delete mode 100644 gajim/data/gui/chat_context_menu.ui diff --git a/gajim/app_actions.py b/gajim/app_actions.py index 4471084ae..1e76a72d0 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -14,6 +14,9 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . +from gi.repository import Gtk +from gi.repository import Gdk + from gajim.common import app from gajim.common import helpers from gajim.common.app import interface @@ -130,24 +133,29 @@ def on_service_disco(action, param): pass -def on_join_gc(action, param): - account = None +def on_join_gc(_action, param): + account, jid = None, None if param is None: if not app.get_connected_accounts(): return else: - account = param.get_string() + account, jid = param.get_strv() + if not jid: + jid = None window = app.get_app_window(JoinGroupchatWindow) if window is None: - JoinGroupchatWindow(account, None) + JoinGroupchatWindow(account, jid) else: window.present() -def on_add_contact(action, param): - window = app.get_app_window(AddNewContactWindow) +def on_add_contact(_action, param): + account, jid = param.get_strv() + if not jid: + jid = None + window = app.get_app_window(AddNewContactWindow, account) if window is None: - AddNewContactWindow(param.get_string()) + AddNewContactWindow(account, jid) else: window.present() @@ -337,3 +345,25 @@ def show_next_pending_event(action, param): if not event: return app.interface.handle_event(account, jid, event.type_) + + +def open_link(_action, param): + kind, link = param.get_strv() + helpers.launch_browser_mailer(kind, link) + + +def copy_link(_action, param): + text = param.get_string() + clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clip.set_text(text, -1) + + +def start_chat(_action, param): + account, jid = param.get_strv() + app.interface.new_chat_from_jid(account, jid) + + +def join_groupchat(_action, param): + account, jid = param.get_strv() + room_jid = jid.split('?')[0] + app.interface.join_gc_minimal(account, room_jid) diff --git a/gajim/application.py b/gajim/application.py index 7ab32c38b..0ea34f025 100644 --- a/gajim/application.py +++ b/gajim/application.py @@ -363,6 +363,14 @@ class GajimApplication(Gtk.Application): act.connect("activate", app_actions.on_add_contact_jid) self.add_action(act) + act = Gio.SimpleAction.new('copy-link', GLib.VariantType.new('s')) + act.connect("activate", app_actions.copy_link) + self.add_action(act) + + act = Gio.SimpleAction.new('open-link', GLib.VariantType.new('as')) + act.connect("activate", app_actions.open_link) + self.add_action(act) + for action in general_actions: action_name, func = action act = Gio.SimpleAction.new(action_name, None) @@ -388,8 +396,9 @@ class GajimApplication(Gtk.Application): return [ ('-start-single-chat', a.on_single_message, 'online', 's'), - ('-join-groupchat', a.on_join_gc, 'online', 's'), - ('-add-contact', a.on_add_contact, 'online', 's'), + ('-start-chat', a.start_chat, 'online', 'as'), + ('-join-groupchat', a.on_join_gc, 'online', 'as'), + ('-add-contact', a.on_add_contact, 'online', 'as'), ('-services', a.on_service_disco, 'online', 's'), ('-profile', a.on_profile, 'feature', 's'), ('-xml-console', a.on_xml_console, 'always', 's'), diff --git a/gajim/conversation_textview.py b/gajim/conversation_textview.py index 7e1c2717b..8e5799e22 100644 --- a/gajim/conversation_textview.py +++ b/gajim/conversation_textview.py @@ -23,21 +23,20 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . +import time +import os +import queue +import urllib +import logging +from calendar import timegm + from gi.repository import Gtk -from gi.repository import Gdk from gi.repository import Pango from gi.repository import GObject from gi.repository import GLib -import time -import os -import queue -import urllib - -from gajim.gtk import AddNewContactWindow from gajim.gtk import util from gajim.gtk.util import load_icon -from gajim.gtk.util import get_builder from gajim.gtk.util import get_cursor from gajim.gtk.emoji_data import emoji_pixbufs from gajim.gtk.emoji_data import is_emoji @@ -45,7 +44,7 @@ from gajim.gtk.emoji_data import get_emoji_pixbuf from gajim.common import app from gajim.common import helpers from gajim.common import i18n -from calendar import timegm + from gajim.common.fuzzyclock import FuzzyClock from gajim.common.const import StyleAttr @@ -55,7 +54,6 @@ NOT_SHOWN = 0 ALREADY_RECEIVED = 1 SHOWN = 2 -import logging log = logging.getLogger('gajim.conversation_textview') def is_selection_modified(mark): @@ -172,10 +170,7 @@ class ConversationTextview(GObject.GObject): self.fc = FuzzyClock() # no need to inherit TextView, use it as atrribute is safer - self.tv = HtmlTextView() - # we have to override HtmlTextView Event handlers - # because we don't inherit - self.tv.hyperlink_handler = self.hyperlink_handler + self.tv = HtmlTextView(account) self.tv.connect_tooltip(self.query_tooltip) # set properties @@ -658,146 +653,6 @@ class ConversationTextview(GObject.GObject): finish_sel.forward_word_end() self.selected_phrase = buffer_.get_text(start_sel, finish_sel, True) - def on_open_link_activate(self, widget, kind, text): - helpers.launch_browser_mailer(kind, text) - - def on_copy_link_activate(self, widget, text): - clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clip.set_text(text, -1) - - def on_start_chat_activate(self, widget, jid): - app.interface.new_chat_from_jid(self.account, jid) - - def on_join_group_chat_menuitem_activate(self, widget, room_jid): - # Remove ?join - room_jid = room_jid.split('?')[0] - app.interface.join_gc_minimal(self.account, room_jid) - - def on_add_to_roster_activate(self, widget, jid): - AddNewContactWindow(self.account, jid) - - def make_link_menu(self, event, kind, text): - xml = get_builder('chat_context_menu.ui') - menu = xml.get_object('chat_context_menu') - childs = menu.get_children() - if kind == 'url': - id_ = childs[0].connect('activate', self.on_copy_link_activate, text) - self.handlers[id_] = childs[0] - id_ = childs[1].connect('activate', self.on_open_link_activate, kind, - text) - self.handlers[id_] = childs[1] - childs[2].hide() # copy mail/jid address - childs[3].hide() # open mail composer - childs[4].hide() # jid section separator - childs[5].hide() # start chat - childs[6].hide() # join group chat - childs[7].hide() # add to roster - else: # It's a mail or a JID - text = text.lower() - if text.startswith('xmpp:'): - text = text[5:] - id_ = childs[2].connect('activate', self.on_copy_link_activate, text) - self.handlers[id_] = childs[2] - id_ = childs[3].connect('activate', self.on_open_link_activate, kind, - text) - self.handlers[id_] = childs[3] - id_ = childs[5].connect('activate', self.on_start_chat_activate, text) - self.handlers[id_] = childs[5] - id_ = childs[6].connect('activate', - self.on_join_group_chat_menuitem_activate, text) - self.handlers[id_] = childs[6] - - if self.account and app.connections[self.account].\ - roster_supported: - id_ = childs[7].connect('activate', - self.on_add_to_roster_activate, text) - self.handlers[id_] = childs[7] - childs[7].show() # show add to roster menuitem - else: - childs[7].hide() # hide add to roster menuitem - - if kind == 'xmpp': - id_ = childs[0].connect('activate', self.on_copy_link_activate, - 'xmpp:' + text) - self.handlers[id_] = childs[0] - childs[0].set_label(_('Copy JID')) - childs[2].hide() # copy mail/jid address - childs[3].hide() # open mail composer - childs[4].hide() # jid section separator - elif kind == 'mail': - childs[2].set_label(_('Copy Email Address')) - childs[4].hide() # jid section separator - childs[5].hide() # start chat - childs[6].hide() # join group chat - childs[7].hide() # add to roster - - if kind != 'xmpp': - childs[0].hide() # copy link location - childs[1].hide() # open link in browser - - menu.attach_to_widget(self.tv, None) - menu.popup(None, None, None, None, event.button.button, event.time) - - def hyperlink_handler(self, texttag, widget, event, iter_, kind): - if event.type == Gdk.EventType.BUTTON_PRESS: - begin_iter = iter_.copy() - # we get the beginning of the tag - while not begin_iter.begins_tag(texttag): - begin_iter.backward_char() - end_iter = iter_.copy() - # we get the end of the tag - while not end_iter.ends_tag(texttag): - end_iter.forward_char() - - # Detect XHTML-IM link - word = getattr(texttag, 'href', None) - if word: - if word.startswith('xmpp'): - kind = 'xmpp' - elif word.startswith('mailto:'): - kind = 'mail' - elif app.interface.sth_at_sth_dot_sth_re.match(word): - # it's a JID or mail - kind = 'sth_at_sth' - else: - word = self.tv.get_buffer().get_text(begin_iter, end_iter, True) - - if event.button.button == 3: # right click - self.make_link_menu(event, kind, word) - return True - - self.plugin_modified = False - app.plugin_manager.extension_point( - 'hyperlink_handler', word, kind, self, - self.tv.get_toplevel()) - if self.plugin_modified: - return - - # we launch the correct application - if kind == 'xmpp': - word = word[5:] - if '?' in word: - (jid, action) = word.split('?') - if action == 'join': - app.interface.join_gc_minimal(self.account, jid) - else: - self.on_start_chat_activate(None, jid) - else: - self.on_start_chat_activate(None, word) - # handle geo:-URIs - elif word[:4] == 'geo:': - location = word[4:] - lat, _, lon = location.partition(',') - if lon == '': - return - uri = 'https://www.openstreetmap.org/?' \ - 'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % \ - {'lat': lat, 'lon': lon} - helpers.launch_browser_mailer(kind, uri) - # other URIs - else: - helpers.launch_browser_mailer(kind, word) - def detect_and_print_special_text(self, otext, other_tags, graphics=True, iter_=None, additional_data=None): """ diff --git a/gajim/data/gui/chat_context_menu.ui b/gajim/data/gui/chat_context_menu.ui deleted file mode 100644 index c5ef3ce7e..000000000 --- a/gajim/data/gui/chat_context_menu.ui +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - False - - - True - False - _Copy Link Location - True - - - - - True - False - _Open Link in Browser - True - - - - - True - False - _Copy JID/Email Address - True - - - - - True - False - _Open Email Composer - True - - - - - True - False - - - - - True - False - _Start Chat - True - - - - - True - False - Join _Group Chat - True - - - - - - True - False - _Add to Roster... - True - - - - diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index a849ad7ce..e71f51bef 100644 --- a/gajim/gui_menu_builder.py +++ b/gajim/gui_menu_builder.py @@ -675,7 +675,7 @@ def get_bookmarks_menu(account, rebuild=False): # Build Join Groupchat action = 'app.{}-join-groupchat'.format(account) menuitem = Gio.MenuItem.new(_('Join Group Chat'), action) - variant = GLib.Variant('s', account) + variant = GLib.Variant('as', [account, '']) menuitem.set_action_and_target_value(action, variant) menu.append_item(menuitem) @@ -749,7 +749,10 @@ def get_account_menu(account): continue action = 'app.{}{}'.format(account, action) menuitem = Gio.MenuItem.new(label, action) - variant = GLib.Variant('s', account) + if 'add_contact' in action: + variant = GLib.Variant('as', [account, '']) + else: + variant = GLib.Variant('s', account) menuitem.set_action_and_target_value(action, variant) menu.append_item(menuitem) else: @@ -840,6 +843,75 @@ def get_encryption_menu(control_id, type_id, zeroconf=False): return menu +def get_conv_context_menu(account, kind, text): + if kind == 'xmpp': + if '?join' in text: + context_menu = [ + ('copy-link', _('Copy JID')), + ('-join-groupchat', _('Join Groupchat')), + ] + else: + context_menu = [ + ('copy-link', _('Copy JID')), + ('-start-chat', _('Start Chat')), + ('-add-contact', _('Add to Roster…')), + ] + + elif kind == 'url': + context_menu = [ + ('copy-link', _('Copy Link Location')), + ('open-link', _('Open Link in Browser')), + ] + + elif kind == 'mail': + context_menu = [ + ('copy-link', _('Copy Email Address')), + ('open-link', _('Open Email Composer')), + ] + + elif kind == 'sth_at_sth': + context_menu = [ + ('copy-link', _('Copy JID/Email')), + ('open-link', _('Open Email Composer')), + ('-start-chat', _('Start Chat')), + ('-join-groupchat', _('Join Groupchat')), + ('-add-contact', _('Add to Roster…')), + ] + else: + return + + menu = Gtk.Menu() + for item in context_menu: + action, label = item + menuitem = Gtk.MenuItem() + menuitem.set_label(label) + + if action.startswith('-'): + action = 'app.%s%s' % (account, action) + else: + action = 'app.%s' % action + menuitem.set_action_name(action) + + if 'join-groupchat' in action: + text = text.replace('xmpp:', '') + text = text.split('?')[0] + + if 'add-contact' in action: + text = text.replace('xmpp:', '') + text = text.split('?')[0] + + if action == 'app.open-link': + value = GLib.Variant.new_strv([kind, text]) + elif action == 'app.copy-link': + value = GLib.Variant.new_string(text) + else: + value = GLib.Variant.new_strv([account, text]) + menuitem.set_action_target_value(value) + menuitem.show() + menu.append(menuitem) + return menu + + def escape_mnemonic(label): if label is None: return diff --git a/gajim/htmltextview.py b/gajim/htmltextview.py index ffed884af..ac8ce38ba 100644 --- a/gajim/htmltextview.py +++ b/gajim/htmltextview.py @@ -43,16 +43,16 @@ from gi.repository import Pango from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf +from gi.repository import GLib from gajim.common import app from gajim.common import helpers from gajim.common.i18n import _ from gajim.common.const import StyleAttr -from gajim.gtk import JoinGroupchatWindow -from gajim.gtk import AddNewContactWindow from gajim.gtk.util import load_icon from gajim.gtk.util import get_cursor -from gajim.gtk.util import get_builder + +from gajim.gui_menu_builder import get_conv_context_menu log = logging.getLogger('gajim.htmlview') @@ -825,7 +825,7 @@ class HtmlHandler(xml.sax.handler.ContentHandler): class HtmlTextView(Gtk.TextView): - def __init__(self): + def __init__(self, account=None): Gtk.TextView.__init__(self) self.set_wrap_mode(Gtk.WrapMode.CHAR) self.set_editable(False) @@ -835,11 +835,12 @@ class HtmlTextView(Gtk.TextView): self.connect('unrealize', self.on_html_text_view_unrealized) self.connect('copy-clipboard', self.on_html_text_view_copy_clipboard) self.id_ = self.connect('button-release-event', - self.on_left_mouse_button_release) + self.on_left_mouse_button_release) self.get_buffer().eol_tag = self.get_buffer().create_tag('eol') self.config = app.config self.interface = app.interface - # end big hack + self.account = account + self.plugin_modified = False def connect_tooltip(self, func=None): self.connect('query-tooltip', func or self.__query_tooltip) @@ -892,73 +893,21 @@ class HtmlTextView(Gtk.TextView): self._changed_cursor = False return False - def on_open_link_activate(self, widget, kind, text): - helpers.launch_browser_mailer(kind, text) + def show_context_menu(self, _event, kind, text): + menu = get_conv_context_menu(self.account, kind, text) + if menu is None: + return - def on_copy_link_activate(self, widget, text): - clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clip.set_text(text, -1) + def destroy(menu, pspec): + visible = menu.get_property('visible') + if not visible: + GLib.idle_add(menu.destroy) -# def on_start_chat_activate(self, widget, jid): -# app.interface.new_chat_from_jid(self.account, jid) + menu.attach_to_widget(self, None) + menu.connect('notify::visible', destroy) + menu.popup_at_pointer() - def on_join_group_chat_menuitem_activate(self, widget, room_jid): - JoinGroupchatWindow(None, room_jid) - - def on_add_to_roster_activate(self, widget, jid): - AddNewContactWindow(self.account, jid) - - def make_link_menu(self, event, kind, text): - ui = get_builder('chat_context_menu.ui') - menu = ui.get_object('chat_context_menu') - childs = menu.get_children() - if kind == 'url': - childs[0].connect('activate', self.on_copy_link_activate, text) - childs[1].connect('activate', self.on_open_link_activate, kind, - text) - childs[2].hide() # copy mail address - childs[3].hide() # open mail composer - childs[4].hide() # jid section separator - childs[5].hide() # start chat - childs[6].hide() # join group chat - childs[7].hide() # add to roster - else: # It's a mail or a JID - text = text.lower() - if text.startswith('xmpp:'): - text = text[5:] - childs[2].connect('activate', self.on_copy_link_activate, text) - childs[3].connect('activate', self.on_open_link_activate, kind, - text) -# childs[5].connect('activate', self.on_start_chat_activate, text) - childs[6].connect('activate', - self.on_join_group_chat_menuitem_activate, text) - -# if self.account and app.connections[self.account].\ -# roster_supported: -# childs[7].connect('activate', -# self.on_add_to_roster_activate, text) -# childs[7].show() # show add to roster menuitem -# else: -# childs[7].hide() # hide add to roster menuitem - - if kind == 'xmpp': - childs[0].connect('activate', self.on_copy_link_activate, - 'xmpp:' + text) - childs[2].hide() # copy mail address - childs[3].hide() # open mail composer - elif kind == 'mail': - childs[6].hide() # join group chat - - if kind != 'xmpp': - childs[0].hide() # copy link location - childs[1].hide() # open link in browser - childs[4].hide() # jid section separator - childs[5].hide() # start chat - childs[7].hide() # add to roster - - menu.popup(None, None, None, event.button, event.time) - - def hyperlink_handler(self, texttag, widget, event, iter_, kind): + def hyperlink_handler(self, texttag, _widget, event, iter_, kind): if event.type == Gdk.EventType.BUTTON_PRESS: begin_iter = iter_.copy() # we get the beginning of the tag @@ -980,31 +929,45 @@ class HtmlTextView(Gtk.TextView): # it's a JID or mail kind = 'sth_at_sth' else: - word = self.textview.get_buffer().get_text(begin_iter, - end_iter) + word = self.get_buffer().get_text(begin_iter, end_iter, True) - if event.button == 3: # right click - self.make_link_menu(event, kind, word) + if event.button.button == 3: # right click + self.show_context_menu(event, kind, word) return True + self.plugin_modified = False + app.plugin_manager.extension_point( + 'hyperlink_handler', word, kind, self, + self.get_toplevel()) + if self.plugin_modified: + return + # we launch the correct application if kind == 'xmpp': word = word[5:] if '?' in word: (jid, action) = word.split('?') if action == 'join': - self.on_join_group_chat_menuitem_activate(None, jid) + app.interface.join_gc_minimal(self.account, jid) else: - self.on_start_chat_activate(None, jid) + app.interface.new_chat_from_jid(self.account, jid) else: - self.on_start_chat_activate(None, word) + app.interface.new_chat_from_jid(self.account, word) + + # handle geo:-URIs + elif word[:4] == 'geo:': + location = word[4:] + lat, _, lon = location.partition(',') + if lon == '': + return + uri = 'https://www.openstreetmap.org/?' \ + 'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % \ + {'lat': lat, 'lon': lon} + helpers.launch_browser_mailer(kind, uri) + # other URIs else: helpers.launch_browser_mailer(kind, word) - def _hyperlink_handler(self, texttag, widget, event, iter_, kind): - # self.hyperlink_handler can be overwritten, so call it when needed - self.hyperlink_handler(texttag, widget, event, iter_, kind) - def display_html(self, html, textview, conv_textview, iter_=None): buffer_ = self.get_buffer() if iter_: diff --git a/gajim/roster_window.py b/gajim/roster_window.py index 29fb4839c..42bd1d732 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -3679,7 +3679,7 @@ class RosterWindow: When the join gc menuitem is clicked, show the join gc window """ app.app.activate_action('%s-join-groupchat' % account, - GLib.Variant('s', account)) + GLib.Variant('as', [account, ''])) def on_show_transports_action(self, action, param): app.config.set('show_transports_group', param.get_boolean())