From 970d6f8c3f5b5895eda4fe45087c79197ba79ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 3 Oct 2017 13:08:06 +0200 Subject: [PATCH 1/2] New style for ChatControl - Move ActionBar into HeaderMenu - Make Design of ChatControl look cleaner - Hide the Roster in Groupchats per default - Add Button to hide/show Roster in Groupchats - Move Groupchat topic into popover - Display Avatars on the right side of the ChatControl and status on the left - Add a default Avatar for contacts that have none --- gajim/chat_control.py | 280 ++++----- gajim/chat_control_base.py | 77 ++- .../command_system/implementation/standard.py | 6 - gajim/common/config.py | 1 - gajim/common/connection.py | 7 + gajim/common/const.py | 2 +- gajim/config.py | 10 - gajim/data/gui/chat_control.ui | 471 ++++----------- gajim/data/gui/groupchat_control.ui | 460 ++++++-------- gajim/data/gui/message_window.ui | 26 +- gajim/data/gui/preferences_window.ui | 25 +- gajim/data/gui/roster_window.ui | 25 + gajim/data/style/gajim.css | 31 +- gajim/emoticons.py | 1 + gajim/gajim.py | 11 +- gajim/groupchat_control.py | 565 ++++++++---------- gajim/gtkgui_helpers.py | 1 + gajim/gtkspell.py | 1 + gajim/gui_menu_builder.py | 75 ++- gajim/message_control.py | 7 - gajim/message_textview.py | 29 +- gajim/message_window.py | 51 +- gajim/roster_window.py | 9 + gajim/scrolled_window.py | 89 --- 24 files changed, 994 insertions(+), 1266 deletions(-) delete mode 100644 gajim/scrolled_window.py diff --git a/gajim/chat_control.py b/gajim/chat_control.py index a00f6b81b..d21aaf192 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -30,7 +30,7 @@ import os import time from gi.repository import Gtk -from gi.repository import Gdk +from gi.repository import Gio from gi.repository import GdkPixbuf from gi.repository import Pango from gi.repository import GLib @@ -95,60 +95,10 @@ class ChatControl(ChatControlBase): self.last_recv_message_marks = None self.last_message_timestamp = None - # for muc use: - # widget = self.xml.get_object('muc_window_actions_button') - self.actions_button = self.xml.get_object('message_window_actions_button') - id_ = self.actions_button.connect('clicked', - self.on_actions_button_clicked) - self.handlers[id_] = self.actions_button - self._formattings_button = self.xml.get_object('formattings_button') self.emoticons_button = self.xml.get_object('emoticons_button') self.toggle_emoticons() - self._add_to_roster_button = self.xml.get_object( - 'add_to_roster_button') - id_ = self._add_to_roster_button.connect('clicked', - self._on_add_to_roster_menuitem_activate) - self.handlers[id_] = self._add_to_roster_button - - self._audio_button = self.xml.get_object('audio_togglebutton') - id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled) - self.handlers[id_] = self._audio_button - # add a special img - gtkgui_helpers.add_image_to_button(self._audio_button, - 'gajim-mic_inactive') - - self._video_button = self.xml.get_object('video_togglebutton') - id_ = self._video_button.connect('toggled', self.on_video_button_toggled) - self.handlers[id_] = self._video_button - # add a special img - gtkgui_helpers.add_image_to_button(self._video_button, - 'gajim-cam_inactive') - - self._send_file_button = self.xml.get_object('send_file_button') - # add a special img for send file button - pixbuf = gtkgui_helpers.get_icon_pixmap('document-send', quiet=True) - img = Gtk.Image.new_from_pixbuf(pixbuf) - self._send_file_button.set_image(img) - id_ = self._send_file_button.connect('clicked', - self._on_send_file_menuitem_activate) - self.handlers[id_] = self._send_file_button - - self._convert_to_gc_button = self.xml.get_object( - 'convert_to_gc_button') - id_ = self._convert_to_gc_button.connect('clicked', - self._on_convert_to_gc_menuitem_activate) - self.handlers[id_] = self._convert_to_gc_button - - self._contact_information_button = self.xml.get_object( - 'contact_information_button') - id_ = self._contact_information_button.connect('clicked', - self._on_contact_information_menuitem_activate) - self.handlers[id_] = self._contact_information_button - - compact_view = app.config.get('compact_view') - self.chat_buttons_set_visible(compact_view) self.widget_set_visible(self.xml.get_object('banner_eventbox'), app.config.get('hide_chat_banner')) @@ -161,10 +111,9 @@ class ChatControl(ChatControlBase): # Add lock image to show chat encryption self.lock_image = self.xml.get_object('lock_image') - # Convert to GC icon - img = self.xml.get_object('convert_to_gc_button_image') - img.set_from_pixbuf(gtkgui_helpers.load_icon( - 'muc_active').get_pixbuf()) + # Menu for the HeaderBar + self.control_menu = gui_menu_builder.get_singlechat_menu( + self.control_id) self._audio_banner_image = self.xml.get_object('audio_banner_image') self._video_banner_image = self.xml.get_object('video_banner_image') @@ -270,7 +219,7 @@ class ChatControl(ChatControlBase): # Enable encryption if needed self.no_autonegotiation = False - + self.add_actions() self.update_ui() self.set_lock_image() @@ -298,6 +247,107 @@ class ChatControl(ChatControlBase): # PluginSystem: adding GUI extension point for this ChatControl # instance object app.plugin_manager.gui_extension_point('chat_control', self) + self.update_actions() + + def add_actions(self): + actions = [ + ('send-file-', self._on_send_file), + ('invite-contacts-', self._on_invite_contacts), + ('add-to-roster-', self._on_add_to_roster), + ('information-', self._on_information), + ] + + for action in actions: + action_name, func = action + act = Gio.SimpleAction.new(action_name + self.control_id, None) + act.connect("activate", func) + self.parent_win.window.add_action(act) + + act = Gio.SimpleAction.new_stateful( + 'toggle-audio-' + self.control_id, None, + GLib.Variant.new_boolean(False)) + act.connect('change-state', self._on_audio) + self.parent_win.window.add_action(act) + + act = Gio.SimpleAction.new_stateful( + 'toggle-video-' + self.control_id, + None, GLib.Variant.new_boolean(False)) + act.connect('change-state', self._on_video) + self.parent_win.window.add_action(act) + + def update_actions(self): + win = self.parent_win.window + online = app.account_is_connected(self.account) + + # Add to roster + if not isinstance(self.contact, GC_Contact) \ + and _('Not in Roster') in self.contact.groups and \ + app.connections[self.account].roster_supported and online: + win.lookup_action( + 'add-to-roster-' + self.control_id).set_enabled(True) + else: + win.lookup_action( + 'add-to-roster-' + self.control_id).set_enabled(False) + + # Audio + win.lookup_action('toggle-audio-' + self.control_id).set_enabled( + online and self.audio_available) + + # Video + win.lookup_action('toggle-video-' + self.control_id).set_enabled( + online and self.video_available) + + # Send file + if ((self.contact.supports(NS_FILE) or \ + self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and \ + (self.type_id == 'chat' or self.gc_contact.resource)) and \ + self.contact.show != 'offline' and online: + win.lookup_action('send-file-' + self.control_id).set_enabled( + True) + else: + win.lookup_action('send-file-' + self.control_id).set_enabled( + False) + + # Convert to GC + if app.config.get_per('accounts', self.account, 'is_zeroconf'): + win.lookup_action( + 'invite-contacts-' + self.control_id).set_enabled(False) + else: + if self.contact.supports(NS_MUC) and online: + win.lookup_action( + 'invite-contacts-' + self.control_id).set_enabled(True) + else: + win.lookup_action( + 'invite-contacts-' + self.control_id).set_enabled(False) + + # Information + win.lookup_action( + 'information-' + self.control_id).set_enabled(online) + + def _on_send_file(self, action, param): + super()._on_send_file() + + def _on_add_to_roster(self, action, param): + dialogs.AddNewContactWindow(self.account, self.contact.jid) + + def _on_information(self, action, param): + app.interface.roster.on_info(None, self.contact, self.account) + + def _on_invite_contacts(self, action, param): + """ + User wants to invite some friends to chat + """ + dialogs.TransformChatToMUC(self.account, [self.contact.jid]) + + def _on_audio(self, action, param): + action.set_state(param) + state = param.get_boolean() + self.on_jingle_button_toggled(state, 'audio') + + def _on_video(self, action, param): + action.set_state(param) + state = param.get_boolean() + self.on_jingle_button_toggled(state, 'video') def subscribe_events(self): """ @@ -329,14 +379,6 @@ class ChatControl(ChatControlBase): self._formattings_button.set_tooltip_text(_('This contact does ' 'not support HTML')) - # Add to roster - if not isinstance(self.contact, GC_Contact) \ - and _('Not in Roster') in self.contact.groups and \ - app.connections[self.account].roster_supported: - self._add_to_roster_button.show() - else: - self._add_to_roster_button.hide() - # Jingle detection if self.contact.supports(NS_JINGLE_ICE_UDP) and \ app.HAVE_FARSTREAM and self.contact.resource: @@ -348,63 +390,6 @@ class ChatControl(ChatControlBase): self.video_available = False self.audio_available = False - # Audio buttons - self._audio_button.set_sensitive(self.audio_available) - - # Video buttons - self._video_button.set_sensitive(self.video_available) - - # change tooltip text for audio and video buttons if farstream is - # not installed - audio_tooltip_text = _('Toggle audio session') + '\n' - video_tooltip_text = _('Toggle video session') + '\n' - if not app.HAVE_FARSTREAM: - ext_text = _('Feature not available, see Help->Features') - self._audio_button.set_tooltip_text(audio_tooltip_text + ext_text) - self._video_button.set_tooltip_text(video_tooltip_text + ext_text) - elif not self.audio_available : - ext_text =_('Feature not supported by remote client') - self._audio_button.set_tooltip_text(audio_tooltip_text + ext_text) - self._video_button.set_tooltip_text(video_tooltip_text + ext_text) - else: - self._audio_button.set_tooltip_text(audio_tooltip_text[:-1]) - self._video_button.set_tooltip_text(video_tooltip_text[:-1]) - - # Send file - if ((self.contact.supports(NS_FILE) or \ - self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and \ - (self.type_id == 'chat' or self.gc_contact.resource)) and \ - self.contact.show != 'offline': - self._send_file_button.set_sensitive(True) - self._send_file_button.set_tooltip_text(_('Send files')) - else: - self._send_file_button.set_sensitive(False) - if not (self.contact.supports(NS_FILE) or self.contact.supports( - NS_JINGLE_FILE_TRANSFER_5)): - self._send_file_button.set_tooltip_text(_( - "This contact does not support file transfer.")) - else: - self._send_file_button.set_tooltip_text( - _("You need to know the real JID of the contact to send " - "them a file.")) - - # Convert to GC - if app.config.get_per('accounts', self.account, 'is_zeroconf'): - self._convert_to_gc_button.set_no_show_all(True) - self._convert_to_gc_button.hide() - else: - if self.contact.supports(NS_MUC): - self._convert_to_gc_button.set_sensitive(True) - else: - self._convert_to_gc_button.set_sensitive(False) - - # Information - if app.account_is_disconnected(self.account): - self._contact_information_button.set_sensitive(False) - else: - self._contact_information_button.set_sensitive(True) - - def update_all_pep_types(self): for pep_type in self._pep_images: self.update_pep(pep_type) @@ -751,12 +736,12 @@ class ChatControl(ChatControlBase): getattr(self, '_' + jingle_type + '_button').set_active(False) getattr(self, 'update_' + jingle_type)() - def on_jingle_button_toggled(self, widget, jingle_type): + def on_jingle_button_toggled(self, state, jingle_type): img_name = 'gajim-%s_%s' % ({'audio': 'mic', 'video': 'cam'}[jingle_type], - {True: 'active', False: 'inactive'}[widget.get_active()]) + {True: 'active', False: 'inactive'}[state]) path_to_img = gtkgui_helpers.get_icon_path(img_name) - if widget.get_active(): + if state: if getattr(self, jingle_type + '_state') == \ self.JINGLE_STATE_NULL: if jingle_type == 'video': @@ -795,12 +780,6 @@ class ChatControl(ChatControlBase): img = getattr(self, '_' + jingle_type + '_button').get_property('image') img.set_from_file(path_to_img) - def on_audio_button_toggled(self, widget): - self.on_jingle_button_toggled(widget, 'audio') - - def on_video_button_toggled(self, widget): - self.on_jingle_button_toggled(widget, 'video') - def set_lock_image(self): loggable = self.session and self.session.is_loggable() @@ -832,10 +811,6 @@ class ChatControl(ChatControlBase): self.authentication_button.set_tooltip_text(tooltip) self.widget_set_visible(self.authentication_button, not visible) context = self.msg_scrolledwindow.get_style_context() - if visible: - context.add_class('authentication') - else: - context.remove_class('authentication') self.lock_image.set_sensitive(visible) def _on_authentication_button_clicked(self, widget): @@ -1272,10 +1247,18 @@ class ChatControl(ChatControlBase): if not app.config.get('show_avatar_in_chat'): return - pixbuf = app.contacts.get_avatar( - self.account, self.contact.jid, AvatarSize.CHAT) + if self.TYPE_ID == message_control.TYPE_CHAT: + pixbuf = app.contacts.get_avatar( + self.account, self.contact.jid, AvatarSize.CHAT) + else: + pixbuf = app.interface.get_avatar( + self.gc_contact.avatar_sha, AvatarSize.CHAT) + image = self.xml.get_object('avatar_image') - image.set_from_pixbuf(pixbuf) + if pixbuf is None: + image.set_from_icon_name('avatar-default', Gtk.IconSize.DIALOG) + else: + image.set_from_pixbuf(pixbuf) def _nec_update_avatar(self, obj): if obj.account != self.account: @@ -1446,15 +1429,6 @@ class ChatControl(ChatControlBase): elif typ == 'pm': control.remove_contact(nick) - def _on_send_file_menuitem_activate(self, widget): - self._on_send_file() - - def _on_add_to_roster_menuitem_activate(self, widget): - dialogs.AddNewContactWindow(self.account, self.contact.jid) - - def _on_contact_information_menuitem_activate(self, widget): - app.interface.roster.on_info(widget, self.contact, self.account) - def _on_convert_to_gc_menuitem_activate(self, widget): """ User wants to invite some friends to chat @@ -1522,21 +1496,11 @@ class ChatControl(ChatControlBase): if contact: self.contact = contact self.draw_banner() + self.update_actions() def got_disconnected(self): - # Add to roster - self._add_to_roster_button.hide() - # Audio button - self._audio_button.set_sensitive(False) - # Video button - self._video_button.set_sensitive(False) - # Send file button - self._send_file_button.set_tooltip_text('') - self._send_file_button.set_sensitive(False) - # Convert to GC button - self._convert_to_gc_button.set_sensitive(False) - ChatControlBase.got_disconnected(self) + self.update_actions() def update_status_display(self, name, uf_show, status): """ diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py index 23c5045b9..db05c10d6 100644 --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -44,7 +44,6 @@ from gajim import notify import re from gajim import emoticons -from gajim.scrolled_window import ScrolledWindow from gajim.common import events from gajim.common import app from gajim.common import helpers @@ -256,28 +255,21 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): MessageControl.__init__(self, type_id, parent_win, widget_name, contact, acct, resource=resource) - widget = self.xml.get_object('history_button') - # set document-open-recent icon for history button - if gtkgui_helpers.gtk_icon_theme.has_icon('document-open-recent'): - img = self.xml.get_object('history_image') - img.set_from_icon_name('document-open-recent', Gtk.IconSize.MENU) - - id_ = widget.connect('clicked', self._on_history_menuitem_activate) - self.handlers[id_] = widget - - # Create banner and connect signals - widget = self.xml.get_object('banner_eventbox') - id_ = widget.connect('button-press-event', - self._on_banner_eventbox_button_press_event) - self.handlers[id_] = widget + if self.TYPE_ID != message_control.TYPE_GC: + # Create banner and connect signals + widget = self.xml.get_object('banner_eventbox') + id_ = widget.connect('button-press-event', + self._on_banner_eventbox_button_press_event) + self.handlers[id_] = widget self.urlfinder = re.compile( r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]") self.banner_status_label = self.xml.get_object('banner_label') - id_ = self.banner_status_label.connect('populate_popup', - self.on_banner_label_populate_popup) - self.handlers[id_] = self.banner_status_label + if self.banner_status_label is not None: + id_ = self.banner_status_label.connect('populate_popup', + self.on_banner_label_populate_popup) + self.handlers[id_] = self.banner_status_label # Init DND self.TARGET_TYPE_URI_LIST = 80 @@ -332,9 +324,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.msg_scrolledwindow = ScrolledWindow() self.msg_scrolledwindow.set_max_content_height(100) - self.msg_scrolledwindow.set_min_content_height(23) + self.msg_scrolledwindow.set_propagate_natural_height(True) self.msg_scrolledwindow.get_style_context().add_class('scrolledtextview') - self.msg_scrolledwindow.set_property('shadow_type', Gtk.ShadowType.IN) self.msg_scrolledwindow.add(self.msg_textview) @@ -417,6 +408,28 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): action.connect("change-state", self.change_encryption) self.parent_win.window.add_action(action) + action = Gio.SimpleAction.new( + 'browse-history-%s' % self.control_id, GLib.VariantType.new('s')) + action.connect('activate', self._on_history) + self.parent_win.window.add_action(action) + + # Actions + + def _on_history(self, action, param): + """ + When history menuitem is pressed: call history window + """ + jid = param.get_string() + if jid == 'none': + jid = self.contact.jid + + if 'logs' in app.interface.instances: + app.interface.instances['logs'].window.present() + app.interface.instances['logs'].open_history(jid, self.account) + else: + app.interface.instances['logs'] = \ + history_window.HistoryWindow(jid, self.account) + def change_encryption(self, action, param): encryption = param.get_string() if encryption == 'disabled': @@ -549,6 +562,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): menu.show_all() def on_quote(self, widget, text): + self.msg_textview.remove_placeholder() text = '>' + text.replace('\n', '\n>') + '\n' message_buffer = self.msg_textview.get_buffer() message_buffer.insert_at_cursor(text) @@ -1283,13 +1297,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): else: widget.show_all() - def chat_buttons_set_visible(self, state): - """ - Toggle chat buttons - """ - MessageControl.chat_buttons_set_visible(self, state) - self.widget_set_visible(self.xml.get_object('actions_hbox'), state) - def got_connected(self): self.msg_textview.set_sensitive(True) self.msg_textview.set_editable(True) @@ -1302,3 +1309,19 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.no_autonegotiation = False self.update_toolbar() + + +class ScrolledWindow(Gtk.ScrolledWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def do_get_preferred_height(self): + min_height, natural_height = Gtk.ScrolledWindow.do_get_preferred_height(self) + child = self.get_child() + if natural_height and self.get_max_content_height() > -1 and child: + _, child_nat_height = child.get_preferred_height() + if natural_height > child_nat_height: + if child_nat_height < 26: + return 26, 26 + + return min_height, natural_height diff --git a/gajim/command_system/implementation/standard.py b/gajim/command_system/implementation/standard.py index 87b068e73..0d96cc30f 100644 --- a/gajim/command_system/implementation/standard.py +++ b/gajim/command_system/implementation/standard.py @@ -41,12 +41,6 @@ class StandardCommonCommands(CommandContainer): AUTOMATIC = True HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands - @command - @doc(_("Hide the chat buttons")) - def compact(self): - new_status = not self.hide_chat_buttons - self.chat_buttons_set_visible(new_status) - @command(overlap=True) @doc(_("Show help on a given command or a list of available commands if -a is given")) def help(self, command=None, all=False): diff --git a/gajim/common/config.py b/gajim/common/config.py index 4da11fe47..c805f97ae 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -264,7 +264,6 @@ class Config: 'show_roster_on_startup':[opt_str, 'always', _('Show roster on startup.\n\'always\' - Always show roster.\n\'never\' - Never show roster.\n\'last_state\' - Restore the last state roster.')], 'show_avatar_in_chat': [opt_bool, True, _('If False, you will no longer see the avatar in the chat window.')], 'escape_key_closes': [opt_bool, True, _('If True, pressing the escape key closes a tab/window.')], - 'compact_view': [opt_bool, False, _('Hides the buttons in chat windows.')], 'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window')], 'hide_chat_banner': [opt_bool, False, _('Hides the banner in two persons chat window')], 'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat occupants list in group chat window.')], diff --git a/gajim/common/connection.py b/gajim/common/connection.py index bc4c65183..131049419 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -2342,6 +2342,13 @@ class Connection(CommonConnection, ConnectionHandlers): if rule['type'] == 'group': roster.draw_group(rule['value'], self.name) + def bookmarks_available(self): + if self.private_storage_supported: + return True + if self.pubsub_publish_options_supported: + return True + return False + def _request_bookmarks_xml(self): if not app.account_is_connected(self.name): return diff --git a/gajim/common/const.py b/gajim/common/const.py index 3ab2706b5..baa92d679 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -30,8 +30,8 @@ class OptionType(IntEnum): class AvatarSize(IntEnum): ROSTER = 32 + CHAT = 48 NOTIFICATION = 48 - CHAT = 52 PROFILE = 64 TOOLTIP = 125 VCARD = 200 diff --git a/gajim/config.py b/gajim/config.py index 61ad624fd..12d1a199e 100644 --- a/gajim/config.py +++ b/gajim/config.py @@ -187,10 +187,6 @@ class PreferencesWindow: else: show_roster_combobox.set_active(0) - # Compact View - st = app.config.get('compact_view') - self.xml.get_object('compact_view_checkbutton').set_active(st) - # Ignore XHTML st = app.config.get('ignore_incoming_xhtml') self.xml.get_object('xhtml_checkbutton').set_active(st) @@ -657,12 +653,6 @@ class PreferencesWindow: config_type = c_config.opt_show_roster_on_startup[active] app.config.set('show_roster_on_startup', config_type) - def on_compact_view_checkbutton_toggled(self, widget): - active = widget.get_active() - for ctrl in self._get_all_controls(): - ctrl.chat_buttons_set_visible(active) - app.config.set('compact_view', active) - def on_xhtml_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'ignore_incoming_xhtml') helpers.update_optional_features() diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui index db4abefe6..d50f13956 100644 --- a/gajim/data/gui/chat_control.ui +++ b/gajim/data/gui/chat_control.ui @@ -374,7 +374,6 @@ True False vertical - 5 False diff --git a/gajim/data/gui/message_window.ui b/gajim/data/gui/message_window.ui index 0aab2b026..854cd7db3 100644 --- a/gajim/data/gui/message_window.ui +++ b/gajim/data/gui/message_window.ui @@ -1,5 +1,5 @@ - + @@ -28,10 +28,10 @@ 70 True False - 0 True end 9 + 0 False @@ -64,15 +64,35 @@ + + True + False + True + + + True + True + True + + + True + False + open-menu-symbolic + + + + + + MessageWindow False 480 440 + False True True - 2 True diff --git a/gajim/data/gui/preferences_window.ui b/gajim/data/gui/preferences_window.ui index 15a2156a9..101c4f255 100644 --- a/gajim/data/gui/preferences_window.ui +++ b/gajim/data/gui/preferences_window.ui @@ -426,23 +426,6 @@ 2 - - - Ma_ke message windows compact - True - False - Hide all buttons in chat windows - True - 0 - True - - - - 0 - 3 - 2 - - _Ignore rich content in incoming messages @@ -457,7 +440,7 @@ 0 - 4 + 3 2 @@ -474,7 +457,7 @@ 0 - 5 + 4 2 @@ -490,7 +473,7 @@ 0 - 6 + 5 2 @@ -555,7 +538,7 @@ 0 - 7 + 6 2 diff --git a/gajim/data/gui/roster_window.ui b/gajim/data/gui/roster_window.ui index e70381f9e..4601484bc 100644 --- a/gajim/data/gui/roster_window.ui +++ b/gajim/data/gui/roster_window.ui @@ -4,6 +4,7 @@ + RosterWindow 85 200 False @@ -115,4 +116,28 @@ + + True + False + Gajim + True + + + True + True + True + True + + + True + False + open-menu-symbolic + + + + + end + + + diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index 83c6b9267..ce8663eff 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -1,28 +1,31 @@ /* Gajim Application CSS File */ -.msgtextview-button { + +.chatcontrol-actionbar-button { padding: 0px 5px 0px 5px; background-color: @theme_base_color; - border: 1px solid; - border-radius: 0px; - border-color: @borders; + border: none; } -.msgtextview-button:hover, .msgtextview-button:checked { - color: @theme_base_color; - border-color: @borders; - text-shadow: none; - -gtk-icon-shadow: none; - box-shadow: none; +.scrolled-no-border {border: none} +.scrolled-no-border undershoot.top, undershoot.bottom { background-image: none; } + +.actionbar-no-border box {border: none} + +.actionbar-no-border button { + padding: 0px; + background-color: @theme_base_color; + border: none; background-image: none; } +#MessageWindow, #RosterWindow paned { background-color: @theme_base_color; } -.msgtextview-button.left { border-right: none; } -.msgtextview-button.right { border-left: none; } +.scrolledtextview { border:none; } -.scrolledtextview { border-left:none; } -.scrolledtextview.authentication { border-right:none; } +.chatcontrol-separator {margin-bottom: 6px;} + +#SubjectPopover box { padding: 10px; } /* VCardWindow */ .VCard-GtkLinkButton { padding-left: 5px; border-left: none; } diff --git a/gajim/emoticons.py b/gajim/emoticons.py index f0e6c6ceb..2d39b60ee 100644 --- a/gajim/emoticons.py +++ b/gajim/emoticons.py @@ -289,6 +289,7 @@ class EmoticonPopover(Gtk.Popover): self.append_emoticon(child.get_child().get_text()) def append_emoticon(self, pix): + self.text_widget.remove_placeholder() buffer_ = self.text_widget.get_buffer() if buffer_.get_char_count(): buffer_.insert_at_cursor(' ') diff --git a/gajim/gajim.py b/gajim/gajim.py index ddaa8e7b3..2263ac531 100644 --- a/gajim/gajim.py +++ b/gajim/gajim.py @@ -212,8 +212,15 @@ class GajimApplication(Gtk.Application): builder = Gtk.Builder() builder.set_translation_domain(i18n.APP) builder.add_from_file(path) - self.set_menubar(builder.get_object("menubar")) - self.set_app_menu(builder.get_object("appmenu")) + menubar = builder.get_object("menubar") + appmenu = builder.get_object("appmenu") + if os.name != 'nt': + self.set_app_menu(appmenu) + else: + # Dont set Application Menu for Windows + # Add it to the menubar instead + menubar.prepend_submenu('Gajim', appmenu) + self.set_menubar(menubar) def do_activate(self): Gtk.Application.do_activate(self) diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index 9c9fd90b1..b2c837bed 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -36,6 +36,7 @@ from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Pango from gi.repository import GLib +from gi.repository import Gio from gajim import gtkgui_helpers from gajim import gui_menu_builder from gajim import message_control @@ -251,15 +252,6 @@ class PrivateChatControl(ChatControl): return self.show_avatar() - def show_avatar(self): - if not app.config.get('show_avatar_in_chat'): - return - - pixbuf = app.interface.get_avatar( - self.gc_contact.avatar_sha, AvatarSize.CHAT) - image = self.xml.get_object('avatar_image') - image.set_from_pixbuf(pixbuf) - def update_contact(self): self.contact = self.gc_contact.as_contact() @@ -274,20 +266,6 @@ class PrivateChatControl(ChatControl): self.session.negotiate_e2e(False) - def prepare_context_menu(self, hide_buttonbar_items=False): - """ - Set compact view menuitem active state sets active and sensitivity state - for history_menuitem (False for tranasports) and file_transfer_menuitem - and hide()/show() for add_to_roster_menuitem - """ - menu = gui_menu_builder.get_contact_menu(self.contact, self.account, - use_multiple_contacts=False, show_start_chat=False, - show_encryption=True, control=self, - show_buttonbar_items=not hide_buttonbar_items, - gc_contact=self.gc_contact, - is_anonymous=self.room_ctrl.is_anonymous) - return menu - def got_disconnected(self): ChatControl.got_disconnected(self) @@ -317,56 +295,18 @@ class GroupchatControl(ChatControlBase): # Keep error dialog instance to be sure to have only once at a time self.error_dialog = None - self.actions_button = self.xml.get_object('muc_window_actions_button') - id_ = self.actions_button.connect('clicked', - self.on_actions_button_clicked) - self.handlers[id_] = self.actions_button - self.emoticons_button = self.xml.get_object('emoticons_button') self.toggle_emoticons() - widget = self.xml.get_object('change_nick_button') - widget.set_sensitive(False) - id_ = widget.connect('clicked', self._on_change_nick_menuitem_activate) - self.handlers[id_] = widget - - widget = self.xml.get_object('change_subject_button') - widget.set_sensitive(False) - id_ = widget.connect('clicked', - self._on_change_subject_menuitem_activate) - self.handlers[id_] = widget - formattings_button = self.xml.get_object('formattings_button') formattings_button.set_sensitive(False) - widget = self.xml.get_object('bookmark_button') - for bm in app.connections[self.account].bookmarks: - if bm['jid'] == self.contact.jid: - widget.hide() - break - else: - id_ = widget.connect('clicked', - self._on_bookmark_room_menuitem_activate) - self.handlers[id_] = widget - - if gtkgui_helpers.gtk_icon_theme.has_icon('bookmark-new'): - img = self.xml.get_object('image7') - img.set_from_icon_name('bookmark-new', Gtk.IconSize.MENU) - widget.set_sensitive( - app.connections[self.account].private_storage_supported or \ - (app.connections[self.account].pep_supported and \ - app.connections[self.account].pubsub_publish_options_supported)) - widget.show() - - if gtkgui_helpers.gtk_icon_theme.has_icon('document-open-recent'): - img = self.xml.get_object('history_image') - img.set_from_icon_name('document-open-recent', Gtk.IconSize.MENU) - self.current_tooltip = None if parent_win is not None: # On AutoJoin with minimize Groupchats are created without parent - # Tooltip Window has to be created with parent + # Tooltip Window and Actions have to be created with parent self.set_tooltip() + self.add_actions() widget = self.xml.get_object('list_treeview') id_ = widget.connect('row_expanded', self.on_list_treeview_row_expanded) @@ -399,8 +339,6 @@ class GroupchatControl(ChatControlBase): if not self.name: self.name = self.room_jid.split('@')[0] - compact_view = app.config.get('compact_view') - self.chat_buttons_set_visible(compact_view) self.widget_set_visible(self.xml.get_object('banner_eventbox'), app.config.get('hide_groupchat_banner')) self.widget_set_visible(self.xml.get_object('list_scrolledwindow'), @@ -444,6 +382,10 @@ class GroupchatControl(ChatControlBase): id_ = self.hpaned.connect('notify', self.on_hpaned_notify) self.handlers[id_] = self.hpaned + # Hide the Roster per default + self.hpaned.get_child2().set_no_show_all(True) + self.hpaned.get_child2().hide() + # set the position of the current hpaned hpaned_position = app.config.get('gc-hpaned-position') self.hpaned.set_position(hpaned_position) @@ -516,6 +458,22 @@ class GroupchatControl(ChatControlBase): gui_menu_builder.get_encryption_menu(self.control_id, self.type_id)) self.set_encryption_menu_icon() + # Banner + self.banner_actionbar = self.xml.get_object('banner_actionbar') + self.hide_roster_button = Gtk.Button.new_from_icon_name( + 'go-previous-symbolic', Gtk.IconSize.MENU) + self.hide_roster_button.connect('clicked', + lambda *args: self.show_roster()) + self.subject_button = Gtk.MenuButton() + self.subject_button.set_image(Gtk.Image.new_from_icon_name( + 'go-down-symbolic', Gtk.IconSize.MENU)) + self.subject_button.set_popover(SubjectPopover()) + self.subject_button.set_no_show_all(True) + self.banner_actionbar.pack_end(self.hide_roster_button) + self.banner_actionbar.pack_start(self.subject_button) + + self.control_menu = gui_menu_builder.get_groupchat_menu(self.control_id) + app.ged.register_event_handler('gc-presence-received', ged.GUI1, self._nec_gc_presence_received) app.ged.register_event_handler('gc-message-received', ged.GUI1, @@ -538,14 +496,199 @@ class GroupchatControl(ChatControlBase): self.update_ui() self.widget.show_all() - # PluginSystem: adding GUI extension point for this GroupchatControl # instance object app.plugin_manager.gui_extension_point('groupchat_control', self) + def add_actions(self): + actions = [ + ('change-subject-', self._on_change_subject), + ('change-nick-', self._on_change_nick), + ('disconnect-', self._on_disconnect), + ('destroy-', self._on_destroy_room), + ('configure-', self._on_configure_room), + ('bookmark-', self._on_bookmark_room), + ('request-voice-', self._on_request_voice), + ] + + for action in actions: + action_name, func = action + act = Gio.SimpleAction.new(action_name + self.control_id, None) + act.connect("activate", func) + self.parent_win.window.add_action(act) + + non_minimized_gc = app.config.get_per( + 'accounts', self.account, 'non_minimized_gc').split() + value = self.contact.jid not in non_minimized_gc + + act = Gio.SimpleAction.new_stateful( + 'minimize-' + self.control_id, None, + GLib.Variant.new_boolean(value)) + act.connect('change-state', self._on_minimize) + self.parent_win.window.add_action(act) + + value = app.config.get_per( + 'rooms', self.contact.jid, 'notify_on_all_messages') + + act = Gio.SimpleAction.new_stateful( + 'notify-on-message-' + self.control_id, + None, GLib.Variant.new_boolean(value)) + act.connect('change-state', self._on_notify_on_all_messages) + self.parent_win.window.add_action(act) + + def update_actions(self): + if self.parent_win is None: + return + win = self.parent_win.window + contact = app.contacts.get_gc_contact( + self.account, self.room_jid, self.nick) + online = app.gc_connected[self.account][self.room_jid] + + # Destroy Room + win.lookup_action('destroy-' + self.control_id).set_enabled( + online and contact.affiliation == 'owner') + + # Configure Room + win.lookup_action('configure-' + self.control_id).set_enabled( + online and contact.affiliation in ('admin', 'owner')) + + # Bookmarks + con = app.connections[self.account] + bookmark_support = con.bookmarks_available() + not_bookmarked = True + for bm in con.bookmarks: + if bm['jid'] == self.room_jid: + not_bookmarked = False + break + win.lookup_action('bookmark-' + self.control_id).set_enabled( + online and bookmark_support and not_bookmarked) + + # Request Voice + role = self.get_role(self.nick) + win.lookup_action('request-voice-' + self.control_id).set_enabled( + online and role == 'visitor') + + # Change Subject + # Get this from Room Disco + win.lookup_action('change-subject-' + self.control_id).set_enabled( + online) + + # Change Nick + win.lookup_action('change-nick-' + self.control_id).set_enabled( + online) + + # Actions + + def _on_change_subject(self, action, param): + def on_ok(subject): + # Note, we don't update self.subject since we don't know whether it + # will work yet + app.connections[self.account].send_gc_subject( + self.room_jid, subject) + + dialogs.InputTextDialog(_('Changing Subject'), + _('Please specify the new subject:'), input_str=self.subject, + ok_handler=on_ok, transient_for=self.parent_win.window) + + def _on_change_nick(self, action, param): + if 'change_nick_dialog' in app.interface.instances: + app.interface.instances['change_nick_dialog'].dialog.present() + else: + title = _('Changing Nickname') + prompt = _('Please specify the new nickname you want to use:') + app.interface.instances['change_nick_dialog'] = \ + dialogs.ChangeNickDialog(self.account, self.room_jid, title, + prompt, change_nick=True, transient_for=self.parent_win.window) + + def _on_disconnect(self, action, param): + self.force_non_minimizable = True + self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND) + self.force_non_minimizable = False + + def _on_destroy_room(self, action, param): + def on_ok(reason, jid): + if jid: + # Test jid + try: + jid = helpers.parse_jid(jid) + except Exception: + dialogs.ErrorDialog(_('Invalid group chat JID'), + _('The group chat JID has not allowed characters.')) + return + app.connections[self.account].destroy_gc_room( + self.room_jid, reason, jid) + + # Ask for a reason + dialogs.DoubleInputDialog(_('Destroying %s') % '\u200E' + \ + self.room_jid, _('You are going to remove this room permanently.' + '\nYou may specify a reason below:'), + _('You may also enter an alternate venue:'), ok_handler=on_ok, + transient_for=self.parent_win.window) + + def _on_configure_room(self, action, param): + c = app.contacts.get_gc_contact( + self.account, self.room_jid, self.nick) + if c.affiliation == 'owner': + app.connections[self.account].request_gc_config(self.room_jid) + elif c.affiliation == 'admin': + if self.room_jid not in app.interface.instances[self.account][ + 'gc_config']: + app.interface.instances[self.account]['gc_config'][ + self.room_jid] = config.GroupchatConfigWindow(self.account, + self.room_jid) + + def _on_bookmark_room(self, action, param): + """ + Bookmark the room, without autojoin and not minimized + """ + password = app.gc_passwords.get(self.room_jid, '') + app.interface.add_gc_bookmark( + self.account, self.name, self.room_jid, + '0', '0', password, self.nick) + + def _on_request_voice(self, action, param): + """ + Request voice in the current room + """ + app.connections[self.account].request_voice(self.room_jid) + + def _on_minimize(self, action, param): + """ + When a grouchat is minimized, unparent the tab, put it in roster etc + """ + action.set_state(param) + non_minimized_gc = app.config.get_per( + 'accounts', self.account, 'non_minimized_gc').split() + + minimize = param.get_boolean() + if minimize: + non_minimized_gc.remove(self.contact.jid) + else: + non_minimized_gc.append(self.contact.jid) + + app.config.set_per('accounts', self.account, + 'non_minimized_gc', ' '.join(non_minimized_gc)) + + def _on_notify_on_all_messages(self, action, param): + action.set_state(param) + app.config.set_per('rooms', self.contact.jid, + 'notify_on_all_messages', param.get_boolean()) + + def show_roster(self): + new_state = not self.hpaned.get_child2().is_visible() + image = self.hide_roster_button.get_image() + if new_state: + self.hpaned.get_child2().show() + image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU) + else: + self.hpaned.get_child2().hide() + image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU) + def on_groupchat_maximize(self): self.set_tooltip() self.add_window_actions() + self.add_actions() + self.update_actions() self.set_lock_image() self._schedule_activity_timers() @@ -823,10 +966,6 @@ class GroupchatControl(ChatControlBase): self.authentication_button.set_tooltip_text(tooltip) self.widget_set_visible(self.authentication_button, not visible) context = self.msg_scrolledwindow.get_style_context() - if visible: - context.add_class('authentication') - else: - context.remove_class('authentication') self.lock_image.set_sensitive(visible) def _on_authentication_button_clicked(self, widget): @@ -886,7 +1025,6 @@ class GroupchatControl(ChatControlBase): room jid, subject """ self.name_label.set_ellipsize(Pango.EllipsizeMode.END) - self.banner_status_label.set_ellipsize(Pango.EllipsizeMode.END) font_attrs, font_attrs_small = self.get_font_attrs() if self.is_continued: name = self.get_continued_conversation_name() @@ -896,169 +1034,10 @@ class GroupchatControl(ChatControlBase): self.name_label.set_markup(text) if self.subject: - subject = helpers.reduce_chars_newlines(self.subject, max_lines=2) - subject = GLib.markup_escape_text(subject) + subject = GLib.markup_escape_text(self.subject) subject_text = self.urlfinder.sub(self.make_href, subject) - subject_text = '%s' % (font_attrs_small, - subject_text) - - # tooltip must always hold ALL the subject - self.event_box.set_tooltip_text(self.subject) - self.banner_status_label.set_no_show_all(False) - self.banner_status_label.show() - else: - subject_text = '' - self.event_box.set_has_tooltip(False) - self.banner_status_label.hide() - self.banner_status_label.set_no_show_all(True) - - self.banner_status_label.set_markup(subject_text) - - def prepare_context_menu(self, hide_buttonbar_items=False): - """ - Set sensitivity state for configure_room - """ - xml = gtkgui_helpers.get_gtk_builder('gc_control_popup_menu.ui') - menu = xml.get_object('gc_control_popup_menu') - - bookmark_room_menuitem = xml.get_object('bookmark_room_menuitem') - change_nick_menuitem = xml.get_object('change_nick_menuitem') - configure_room_menuitem = xml.get_object('configure_room_menuitem') - destroy_room_menuitem = xml.get_object('destroy_room_menuitem') - change_subject_menuitem = xml.get_object('change_subject_menuitem') - history_menuitem = xml.get_object('history_menuitem') - disconnect_menuitem = xml.get_object('disconnect_menuitem') - minimize_menuitem = xml.get_object('minimize_menuitem') - notify_menuitem = xml.get_object('notify_menuitem') - request_voice_menuitem = xml.get_object('request_voice_menuitem') - bookmark_separator = xml.get_object('bookmark_separator') - separatormenuitem2 = xml.get_object('separatormenuitem2') - request_voice_separator = xml.get_object('request_voice_separator') - - if hide_buttonbar_items: - change_nick_menuitem.hide() - change_subject_menuitem.hide() - bookmark_room_menuitem.hide() - history_menuitem.hide() - bookmark_separator.hide() - separatormenuitem2.hide() - else: - change_nick_menuitem.show() - change_subject_menuitem.show() - bookmark_room_menuitem.show() - history_menuitem.show() - bookmark_separator.show() - separatormenuitem2.show() - for bm in app.connections[self.account].bookmarks: - if bm['jid'] == self.room_jid: - bookmark_room_menuitem.hide() - bookmark_separator.hide() - break - - ag = Gtk.accel_groups_from_object(self.parent_win.window)[0] - change_nick_menuitem.add_accelerator('activate', ag, Gdk.KEY_n, - Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK, Gtk.AccelFlags.VISIBLE) - change_subject_menuitem.add_accelerator('activate', ag, - Gdk.KEY_t, Gdk.ModifierType.MOD1_MASK, Gtk.AccelFlags.VISIBLE) - bookmark_room_menuitem.add_accelerator('activate', ag, Gdk.KEY_b, - Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) - history_menuitem.add_accelerator('activate', ag, Gdk.KEY_h, - Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) - - if self.contact.jid not in app.config.get_per('accounts', self.account, - 'non_minimized_gc').split(' '): - minimize_menuitem.set_active(True) - notify_menuitem.set_active(app.config.get_per('rooms', self.contact.jid, - 'notify_on_all_messages')) - conn = app.connections[self.account] - if not conn.private_storage_supported and (not conn.pep_supported or \ - not conn.pubsub_publish_options_supported): - bookmark_room_menuitem.set_sensitive(False) - if app.gc_connected[self.account][self.room_jid]: - c = app.contacts.get_gc_contact(self.account, self.room_jid, - self.nick) - if c.affiliation not in ('owner', 'admin'): - configure_room_menuitem.set_sensitive(False) - else: - configure_room_menuitem.set_sensitive(True) - if c.affiliation != 'owner': - destroy_room_menuitem.set_sensitive(False) - else: - destroy_room_menuitem.set_sensitive(True) - change_subject_menuitem.set_sensitive(True) - change_nick_menuitem.set_sensitive(True) - if c.role == 'visitor': - request_voice_menuitem.set_sensitive(True) - else: - request_voice_menuitem.set_sensitive(False) - else: - # We are not connected to this groupchat, disable unusable menuitems - configure_room_menuitem.set_sensitive(False) - destroy_room_menuitem.set_sensitive(False) - change_subject_menuitem.set_sensitive(False) - change_nick_menuitem.set_sensitive(False) - request_voice_menuitem.set_sensitive(False) - - # connect the menuitems to their respective functions - id_ = bookmark_room_menuitem.connect('activate', - self._on_bookmark_room_menuitem_activate) - self.handlers[id_] = bookmark_room_menuitem - - id_ = change_nick_menuitem.connect('activate', - self._on_change_nick_menuitem_activate) - self.handlers[id_] = change_nick_menuitem - - id_ = configure_room_menuitem.connect('activate', - self._on_configure_room_menuitem_activate) - self.handlers[id_] = configure_room_menuitem - - id_ = destroy_room_menuitem.connect('activate', - self._on_destroy_room_menuitem_activate) - self.handlers[id_] = destroy_room_menuitem - - id_ = change_subject_menuitem.connect('activate', - self._on_change_subject_menuitem_activate) - self.handlers[id_] = change_subject_menuitem - - id_ = history_menuitem.connect('activate', - self._on_history_menuitem_activate) - self.handlers[id_] = history_menuitem - - id_ = disconnect_menuitem.connect('activate', - self._on_disconnect_menuitem_activate) - self.handlers[id_] = disconnect_menuitem - - id_ = request_voice_menuitem.connect('activate', - self._on_request_voice_menuitem_activate) - self.handlers[id_] = request_voice_menuitem - - id_ = minimize_menuitem.connect('toggled', - self.on_minimize_menuitem_toggled) - self.handlers[id_] = minimize_menuitem - - id_ = notify_menuitem.connect('toggled', - self.on_notify_menuitem_toggled) - self.handlers[id_] = notify_menuitem - - menu.connect('selection-done', self.destroy_menu, - change_nick_menuitem, change_subject_menuitem, - bookmark_room_menuitem, history_menuitem) - return menu - - def destroy_menu(self, menu, change_nick_menuitem, change_subject_menuitem, - bookmark_room_menuitem, history_menuitem): - # destroy accelerators - ag = Gtk.accel_groups_from_object(self.parent_win.window)[0] - change_nick_menuitem.remove_accelerator(ag, Gdk.KEY_n, - Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) - change_subject_menuitem.remove_accelerator(ag, Gdk.KEY_t, - Gdk.ModifierType.MOD1_MASK) - bookmark_room_menuitem.remove_accelerator(ag, Gdk.KEY_b, - Gdk.ModifierType.CONTROL_MASK) - history_menuitem.remove_accelerator(ag, Gdk.KEY_h, - Gdk.ModifierType.CONTROL_MASK) - # destroy menu - menu.destroy() + subject_text = '%s' % subject_text + self.subject_button.get_popover().set_text(subject_text) def _nec_vcard_published(self, obj): if obj.conn.name != self.account: @@ -1379,6 +1358,11 @@ class GroupchatControl(ChatControlBase): else: self.print_conversation(text) + if obj.subject == '': + self.subject_button.hide() + else: + self.subject_button.show() + def _nec_gc_config_changed_received(self, obj): # statuscode is a list # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify @@ -1467,18 +1451,12 @@ class GroupchatControl(ChatControlBase): formattings_button = self.xml.get_object('formattings_button') formattings_button.set_sensitive(True) - change_nick_button = self.xml.get_object('change_nick_button') - change_nick_button.set_sensitive(True) - change_subject_button = self.xml.get_object('change_subject_button') - change_subject_button.set_sensitive(True) + + self.update_actions() def got_disconnected(self): formattings_button = self.xml.get_object('formattings_button') formattings_button.set_sensitive(False) - change_nick_button = self.xml.get_object('change_nick_button') - change_nick_button.set_sensitive(False) - change_subject_button = self.xml.get_object('change_subject_button') - change_subject_button.set_sensitive(False) self.list_treeview.set_model(None) self.model.clear() nick_list = app.contacts.get_nick_list(self.account, self.room_jid) @@ -1512,6 +1490,8 @@ class GroupchatControl(ChatControlBase): if ar_to: self.autorejoin = GLib.timeout_add_seconds(ar_to, self.rejoin) + self.update_actions() + def rejoin(self): if not self.autorejoin: return False @@ -1904,6 +1884,11 @@ class GroupchatControl(ChatControlBase): st += ' (' + obj.status + ')' self.print_conversation(st, graphics=False) + # Update Actions + if obj.status_code: + if '110' in obj.status_code: + self.update_actions() + def add_contact_to_roster(self, nick, show, role, affiliation, status, jid='', avatar_sha=None): role_name = helpers.get_uf_role(role, plural=True) @@ -2262,67 +2247,6 @@ class GroupchatControl(ChatControlBase): _('Please specify the new subject:'), input_str=self.subject, ok_handler=on_ok, transient_for=self.parent_win.window) - def _on_disconnect_menuitem_activate(self, widget): - self.force_non_minimizable = True - self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND) - self.force_non_minimizable = False - - def _on_change_nick_menuitem_activate(self, widget): - if 'change_nick_dialog' in app.interface.instances: - app.interface.instances['change_nick_dialog'].dialog.present() - else: - title = _('Changing Nickname') - prompt = _('Please specify the new nickname you want to use:') - app.interface.instances['change_nick_dialog'] = \ - dialogs.ChangeNickDialog(self.account, self.room_jid, title, - prompt, change_nick=True, transient_for=self.parent_win.window) - - def _on_configure_room_menuitem_activate(self, widget): - c = app.contacts.get_gc_contact(self.account, self.room_jid, - self.nick) - if c.affiliation == 'owner': - app.connections[self.account].request_gc_config(self.room_jid) - elif c.affiliation == 'admin': - if self.room_jid not in app.interface.instances[self.account][ - 'gc_config']: - app.interface.instances[self.account]['gc_config'][ - self.room_jid] = config.GroupchatConfigWindow(self.account, - self.room_jid) - - def _on_destroy_room_menuitem_activate(self, widget): - def on_ok(reason, jid): - if jid: - # Test jid - try: - jid = helpers.parse_jid(jid) - except Exception: - dialogs.ErrorDialog(_('Invalid group chat JID'), - _('The group chat JID has not allowed characters.')) - return - app.connections[self.account].destroy_gc_room(self.room_jid, - reason, jid) - - # Ask for a reason - dialogs.DoubleInputDialog(_('Destroying %s') % '\u200E' + \ - self.room_jid, _('You are going to remove this room permanently.' - '\nYou may specify a reason below:'), - _('You may also enter an alternate venue:'), ok_handler=on_ok, - transient_for=self.parent_win.window) - - def _on_bookmark_room_menuitem_activate(self, widget): - """ - Bookmark the room, without autojoin and not minimized - """ - password = app.gc_passwords.get(self.room_jid, '') - app.interface.add_gc_bookmark(self.account, self.name, self.room_jid,\ - '0', '0', password, self.nick) - - def _on_request_voice_menuitem_activate(self, widget): - """ - Request voice in the current room - """ - app.connections[self.account].request_voice(self.room_jid) - def _on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp): # Invite contact to groupchat @@ -2913,3 +2837,40 @@ class GroupchatControl(ChatControlBase): self.grant_owner(widget, jid) else: self.revoke_owner(widget, jid) + + +class SubjectPopover(Gtk.Popover): + def __init__(self): + Gtk.Popover.__init__(self) + self.set_name('SubjectPopover') + + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_max_content_height(250) + scrolledwindow.set_propagate_natural_height(True) + scrolledwindow.set_propagate_natural_width(True) + scrolledwindow.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + + self.label = Gtk.Label() + self.label.set_line_wrap(True) + self.label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) + self.label.set_max_width_chars(80) + + scrolledwindow.add(self.label) + + box = Gtk.Box() + box.add(scrolledwindow) + box.show_all() + self.add(box) + + self.connect_after('show', self._after_show) + + def set_text(self, text): + self.label.set_markup(text) + + def _after_show(self, *args): + # Gtk Bug: If we set selectable True, on show + # everything inside the Label is selected. + # So we switch after show to False and again to True + self.label.set_selectable(False) + self.label.set_selectable(True) diff --git a/gajim/gtkgui_helpers.py b/gajim/gtkgui_helpers.py index c44b41e78..1db67d1bc 100644 --- a/gajim/gtkgui_helpers.py +++ b/gajim/gtkgui_helpers.py @@ -57,6 +57,7 @@ class Color: BLACK = Gdk.RGBA(red=0, green=0, blue=0, alpha=1) GREEN = Gdk.RGBA(red=115/255, green=210/255, blue=22/255, alpha=1) RED = Gdk.RGBA(red=204/255, green=0, blue=0, alpha=1) + GREY = Gdk.RGBA(red=195/255, green=195/255, blue=192/255, alpha=1) def get_icon_pixmap(icon_name, size=16, color=None, quiet=False): try: diff --git a/gajim/gtkspell.py b/gajim/gtkspell.py index bb75b8e1c..24d52e9d8 100644 --- a/gajim/gtkspell.py +++ b/gajim/gtkspell.py @@ -23,6 +23,7 @@ import gi gi.require_version('GtkSpell', '3.0') from gi.repository import GtkSpell + def ensure_attached(func): def f(self, *args, **kwargs): if self.spell: diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index b07d9b45d..8436acf3d 100644 --- a/gajim/gui_menu_builder.py +++ b/gajim/gui_menu_builder.py @@ -606,6 +606,65 @@ Build dynamic Application Menus ''' +def get_singlechat_menu(control_id): + singlechat_menu = [ + ('win.send-file-', _('Send File...')), + ('win.invite-contacts-', _('Invite Contacts')), + ('win.add-to-roster-', _('Add to Roster')), + ('win.toggle-audio-', _('Audio Session')), + ('win.toggle-video-', _('Video Session')), + ('win.information-', _('Information')), + ('win.browse-history-', _('History')), + ] + + def build_menu(preset): + menu = Gio.Menu() + for item in preset: + action_name, label = item + if action_name == 'win.browse-history-': + menu.append(label, action_name + control_id + '::none') + else: + menu.append(label, action_name + control_id) + return menu + + return build_menu(singlechat_menu) + + +def get_groupchat_menu(control_id): + groupchat_menu = [ + (_('Manage Room'), [ + ('win.change-subject-', _('Change Subject')), + ('win.configure-', _('Configure Room')), + ('win.destroy-', _('Destroy Room')), + ]), + ('win.change-nick-', _('Change Nick')), + ('win.bookmark-', _('Bookmark Room')), + ('win.request-voice-', _('Request Voice')), + ('win.notify-on-message-', _('Notify on all messages')), + ('win.minimize-', _('Minimize on close')), + ('win.browse-history-', _('History')), + ('win.disconnect-', _('Disconnect')), + ] + + def build_menu(preset): + menu = Gio.Menu() + for item in preset: + if isinstance(item[1], str): + action_name, label = item + if action_name == 'win.browse-history-': + menu.append(label, action_name + control_id + '::none') + else: + menu.append(label, action_name + control_id) + else: + label, sub_menu = item + # This is a submenu + submenu = build_menu(sub_menu) + menu.append_submenu(label, submenu) + return menu + + return build_menu(groupchat_menu) + + def get_bookmarks_menu(account, rebuild=False): if not app.connections[account].bookmarks: return None @@ -708,7 +767,11 @@ def get_account_menu(account): def build_accounts_menu(): menubar = app.app.get_menubar() # Accounts Submenu - acc_menu = menubar.get_item_link(0, 'submenu') + menu_position = 0 + if os.name == 'nt': + menu_position = 1 + + acc_menu = menubar.get_item_link(menu_position, 'submenu') acc_menu.remove_all() accounts_list = sorted(app.contacts.get_accounts()) if not accounts_list: @@ -721,8 +784,8 @@ def build_accounts_menu(): acc, get_account_menu(acc)) else: acc_menu = get_account_menu(accounts_list[0]) - menubar.remove(0) - menubar.insert_submenu(0, 'Accounts', acc_menu) + menubar.remove(menu_position) + menubar.insert_submenu(menu_position, 'Accounts', acc_menu) def build_bookmark_menu(account): @@ -731,8 +794,12 @@ def build_bookmark_menu(account): if not bookmark_menu: return + menu_position = 0 + if os.name == 'nt': + menu_position = 1 + # Accounts Submenu - acc_menu = menubar.get_item_link(0, 'submenu') + acc_menu = menubar.get_item_link(menu_position, 'submenu') # We have more than one Account active if acc_menu.get_item_link(0, 'submenu'): diff --git a/gajim/message_control.py b/gajim/message_control.py index 2246c6ef1..a1216ba88 100644 --- a/gajim/message_control.py +++ b/gajim/message_control.py @@ -57,7 +57,6 @@ class MessageControl(object): self.widget_name = widget_name self.contact = contact self.account = account - self.hide_chat_buttons = False self.resource = resource # control_id is a unique id for the control, # its used as action name for actions that belong to a control @@ -175,12 +174,6 @@ class MessageControl(object): """ return None - def chat_buttons_set_visible(self, state): - """ - Derived classes MAY implement this - """ - self.hide_chat_buttons = state - def got_connected(self): pass diff --git a/gajim/message_textview.py b/gajim/message_textview.py index bcaa81cdf..bd760587a 100644 --- a/gajim/message_textview.py +++ b/gajim/message_textview.py @@ -24,7 +24,6 @@ import gc from gi.repository import Gtk -from gi.repository import GObject from gi.repository import GLib from gi.repository import Pango @@ -37,6 +36,7 @@ class MessageTextView(Gtk.TextView): chat/groupchat windows """ UNDO_LIMIT = 20 + PLACEHOLDER = _('Write a message..') def __init__(self): Gtk.TextView.__init__(self) @@ -64,6 +64,9 @@ class MessageTextView(Gtk.TextView): self.color_tags = [] self.fonts_tags = [] self.other_tags = {} + self.placeholder_tag = _buffer.create_tag('placeholder') + self.placeholder_tag.set_property('foreground_rgba', + gtkgui_helpers.Color.GREY) self.other_tags['bold'] = _buffer.create_tag('bold') self.other_tags['bold'].set_property('weight', Pango.Weight.BOLD) self.begin_tags['bold'] = '' @@ -82,6 +85,30 @@ class MessageTextView(Gtk.TextView): self.end_tags['strike'] = '' self.connect_after('paste-clipboard', self.after_paste_clipboard) + self.connect('focus-in-event', self._on_focus_in) + self.connect('focus-out-event', self._on_focus_out) + + start, end = _buffer.get_bounds() + _buffer.insert_with_tags( + start, self.PLACEHOLDER, self.placeholder_tag) + + def _on_focus_in(self, *args): + buf = self.get_buffer() + start, end = buf.get_bounds() + text = buf.get_text(start, end, True) + if text == self.PLACEHOLDER: + buf.set_text('') + + def _on_focus_out(self, *args): + buf = self.get_buffer() + start, end = buf.get_bounds() + text = buf.get_text(start, end, True) + if text == '': + buf.insert_with_tags( + start, self.PLACEHOLDER, self.placeholder_tag) + + def remove_placeholder(self): + self._on_focus_in() def after_paste_clipboard(self, textview): buffer_ = textview.get_buffer() diff --git a/gajim/message_window.py b/gajim/message_window.py index d3b648583..b8c84b7f5 100644 --- a/gajim/message_window.py +++ b/gajim/message_window.py @@ -81,7 +81,6 @@ class MessageWindow(object): self.xml = gtkgui_helpers.get_gtk_builder('%s.ui' % self.widget_name) self.window = self.xml.get_object(self.widget_name) self.window.set_application(app.app) - self.window.set_show_menubar(False) self.notebook = self.xml.get_object('notebook') self.parent_paned = None @@ -94,17 +93,26 @@ class MessageWindow(object): if app.config.get('roster_on_the_right'): child1 = self.parent_paned.get_child1() self.parent_paned.remove(child1) - self.parent_paned.add(self.notebook) - self.parent_paned.pack1(self.notebook, resize=False, - shrink=True) - self.parent_paned.pack2(child1, resize=True, shrink=True) + self.parent_paned.pack1(self.notebook, resize=False) + self.parent_paned.pack2(child1) else: - self.parent_paned.add(self.notebook) - self.parent_paned.pack2(self.notebook, resize=True, shrink=True) + self.parent_paned.pack2(self.notebook) self.window.lookup_action('show-roster').set_enabled(True) orig_window.destroy() del orig_window + # Set headermenu + # single-window mode: show the header menu on the roster window + # all other modes: add the headerbar to the new window + # A headerbar has to be set before the window calls show() + if parent_window: + self.header_menu = app.interface.roster.header_menu + self.header_menu.show() + else: + self.header_menu = self.xml.get_object('header_menu') + headerbar = self.xml.get_object('headerbar') + self.window.set_titlebar(headerbar) + # NOTE: we use 'connect_after' here because in # MessageWindowMgr._new_window we register handler that saves window # state when closing it, and it should be called before @@ -162,6 +170,9 @@ class MessageWindow(object): self.notebook.set_show_border(app.config.get('tabs_border')) self.show_icon() + def set_header_menu(self, menu): + self.header_menu.set_menu_model(menu) + def change_account_name(self, old_name, new_name): if old_name in self._controls: self._controls[new_name] = self._controls[old_name] @@ -324,6 +335,7 @@ class MessageWindow(object): self.notebook.show_all() else: self.window.show_all() + # NOTE: we do not call set_control_active(True) since we don't know # whether the tab is the active one. self.show_title() @@ -436,9 +448,6 @@ class MessageWindow(object): elif chr(keyval) in st: # ALT + 1,2,3.. self.notebook.set_current_page(st.index(chr(keyval))) return True - elif keyval == Gdk.KEY_c: # ALT + C toggles chat buttons - control.chat_buttons_set_visible(not control.hide_chat_buttons) - return True elif keyval == Gdk.KEY_m: # ALT + M show emoticons menu control.emoticons_button.get_popover().show() return True @@ -570,6 +579,7 @@ class MessageWindow(object): ask any confirmation """ def close(ctrl): + self.remove_headermenu(self.notebook, ctrl) if reason is not None: # We are leaving gc with a status message ctrl.shutdown(reason) else: # We are leaving gc without status message or it's a chat @@ -607,6 +617,7 @@ class MessageWindow(object): def on_minimize(ctrl): if method != self.CLOSE_COMMAND: + self.remove_headermenu(self.notebook, ctrl) ctrl.minimize() self.check_tabs() return @@ -618,6 +629,13 @@ class MessageWindow(object): else: ctrl.allow_shutdown(method, on_yes, on_no, on_minimize) + def remove_headermenu(self, notebook, ctrl): + page_num = notebook.page_num(ctrl.widget) + if page_num == notebook.get_current_page(): + self.set_header_menu(None) + elif notebook.get_n_pages() == 1: + self.set_header_menu(None) + def check_tabs(self): if self.parent_paned: # Do nothing in single window mode @@ -822,6 +840,8 @@ class MessageWindow(object): def popup_menu(self, event): menu = self.get_active_control().prepare_context_menu() + if menu is None: + return # show the menu menu.attach_to_widget(app.interface.roster.window, None) menu.show_all() @@ -836,6 +856,7 @@ class MessageWindow(object): new_ctrl = self._widget_to_control(notebook.get_nth_page(page_num)) new_ctrl.set_control_active(True) self.show_title(control = new_ctrl) + self.set_header_menu(new_ctrl.control_menu) control = self.get_active_control() if isinstance(control, ChatControlBase): @@ -879,6 +900,7 @@ class MessageWindow(object): if isinstance(control, ChatControlBase): # we forwarded it to message textview + control.msg_textview.remove_placeholder() control.msg_textview.event(event) control.msg_textview.grab_focus() @@ -1289,6 +1311,7 @@ class MessageWindowMgr(GObject.GObject): gtkgui_helpers.resize_window(w.window, app.config.get('roster_width'), app.config.get('roster_height')) + self.hide_header_bar(self.parent_win) self._windows = {} @@ -1298,8 +1321,16 @@ class MessageWindowMgr(GObject.GObject): mw = self.create_window(ctrl.contact, ctrl.account, ctrl.type_id) ctrl.parent_win = mw + ctrl.add_actions() + ctrl.update_actions() mw.new_tab(ctrl) + @staticmethod + def hide_header_bar(parent_win): + header_bar = parent_win.get_titlebar() + for child in header_bar.get_children(): + child.hide() + def save_opened_controls(self): if not app.config.get('remember_opened_chat_controls'): return diff --git a/gajim/roster_window.py b/gajim/roster_window.py index 6e8433d00..0d09827c0 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -5696,9 +5696,18 @@ class RosterWindow: application.add_window(self.window) self.add_actions() self.hpaned = self.xml.get_object('roster_hpaned') + app.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned) app.interface.msg_win_mgr.connect('window-delete', self.on_message_window_delete) + + # Set headermenu but hide it. + # MessageWindow will show it if we are in single-window mode + headerbar = self.xml.get_object('headerbar') + self.window.set_titlebar(headerbar) + self.header_menu = self.xml.get_object('header_menu') + self.header_menu.hide() + self.advanced_menus = [] # We keep them to destroy them if app.config.get('roster_window_skip_taskbar'): self.window.set_property('skip-taskbar-hint', True) diff --git a/gajim/scrolled_window.py b/gajim/scrolled_window.py deleted file mode 100644 index 9b7392275..000000000 --- a/gajim/scrolled_window.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding:utf-8 -*- -# Copyright (C) 2015 Patrick Griffis -# Copyright (C) 2014 Christian Hergert -# -# This program 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, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, 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 this program. If not, see . - -from gi.repository import GObject, Gtk - -class ScrolledWindow(Gtk.ScrolledWindow): - """ - ScrolledWindow that sets a max size for the child to grow into. - Taken from the Gnome Builder project: - https://git.gnome.org/browse/gnome-builder/tree/contrib/egg/egg-scrolled-window.c - """ - __gtype_name__ = "EggScrolledWindow" - - max_content_height = GObject.Property(type=int, default=-1, nick="Max Content Height", - blurb="The maximum height request that can be made") - max_content_width = GObject.Property(type=int, default=-1, nick="Max Content Width", - blurb="The maximum width request that can be made") - min_content_height = GObject.Property(type=int, default=-1, nick="Min Content Height", - blurb="The minimum height request that can be made") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.connect_after("notify::max-content-height", lambda obj, param: self.queue_resize()) - self.connect_after("notify::max-content-width", lambda obj, param: self.queue_resize()) - - def set_min_content_height(self, value): - self.min_content_height = value - - def set_max_content_height(self, value): - self.max_content_height = value - - def set_max_content_width(self, value): - self.max_content_width = value - - def get_max_content_height(self): - return self.max_content_height - - def get_max_content_width(self): - return self.max_content_width - - def do_get_preferred_height(self): - min_height, natural_height = Gtk.ScrolledWindow.do_get_preferred_height(self) - child = self.get_child() - - if natural_height and self.max_content_height > -1 and child: - - style = self.get_style_context() - border = style.get_border(style.get_state()) - additional = border.top + border.bottom - - child_min_height, child_nat_height = child.get_preferred_height() - if child_nat_height > natural_height and self.max_content_height > natural_height: - natural_height = min(self.max_content_height, child_nat_height) + additional - elif natural_height > child_nat_height: - if child_nat_height < self.min_content_height: - return self.min_content_height, self.min_content_height - min_height, natural_height = child_min_height + additional, child_nat_height + additional - - return min_height, natural_height - - def do_get_preferred_width(self): - min_width, natural_width = Gtk.ScrolledWindow.do_get_preferred_width(self) - child = self.get_child() - - if natural_width and self.max_content_width > -1 and child: - - style = self.get_style_context() - border = style.get_border(style.get_state()) - additional = border.left + border.right + 1 - - child_min_width, child_nat_width = child.get_preferred_width() - if child_nat_width > natural_width and self.max_content_width > natural_width: - natural_width = min(self.max_content_width, child_nat_width) + additional - - return min_width, natural_width From 3c103315ec450fc4f15561ef4136b5632f507ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Thu, 19 Oct 2017 11:26:22 +0200 Subject: [PATCH 2/2] Refactor Speller --- gajim/chat_control.py | 12 +------ gajim/chat_control_base.py | 72 +++++++++++++++++++++++++++----------- gajim/config.py | 38 +++++--------------- gajim/dialogs.py | 25 ++++++------- gajim/gtkspell.py | 45 +++++++++++++++++------- gajim/gui_interface.py | 12 ------- gajim/message_textview.py | 11 +++--- 7 files changed, 111 insertions(+), 104 deletions(-) diff --git a/gajim/chat_control.py b/gajim/chat_control.py index d21aaf192..f23d77711 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -58,13 +58,6 @@ from gajim.common.exceptions import GajimGeneralException from gajim.common.const import AvatarSize from gajim.command_system.implementation.hosts import ChatCommands - -try: - from gajim import gtkspell - HAS_GTK_SPELL = True -except (ImportError, ValueError): - HAS_GTK_SPELL = False - from gajim.chat_control_base import ChatControlBase ################################################################################ @@ -1180,10 +1173,7 @@ class ChatControl(ChatControlBase): self.handlers[i].disconnect(i) del self.handlers[i] self.conv_textview.del_handlers() - if app.config.get('use_speller') and HAS_GTK_SPELL: - spell_obj = gtkspell.get_from_text_view(self.msg_textview) - if spell_obj: - spell_obj.detach() + self.remove_speller() self.msg_textview.destroy() # PluginSystem: calling shutdown of super class (ChatControlBase) to let # it remove it's GUI extension points diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py index db05c10d6..09797222e 100644 --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -35,12 +35,14 @@ from gi.repository import Pango from gi.repository import GObject from gi.repository import GLib from gi.repository import Gio + from gajim import gtkgui_helpers from gajim.gtkgui_helpers import Color from gajim import message_control from gajim import dialogs from gajim import history_window from gajim import notify +from gajim import gtkspell import re from gajim import emoticons @@ -64,11 +66,6 @@ from gajim.command_system.implementation.middleware import CommandTools from gajim.command_system.implementation import standard from gajim.command_system.implementation import execute -try: - from gajim import gtkspell - HAS_GTK_SPELL = True -except (ImportError, ValueError): - HAS_GTK_SPELL = False ################################################################################ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): @@ -355,8 +352,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.set_emoticon_popover() # Attach speller - if app.config.get('use_speller') and HAS_GTK_SPELL: - self.set_speller() + self.spell = None + self.spell_handlers = [] + self.set_speller() self.conv_textview.tv.show() # For XEP-0172 @@ -480,24 +478,57 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): image.set_from_pixbuf(icon) def set_speller(self): - # now set the one the user selected + if not gtkspell.HAS_GTK_SPELL or not app.config.get('use_speller'): + return + + def _on_focus_in(*args): + if self.spell is None: + return + self.spell.attach(self.msg_textview) + + def _on_focus_out(*args): + if self.spell is None: + return + if not self.msg_textview.has_text(): + self.spell.detach() + + lang = self.get_speller_language() + if not lang: + return + try: + self.spell = gtkspell.Spell(self.msg_textview, lang) + self.spell.connect('language_changed', self.on_language_changed) + handler_id = self.msg_textview.connect('focus-in-event', + _on_focus_in) + self.spell_handlers.append(handler_id) + handler_id = self.msg_textview.connect('focus-out-event', + _on_focus_out) + self.spell_handlers.append(handler_id) + except OSError: + dialogs.AspellDictError(lang) + app.config.set('use_speller', False) + + def remove_speller(self): + if self.spell is None: + return + self.spell.detach() + for id_ in self.spell_handlers: + self.msg_textview.disconnect(id_) + self.spell_handlers.remove(id_) + self.spell = None + + def get_speller_language(self): per_type = 'contacts' - if self.type_id == message_control.TYPE_GC: + if self.type_id == 'gc': per_type = 'rooms' - lang = app.config.get_per(per_type, self.contact.jid, - 'speller_language') + lang = app.config.get_per( + per_type, self.contact.jid, 'speller_language') if not lang: # use the default one lang = app.config.get('speller_language') if not lang: lang = app.LANG - if lang: - try: - self.spell = gtkspell.Spell(self.msg_textview, lang) - self.msg_textview.lang = lang - self.spell.connect('language_changed', self.on_language_changed) - except (GObject.GError, RuntimeError, TypeError, OSError): - dialogs.AspellDictError(lang) + return lang or None def on_language_changed(self, spell, lang): per_type = 'contacts' @@ -505,9 +536,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): per_type = 'rooms' if not app.config.get_per(per_type, self.contact.jid): app.config.add_per(per_type, self.contact.jid) - app.config.set_per(per_type, self.contact.jid, 'speller_language', - lang) - self.msg_textview.lang = lang + app.config.set_per( + per_type, self.contact.jid, 'speller_language', lang) def on_banner_label_populate_popup(self, label, menu): """ diff --git a/gajim/config.py b/gajim/config.py index 12d1a199e..72b496526 100644 --- a/gajim/config.py +++ b/gajim/config.py @@ -50,12 +50,7 @@ from gajim import message_control from gajim.chat_control_base import ChatControlBase from gajim import dataforms_widget from gajim import gui_menu_builder - -try: - from gajim import gtkspell - HAS_GTK_SPELL = True -except (ImportError, ValueError): - HAS_GTK_SPELL = False +from gajim import gtkspell from gajim.common import helpers from gajim.common import app @@ -192,7 +187,7 @@ class PreferencesWindow: self.xml.get_object('xhtml_checkbutton').set_active(st) # use speller - if HAS_GTK_SPELL: + if gtkspell.HAS_GTK_SPELL: st = app.config.get('use_speller') self.xml.get_object('speller_checkbutton').set_active(st) else: @@ -660,23 +655,13 @@ class PreferencesWindow: def apply_speller(self): for ctrl in self._get_all_controls(): if isinstance(ctrl, ChatControlBase): - try: - spell_obj = gtkspell.get_from_text_view(ctrl.msg_textview) - except (TypeError, RuntimeError, OSError): - spell_obj = None - - if not spell_obj: - ctrl.set_speller() + ctrl.set_speller() def remove_speller(self): for ctrl in self._get_all_controls(): if isinstance(ctrl, ChatControlBase): - try: - spell_obj = gtkspell.get_from_text_view(ctrl.msg_textview) - except (TypeError, RuntimeError): - spell_obj = None - if spell_obj: - spell_obj.detach() + if ctrl.spell is not None: + ctrl.remove_speller() def on_speller_checkbutton_toggled(self, widget): active = widget.get_active() @@ -685,15 +670,10 @@ class PreferencesWindow: lang = app.config.get('speller_language') if not lang: lang = app.LANG - tv = Gtk.TextView() - try: - gtkspell.Spell(tv, lang) - except (TypeError, RuntimeError, OSError): - dialogs.ErrorDialog( - _('Dictionary for lang %s not available') % lang, - _('You have to install %s dictionary to use spellchecking, or ' - 'choose another language by setting the speller_language option.' - ) % lang) + + available = gtkspell.test_language(lang) + if not available: + dialogs.AspellDictError(lang) app.config.set('use_speller', False) widget.set_active(False) else: diff --git a/gajim/dialogs.py b/gajim/dialogs.py index ce48f6571..ebc8451fd 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -42,6 +42,7 @@ from gajim import gtkgui_helpers from gajim import vcard from gajim import conversation_textview from gajim import dataforms_widget +from gajim import gtkspell from random import randrange from gajim.common import pep @@ -50,12 +51,6 @@ from gajim.common import const from gajim.options_dialog import OptionsDialog from gajim.common.const import Option, OptionKind, OptionType -try: - from gajim import gtkspell - HAS_GTK_SPELL = True -except (ImportError, ValueError): - HAS_GTK_SPELL = False - # those imports are not used in this file, but in files that 'import dialogs' # so they can do dialog.GajimThemesWindow() for example from gajim.filetransfers_window import FileTransfersWindow @@ -1486,11 +1481,11 @@ class FileChooserDialog(Gtk.FileChooserDialog): class AspellDictError: def __init__(self, lang): ErrorDialog( - _('Dictionary for lang %s not available') % lang, - _('You have to install %s dictionary to use spellchecking, or ' - 'choose another language by setting the speller_language option.' - '\n\nHighlighting misspelled words feature will not be used') % lang) - app.config.set('use_speller', False) + _('Dictionary for lang "%s" not available') % lang, + _('You have to install the dictionary "%s" to use spellchecking, ' + 'or choose another language by setting the speller_language ' + 'option.\n\n' + 'Highlighting misspelled words feature will not be used') % lang) class ConfirmationDialog(HigDialog): """ @@ -3082,14 +3077,14 @@ class SingleMessageWindow: else: self.to_entry.set_text(to) - if app.config.get('use_speller') and HAS_GTK_SPELL and action == 'send': + if app.config.get('use_speller') and gtkspell.HAS_GTK_SPELL and action == 'send': try: lang = app.config.get('speller_language') if not lang: lang = app.LANG - gtkspell.Spell(self.conversation_textview.tv, lang) - gtkspell.Spell(self.message_textview, lang) - except (GObject.GError, TypeError, RuntimeError, OSError): + self.spell = gtkspell.Spell(self.message_textview, lang) + self.spell.attach(self.message_textview) + except OSError: AspellDictError(lang) self.prepare_widgets_for(self.action) diff --git a/gajim/gtkspell.py b/gajim/gtkspell.py index 24d52e9d8..f1883f54f 100644 --- a/gajim/gtkspell.py +++ b/gajim/gtkspell.py @@ -19,9 +19,16 @@ from gi.repository import GObject from gi.repository import Gtk +from gi.repository import GLib import gi gi.require_version('GtkSpell', '3.0') -from gi.repository import GtkSpell +try: + from gi.repository import GtkSpell + HAS_GTK_SPELL = True +except ImportError: + HAS_GTK_SPELL = False + +from gajim.common import app def ensure_attached(func): @@ -48,12 +55,13 @@ class Spell(GObject.GObject): if spell: raise RuntimeError("Textview has already a Spell obj attached") self.spell = GtkSpell.Checker.new() - if not self.spell: - raise OSError("Unable to create spell object.") - if not self.spell.attach(textview): - raise OSError("Unable to attach spell object.") - if not self.spell.set_language(language): - raise OSError("Unable to set language: '%s'" % language) + + try: + self.spell.set_language(language) + except GLib.GError as error: + if error.domain == 'gtkspell-error-quark': + raise OSError("Unable to set language: '%s'" % language) + self.spell.connect('language-changed', self.on_language_changed) else: @@ -74,12 +82,25 @@ class Spell(GObject.GObject): def recheck_all(self): self.spell.recheck_all() - @ensure_attached def detach(self): - self.spell.detach() - self.spell = None + if self.spell is not None: + self.spell.detach() + + def attach(self, textview): + spell = GtkSpell.Checker.get_from_text_view(textview) + if spell is None: + print('attached') + self.spell.attach(textview) + GObject.type_register(Spell) -def get_from_text_view(textview): - return Spell(textview, create=False) + +def test_language(lang): + spell = GtkSpell.Checker.new() + try: + spell.set_language(lang) + except GLib.GError as error: + if error.domain == 'gtkspell-error-quark': + return False + return True diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index cf9274cf0..6b221494a 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -2858,18 +2858,6 @@ class Interface: # get transports type from DB app.transport_type = app.logger.get_transports_type() - # test is dictionnary is present for speller - if app.config.get('use_speller'): - lang = app.config.get('speller_language') - if not lang: - lang = app.LANG - tv = Gtk.TextView() - try: - from gajim import gtkspell - spell = gtkspell.Spell(tv, lang) - except (ImportError, TypeError, RuntimeError, OSError, ValueError): - dialogs.AspellDictError(lang) - if app.config.get('soundplayer') == '': # only on first time Gajim starts commands = ('paplay', 'aplay', 'play', 'ossplay') diff --git a/gajim/message_textview.py b/gajim/message_textview.py index bd760587a..acd6c4dbf 100644 --- a/gajim/message_textview.py +++ b/gajim/message_textview.py @@ -57,7 +57,7 @@ class MessageTextView(Gtk.TextView): self.undo_list = [] # needed to know if we undid something self.undo_pressed = False - self.lang = None # Lang used for spell checking + _buffer = self.get_buffer() self.begin_tags = {} self.end_tags = {} @@ -92,12 +92,15 @@ class MessageTextView(Gtk.TextView): _buffer.insert_with_tags( start, self.PLACEHOLDER, self.placeholder_tag) - def _on_focus_in(self, *args): + def has_text(self): buf = self.get_buffer() start, end = buf.get_bounds() text = buf.get_text(start, end, True) - if text == self.PLACEHOLDER: - buf.set_text('') + return text != self.PLACEHOLDER + + def _on_focus_in(self, *args): + if not self.has_text(): + self.get_buffer().set_text('') def _on_focus_out(self, *args): buf = self.get_buffer()