diff --git a/gajim/data/gui/roster_window.ui b/gajim/data/gui/roster_window.ui index 8b83b220273c8b41a6cce434b2a8bdcd662e8f97..b28d21c4ca56105e01034bc315e0c757d38410f4 100644 --- a/gajim/data/gui/roster_window.ui +++ b/gajim/data/gui/roster_window.ui @@ -3,74 +3,6 @@ <interface> <requires lib="gtk+" version="3.22"/> <object class="GtkAccelGroup" id="accelgroup1"/> - <object class="GtkListStore" id="status_liststore"> - <columns> - <!-- column-name statustext --> - <column type="gchararray"/> - <!-- column-name icon-name --> - <column type="gchararray"/> - <!-- column-name show --> - <column type="gchararray"/> - <!-- column-name sensible --> - <column type="gboolean"/> - </columns> - <data> - <row> - <col id="0">online</col> - <col id="1">online</col> - <col id="2">online</col> - <col id="3">True</col> - </row> - <row> - <col id="0">chat</col> - <col id="1">chat</col> - <col id="2">chat</col> - <col id="3">True</col> - </row> - <row> - <col id="0">away</col> - <col id="1">away</col> - <col id="2">away</col> - <col id="3">True</col> - </row> - <row> - <col id="0">xa</col> - <col id="1">xa</col> - <col id="2">xa</col> - <col id="3">True</col> - </row> - <row> - <col id="0">dnd</col> - <col id="1">dnd</col> - <col id="2">dnd</col> - <col id="3">True</col> - </row> - <row> - <col id="0">SEPARATOR</col> - <col id="1">None</col> - <col id="2">None</col> - <col id="3">True</col> - </row> - <row> - <col id="0" translatable="yes">Change Status Message…</col> - <col id="1">gajim-kbd_input</col> - <col id="2">status</col> - <col id="3">False</col> - </row> - <row> - <col id="0">SEPARATOR</col> - <col id="1">None</col> - <col id="2">None</col> - <col id="3">True</col> - </row> - <row> - <col id="0">offline</col> - <col id="1">offline</col> - <col id="2">offline</col> - <col id="3">True</col> - </row> - </data> - </object> <object class="GtkApplicationWindow" id="roster_window"> <property name="name">RosterWindow</property> <property name="width_request">85</property> @@ -151,36 +83,6 @@ <property name="position">1</property> </packing> </child> - <child> - <object class="GtkComboBox" id="status_combobox"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="model">status_liststore</property> - <signal name="changed" handler="on_status_combobox_changed" swapped="no"/> - <child> - <object class="GtkCellRendererPixbuf"/> - <attributes> - <attribute name="sensitive">3</attribute> - <attribute name="icon-name">1</attribute> - </attributes> - </child> - <child> - <object class="GtkCellRendererText"> - <property name="xpad">5</property> - <property name="ellipsize">end</property> - </object> - <attributes> - <attribute name="sensitive">3</attribute> - <attribute name="text">0</attribute> - </attributes> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">2</property> - </packing> - </child> </object> <packing> <property name="resize">False</property> diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index f3d06d3cb210ca59e86b46f81fb2632868fb484b..d5fe4932062ca41edfa93b8bc7afa0bbd987c05c 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -242,6 +242,7 @@ .insensitive-fg-color {color: @insensitive_fg_color;} /* Padding/Margins */ .margin-top6 { margin-top: 6px; } .margin-top12 { margin-top: 12px; } +.margin-3 { margin: 3px; } .margin-12 { margin: 12px; } .margin-18 { margin: 18px; } .padding-18 { margin: 18px; } diff --git a/gajim/gtk/status_selector.py b/gajim/gtk/status_selector.py new file mode 100644 index 0000000000000000000000000000000000000000..368496614bb4bca40879677af5e89453cb588e28 --- /dev/null +++ b/gajim/gtk/status_selector.py @@ -0,0 +1,139 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see <http://www.gnu.org/licenses/>. + +from gi.repository import Gtk +from gi.repository import Pango + +from gajim.common import app +from gajim.common.helpers import get_uf_show +from gajim.common.helpers import get_global_show +from gajim.common.helpers import statuses_unified +from gajim.common.i18n import _ +from gajim.dialogs import ChangeStatusMessageDialog + +from gajim.gtk.util import get_icon_name + + +class StatusSelector(Gtk.MenuButton): + def __init__(self, compact=False): + Gtk.MenuButton.__init__(self) + self.set_direction(Gtk.ArrowType.UP) + self._compact = compact + self._create_popover() + + self._current_show_icon = Gtk.Image() + self._current_show_icon.set_from_icon_name( + get_icon_name('offline'), Gtk.IconSize.MENU) + + box = Gtk.Box(spacing=6) + box.add(self._current_show_icon) + if not self._compact: + self._current_show_label = Gtk.Label(label=get_uf_show('offline')) + self._current_show_label.set_ellipsize(Pango.EllipsizeMode.END) + self._current_show_label.set_halign(Gtk.Align.START) + self._current_show_label.set_xalign(0) + box.add(self._current_show_label) + self.add(box) + + def _create_popover(self): + popover_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + popover_box.get_style_context().add_class('margin-3') + popover_items = [ + 'online', + 'away', + 'xa', + 'dnd', + 'separator', + 'change_status_message', + 'separator', + 'offline', + ] + + for item in popover_items: + if item == 'separator': + popover_box.add(Gtk.Separator()) + continue + + show_icon = Gtk.Image() + show_label = Gtk.Label() + show_label.set_halign(Gtk.Align.START) + + if item == 'change_status_message': + show_icon.set_from_icon_name('document-edit-symbolic', + Gtk.IconSize.MENU) + show_label.set_text_with_mnemonic(_('_Change Status Message')) + else: + show_icon.set_from_icon_name(get_icon_name(item), + Gtk.IconSize.MENU) + show_label.set_text_with_mnemonic( + get_uf_show(item, use_mnemonic=True)) + + show_box = Gtk.Box(spacing=6) + show_box.add(show_icon) + show_box.add(show_label) + + button = Gtk.Button() + button.set_name(item) + button.set_relief(Gtk.ReliefStyle.NONE) + button.add(show_box) + button.connect('clicked', self._on_change_status) + + if item == 'change_status_message': + self._change_status_message = button + + popover_box.add(button) + + popover_box.show_all() + self._status_popover = Gtk.Popover() + self._status_popover.add(popover_box) + self.set_popover(self._status_popover) + + def _on_change_status(self, button): + def _on_response(message, pep_dict): + if message is None: # None if user pressed Cancel + return + for account in app.contacts.get_accounts(): + sync_account = app.config.get_per( + 'accounts', account, 'sync_with_global_status') + if not sync_account: + continue + app.interface.roster.send_status(account, new_show, message) + app.interface.roster.send_pep(account, pep_dict) + + self._status_popover.popdown() + new_show = button.get_name() + if new_show == 'change_status_message': + new_show = get_global_show() + ChangeStatusMessageDialog(_on_response, new_show) + return + + app.interface.roster.get_status_message(new_show, _on_response) + + def update(self): + show = get_global_show() + uf_show = get_uf_show(show) + self._current_show_icon.set_from_icon_name( + get_icon_name(show), Gtk.IconSize.MENU) + if statuses_unified(): + self._current_show_icon.set_tooltip_text(_('Status: %s') % uf_show) + if not self._compact: + self._current_show_label.set_text(uf_show) + else: + show_label = _('%s (desynced)') % uf_show + self._current_show_icon.set_tooltip_text( + _('Status: %s') % show_label) + if not self._compact: + self._current_show_label.set_text(show_label) + + self._change_status_message.set_sensitive(show != 'offline') diff --git a/gajim/roster_window.py b/gajim/roster_window.py index b2e9ca9367b526f5bea348a1bc17dbcd97f76568..da265e18ce6eb7f01d3d5413a03c9b4beb94b7ca 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -76,6 +76,7 @@ from gajim.gtk.discovery import ServiceDiscoveryWindow from gajim.gtk.tooltips import RosterTooltip from gajim.gtk.adhoc import AdHocCommand +from gajim.gtk.status_selector import StatusSelector from gajim.gtk.util import get_icon_name from gajim.gtk.util import resize_window from gajim.gtk.util import restore_roster_position @@ -247,26 +248,6 @@ def _iter_is_separator(model, titer): return True return False - @staticmethod - def _status_cell_data_func(cell_layout, cell, tree_model, iter_): - if isinstance(cell, Gtk.CellRendererPixbuf): - icon_name = tree_model[iter_][1] - if icon_name is None: - return - if tree_model[iter_][2] == 'status': - cell.set_property('icon_name', icon_name) - else: - iconset_name = get_icon_name(icon_name) - cell.set_property('icon_name', iconset_name) - else: - show = tree_model[iter_][0] - id_ = tree_model[iter_][2] - if id_ not in ('status', 'desync'): - show = helpers.get_uf_show(show) - cell.set_property('text', show) - - - ############################################################################# ### Methods for adding and removing roster window items ############################################################################# @@ -2083,6 +2064,7 @@ def send_status_continue(self, account, status, txt): self.delete_pep(app.get_jid_from_account(account), account) app.connections[account].change_status(status, txt) + self._status_selector.update() def chg_contact_status(self, contact, show, status, account): """ @@ -2173,7 +2155,9 @@ def on_status_changed(self, account, show): for contact in [c for c in lcontact if ( (c.show != 'offline' or c.is_transport()) and not ctrl)]: self.chg_contact_status(contact, 'offline', '', account) - self.update_status_combobox() + if app.interface.systray_enabled: + app.interface.systray.change_status(show) + self._status_selector.update() def get_status_message(self, show, on_response, show_pep=True, always_ask=False): @@ -2211,44 +2195,6 @@ def on_response(message, pep_dict): self.send_pep(account, pep_dict) self.get_status_message(status, on_response) - def update_status_combobox(self): - # table to change index in connection.connected to index in combobox - table = { - 'offline':8, - 'connecting':8, - 'error': 8, - 'online':0, - 'chat':1, - 'away':2, - 'xa':3, - 'dnd':4 - } - - liststore = self.status_combobox.get_model() - - # Check if a desync'ed status entry and separator is currently - # in the liststore and remove it. - while len(liststore) > 9: - titer = liststore.get_iter_first() - liststore.remove(titer) - - show = helpers.get_global_show() - # temporarily block signal in order not to send status that we show - # in the combobox - self.combobox_callback_active = False - if helpers.statuses_unified(): - self.status_combobox.set_active(table[show]) - else: - uf_show = helpers.get_uf_show(show) - liststore.prepend(['SEPARATOR', None, '', True]) - status_combobox_text = uf_show + ' (' + _("desynced") + ')' - liststore.prepend( - [status_combobox_text, show, 'desync', False]) - self.status_combobox.set_active(0) - self.combobox_callback_active = True - if app.interface.systray_enabled: - app.interface.systray.change_status(show) - def get_show(self, lcontact): prio = lcontact[0].priority show = lcontact[0].show @@ -2545,17 +2491,10 @@ def _nec_anonymous_auth(self, obj): self.rename_self_contact(obj.old_jid, obj.new_jid, obj.conn.name) def _nec_our_show(self, event): - model = self.status_combobox.get_model() - iter_ = model.get_iter_from_string('6') if event.show == 'offline': - # sensitivity for this menuitem - if app.get_number_of_connected_accounts() == 0: - model[iter_][3] = False self.application.set_account_actions_state(event.account) self.application.update_app_actions_state() - else: - # sensitivity for this menuitem - model[iter_][3] = True + self.on_status_changed(event.account, event.show) def _nec_connection_type(self, obj): @@ -3091,20 +3030,18 @@ def on_roster_treeview_key_press_event(self, widget, event): def accel_group_func(self, accel_group, acceleratable, keyval, modifier): # CTRL mask if modifier & Gdk.ModifierType.CONTROL_MASK: - if keyval == Gdk.KEY_s: # CTRL + s - model = self.status_combobox.get_model() - accounts = list(app.connections.keys()) - status = model[self.previous_status_combobox_active][2] - def on_response(message, pep_dict): - if message is not None: # None if user pressed Cancel - for account in accounts: - if not app.config.get_per('accounts', account, - 'sync_with_global_status'): + if keyval == Gdk.KEY_s: # CTRL + s + show = helpers.get_global_show() + def _on_response(message, pep_dict): + if message is not None: # None if user pressed Cancel + for account in app.contacts.get_accounts(): + sync_account = app.config.get_per( + 'accounts', account, 'sync_with_global_status') + if not sync_account: continue - current_show = app.connections[account].status - self.send_status(account, current_show, message) + self.send_status(account, show, message) self.send_pep(account, pep_dict) - dialogs.ChangeStatusMessageDialog(on_response, status) + dialogs.ChangeStatusMessageDialog(_on_response, show) return True if keyval == Gdk.KEY_k: # CTRL + k self.enable_rfilter('') @@ -3281,78 +3218,6 @@ def on_ok2(): DialogButton.make('Remove', callback=on_ok2)]).show() - def on_status_combobox_changed(self, widget): - """ - When we change our status via the combobox - """ - model = self.status_combobox.get_model() - active = self.status_combobox.get_active() - if active == -1: # no active item - return - if not self.combobox_callback_active: - self.previous_status_combobox_active = active - return - accounts = list(app.connections.keys()) - if not accounts: - ErrorDialog(_('No account available'), - _('You must create an account before you can chat with other ' - 'contacts.')) - self.update_status_combobox() - return - - status = model[active][2] - if status == 'status': - # 'Change status message' selected: - # do not change show, just show change status dialog - status = model[self.previous_status_combobox_active][2] - def on_response(message, pep_dict): - if message is not None: # None if user pressed Cancel - for account in accounts: - if not app.config.get_per('accounts', account, - 'sync_with_global_status'): - continue - current_show = app.connections[account].status - self.send_status(account, current_show, message) - self.send_pep(account, pep_dict) - self.combobox_callback_active = False - self.status_combobox.set_active( - self.previous_status_combobox_active) - self.combobox_callback_active = True - dialogs.ChangeStatusMessageDialog(on_response, status) - return - # we are about to change show, so save this new show so in case - # after user chooses "Change status message" menuitem - # we can return to this show - self.previous_status_combobox_active = active - - def on_continue(message, pep_dict): - if message is None: - # user pressed Cancel to change status message dialog - self.update_status_combobox() - return - global_sync_accounts = [] - for acct in accounts: - if app.config.get_per('accounts', acct, - 'sync_with_global_status'): - global_sync_accounts.append(acct) - global_sync_connected_accounts = \ - app.get_number_of_connected_accounts(global_sync_accounts) - for account in accounts: - if not app.config.get_per('accounts', account, - 'sync_with_global_status'): - continue - # we are connected (so we wanna change show and status) - # or no account is connected and we want to connect with new - # show and status - - if not global_sync_connected_accounts > 0 or \ - app.account_is_available(account): - self.send_status(account, status, message) - self.send_pep(account, pep_dict) - self.update_status_combobox() - - self.get_status_message(status, on_continue) - def on_publish_tune_toggled(self, widget, account): active = widget.get_active() app.connections[account].get_module('UserTune').set_enabled(active) @@ -4321,18 +4186,19 @@ def _on_send_files(account, jid, uris): def update_icons(self): # Update the roster self.setup_and_draw_roster() - # Update the status combobox - self.status_combobox.queue_draw() + # Update the systray if app.interface.systray_enabled: app.interface.systray.set_img() + app.interface.systray.change_status(helpers.get_global_show()) for win in app.interface.msg_win_mgr.windows(): for ctrl in win.controls(): ctrl.update_ui() win.redraw_tab(ctrl) - self.update_status_combobox() + self._status_selector.update() + def set_account_status_icon(self, account): child_iterA = self._get_account_iter(account, self.model) @@ -5294,20 +5160,9 @@ def __init__(self, application): # accounts to draw next time we draw accounts. self.accounts_to_draw = [] - # StatusComboBox - self.status_combobox = self.xml.get_object('status_combobox') - pixbuf_renderer, text_renderer = self.status_combobox.get_cells() - self.status_combobox.set_cell_data_func( - pixbuf_renderer, self._status_cell_data_func) - self.status_combobox.set_cell_data_func( - text_renderer, self._status_cell_data_func) - self.status_combobox.set_row_separator_func(self._iter_is_separator) - - self.status_combobox.set_active(8) - # holds index to previously selected item so if - # "change status message..." is selected we can fallback to previously - # selected item and not stay with that item selected - self.previous_status_combobox_active = 8 + # Status selector + self._status_selector = StatusSelector() + self.xml.roster_vbox2.add(self._status_selector) # Enable/Disable checkboxes at start if app.config.get('showoffline'):