diff --git a/gajim/app_actions.py b/gajim/app_actions.py index b25a87007..d32989993 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -80,6 +80,12 @@ class AppActions(): def on_quit(self, action, param): interface.roster.on_quit_request() + def on_new_chat(self, action, param): + if 'start_chat' in app.interface.instances: + app.interface.instances['start_chat'].present() + else: + app.interface.instances['start_chat'] = dialogs.StartChatDialog() + # Accounts Actions def on_profile(self, action, param): @@ -130,9 +136,6 @@ class AppActions(): def on_add_contact(self, action, param): dialogs.AddNewContactWindow(param.get_string()) - def on_new_chat(self, action, param): - dialogs.NewChatDialog(param.get_string()) - def on_single_message(self, action, param): dialogs.SingleMessageWindow(param.get_string(), action='send') diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 06142f5fd..51eca6afa 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -411,6 +411,16 @@ def get_uf_show(show, use_mnemonic = False): uf_show = Q_('?contact has status:Has errors') return uf_show +def get_css_show_color(show): + if show in ('online', 'chat', 'invisible'): + return 'status-online' + elif show in ('offline', 'not in roster', 'requested'): + return None + elif show in ('xa', 'dnd'): + return 'status-dnd' + elif show in ('away'): + return 'status-away' + def get_uf_sub(sub): if sub == 'none': uf_sub = Q_('?Subscription we already have:None') diff --git a/gajim/data/gui/account_context_menu.ui b/gajim/data/gui/account_context_menu.ui index 665f54d80..333804447 100644 --- a/gajim/data/gui/account_context_menu.ui +++ b/gajim/data/gui/account_context_menu.ui @@ -28,15 +28,6 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - _Start Chat... - True - - True diff --git a/gajim/data/gui/application_menu.ui b/gajim/data/gui/application_menu.ui index 2be3e5eed..6611c813a 100644 --- a/gajim/data/gui/application_menu.ui +++ b/gajim/data/gui/application_menu.ui @@ -72,6 +72,11 @@ app.accounts <Primary><Shift>A + + Start Chat + app.start-chat + <Primary>N + Bookmarks app.bookmarks diff --git a/gajim/data/gui/start_chat_dialog.ui b/gajim/data/gui/start_chat_dialog.ui new file mode 100644 index 000000000..9857be9b4 --- /dev/null +++ b/gajim/data/gui/start_chat_dialog.ui @@ -0,0 +1,56 @@ + + + + + + True + False + 18 + vertical + 6 + + + True + True + True + True + edit-find-symbolic + False + False + + + False + True + 0 + + + + + True + True + never + in + + + True + False + + + StartChatListBox + True + False + browse + False + + + + + + + True + True + 1 + + + + diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index a84354402..696442aa4 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -86,6 +86,16 @@ popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; } background-color: @theme_unfocused_bg_color; color: @theme_text_color; } +/* StartChatListBox */ +#StartChatListBox > row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; } +#StartChatListBox > row:last-child { border-bottom: 0px} +#StartChatListBox > row.activatable:active { box-shadow: none; } +#StartChatListBox > row { padding: 10px 20px 10px 10px; } +#StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color } + /* Text style */ .bold16 { font-size: 16px; font-weight: bold; } +.status-away { color: #ff8533;} +.status-dnd { color: #e62e00;} +.status-online { color: #66bf10;} diff --git a/gajim/dialogs.py b/gajim/dialogs.py index 49d1c657b..8b0675ea4 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -38,6 +38,7 @@ from gi.repository import GLib import os import nbxmpp import time +import locale from gajim import gtkgui_helpers from gajim import vcard @@ -61,6 +62,7 @@ from gajim.common import app from gajim.common import helpers from gajim.common import i18n from gajim.common import dataforms +from gajim.common.const import AvatarSize from gajim.common.caps_cache import muc_caps_cache from gajim.common.exceptions import GajimGeneralException from gajim.common.connection_handlers_events import MessageOutgoingEvent @@ -2757,59 +2759,323 @@ class SynchroniseSelectContactsDialog: iter_ = model.iter_next(iter_) self.dialog.destroy() -class NewChatDialog(InputDialog): - def __init__(self, account): - self.account = account +class StartChatDialog(Gtk.ApplicationWindow): + def __init__(self): + # Must be before ApplicationWindow.__init__ + # or we get our own window + active_window = app.app.get_active_window() - if len(app.connections) > 1: - title = _('Start Chat with account %s') % account + Gtk.ApplicationWindow.__init__(self) + self.set_name('StartChatDialog') + self.set_application(app.app) + mode = app.config.get('one_message_window') != 'always_with_roster' + if active_window == app.interface.roster.window and mode: + self.set_position(Gtk.WindowPosition.CENTER) else: - title = _('Start Chat') - prompt_text = _('Fill in the nickname or the JID of the contact you ' - 'would like\nto send a chat message to:') - InputDialog.__init__(self, title, prompt_text, is_modal=False) - self.input_entry.set_placeholder_text(_('Nickname / JID')) + self.set_transient_for(active_window) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_show_menubar(False) + self.set_title(_('Start new Conversation')) + self.set_default_size(-1, 400) - self.completion_dict = {} - liststore = gtkgui_helpers.get_completion_liststore(self.input_entry) - self.completion_dict = helpers.get_contact_dict_for_account(account) - # add all contacts to the model - keys = sorted(self.completion_dict.keys()) - for jid in keys: - contact = self.completion_dict[jid] - img = app.interface.jabber_state_images['16'][contact.show] - liststore.append((img.get_pixbuf(), jid)) + self.builder = gtkgui_helpers.get_gtk_builder( + 'start_chat_dialog.ui') + self.listbox = self.builder.get_object('listbox') + self.search_entry = self.builder.get_object('search_entry') + self.box = self.builder.get_object('box') - self.ok_handler = self.new_chat_response - okbutton = self.xml.get_object('okbutton') - okbutton.connect('clicked', self.on_okbutton_clicked) - cancelbutton = self.xml.get_object('cancelbutton') - cancelbutton.connect('clicked', self.on_cancelbutton_clicked) - self.dialog.set_transient_for(app.interface.roster.window) - self.dialog.show_all() + self.add(self.box) - def new_chat_response(self, jid): - """ - Called when ok button is clicked - """ - if app.connections[self.account].connected <= 1: - #if offline or connecting - ErrorDialog(_('Connection not available'), - _('Please make sure you are connected with "%s".') % self.account) - return + self.new_contact_row_visible = False + self.new_contact_rows = {} + self.new_groupchat_rows = {} + self.accounts = app.connections.keys() + self.add_contacts() + self.add_groupchats() - if jid in self.completion_dict: - jid = self.completion_dict[jid].jid + self.search_entry.connect('search-changed', + self._on_search_changed) + self.search_entry.connect('next-match', + self._select_new_match, 'next') + self.search_entry.connect('previous-match', + self._select_new_match, 'prev') + self.search_entry.connect('stop-search', + lambda *args: self.search_entry.set_text('')) + + self.listbox.set_filter_func(self._filter_func, None) + self.listbox.set_sort_func(self._sort_func, None) + self.listbox.connect('row-activated', self._on_row_activated) + + self.connect('key-press-event', self._on_key_press) + self.connect('destroy', self._destroy) + + self.select_first_row() + self.show_all() + + def add_contacts(self): + show_account = len(self.accounts) > 1 + for account in self.accounts: + self.new_contact_rows[account] = None + for jid in app.contacts.get_jid_list(account): + contact = app.contacts.get_contact_with_highest_priority( + account, jid) + if contact.is_groupchat(): + continue + row = ContactRow(account, contact, jid, + contact.get_shown_name(), show_account) + self.listbox.add(row) + + def add_groupchats(self): + show_account = len(self.accounts) > 1 + for account in self.accounts: + self.new_groupchat_rows[account] = None + bookmarks = app.connections[account].bookmarks + groupchats = {} + for bookmark in bookmarks: + groupchats[bookmark['jid']] = bookmark['name'] + + for jid in app.contacts.get_gc_list(account): + if jid in groupchats: + continue + groupchats[jid] = None + + for jid in groupchats: + name = groupchats[jid] + if name is None: + name = app.get_nick_from_jid(groupchats[jid]) + row = ContactRow(account, None, jid, name, + show_account, True) + self.listbox.add(row) + + def _on_row_activated(self, listbox, row): + row = row.get_child() + self._start_new_chat(row) + + def _on_key_press(self, widget, event): + if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Tab): + self.search_entry.emit('next-match') + return True + elif (event.state == Gdk.ModifierType.SHIFT_MASK and + event.keyval == Gdk.KEY_ISO_Left_Tab): + self.search_entry.emit('previous-match') + return True + elif event.keyval == Gdk.KEY_Up: + self.search_entry.emit('previous-match') + return True + elif event.keyval == Gdk.KEY_Escape: + if self.search_entry.get_text() != '': + self.search_entry.emit('stop-search') + else: + self.destroy() + return True + elif event.keyval == Gdk.KEY_Return: + row = self.listbox.get_selected_row() + if row is not None: + row.emit('activate') + return True else: + self.search_entry.grab_focus_without_selecting() + + def _start_new_chat(self, row): + if row.new: + if not app.account_is_connected(row.account): + ErrorDialog( + _('You are not connected to the server'), + _('You can not start a new conversation' + ' unless you are connected.'), + transient_for=self) + return try: - jid = helpers.parse_jid(jid) + helpers.parse_jid(row.jid) except helpers.InvalidFormat as e: - ErrorDialog(_('Invalid JID'), str(e)) + ErrorDialog(_('Invalid JID'), str(e), transient_for=self) return - except: - ErrorDialog(_('Invalid JID'), _('Unable to parse "%s".') % jid) + + if row.groupchat: + app.interface.join_gc_minimal(row.account, row.jid) + else: + app.interface.new_chat_from_jid(row.account, row.jid) + + self.destroy() + + def _on_search_changed(self, entry): + search_text = entry.get_text() + if '@' in search_text: + self._add_new_jid_row() + self._update_new_jid_rows(search_text) + else: + self._remove_new_jid_row() + self.listbox.invalidate_filter() + + def _add_new_jid_row(self): + if self.new_contact_row_visible: + return + for account in self.new_contact_rows: + show_account = len(self.accounts) > 1 + row = ContactRow(account, None, '', None, show_account) + self.new_contact_rows[account] = row + group_row = ContactRow(account, None, '', None, show_account, True) + self.new_groupchat_rows[account] = group_row + self.listbox.add(row) + self.listbox.add(group_row) + row.get_parent().show_all() + self.new_contact_row_visible = True + + def _remove_new_jid_row(self): + if not self.new_contact_row_visible: + return + for account in self.new_contact_rows: + self.listbox.remove(self.new_contact_rows[account].get_parent()) + self.listbox.remove(self.new_groupchat_rows[account].get_parent()) + self.new_contact_row_visible = False + + def _update_new_jid_rows(self, search_text): + for account in self.new_contact_rows: + self.new_contact_rows[account].update_jid(search_text) + self.new_groupchat_rows[account].update_jid(search_text) + + def _select_new_match(self, entry, direction): + selected_row = self.listbox.get_selected_row() + index = selected_row.get_index() + + if direction == 'next': + index += 1 + else: + index -= 1 + + while True: + new_selected_row = self.listbox.get_row_at_index(index) + if new_selected_row is None: return - app.interface.new_chat_from_jid(self.account, jid) + if new_selected_row.get_child_visible(): + self.listbox.select_row(new_selected_row) + new_selected_row.grab_focus() + return + if direction == 'next': + index += 1 + else: + index -= 1 + + def select_first_row(self): + first_row = self.listbox.get_row_at_y(0) + self.listbox.select_row(first_row) + + def _filter_func(self, row, user_data): + search_text = self.search_entry.get_text().lower() + search_text_list = search_text.split() + row_text = row.get_child().get_search_text().lower() + for text in search_text_list: + if text not in row_text: + GLib.timeout_add(50, self.select_first_row) + return + GLib.timeout_add(50, self.select_first_row) + return True + + @staticmethod + def _sort_func(row1, row2, user_data): + name1 = row1.get_child().get_search_text() + name2 = row2.get_child().get_search_text() + account1 = row1.get_child().account + account2 = row2.get_child().account + is_groupchat1 = row1.get_child().groupchat + is_groupchat2 = row2.get_child().groupchat + new1 = row1.get_child().new + new2 = row2.get_child().new + + result = locale.strcoll(account1.lower(), account2.lower()) + if result != 0: + return result + + if new1 != new2: + return 1 if new1 else -1 + + if is_groupchat1 != is_groupchat2: + return 1 if is_groupchat1 else -1 + + return locale.strcoll(name1.lower(), name2.lower()) + + @staticmethod + def _destroy(*args): + del app.interface.instances['start_chat'] + +class ContactRow(Gtk.Grid): + def __init__(self, account, contact, jid, name, show_account, + groupchat=False): + Gtk.Grid.__init__(self) + self.set_column_spacing(12) + self.set_size_request(260, -1) + self.account = account + self.account_label = app.config.get_per( + 'accounts', account, 'account_label') or account + self.show_account = show_account + self.jid = jid + self.contact = contact + self.name = name + self.groupchat = groupchat + self.new = jid == '' + + if self.groupchat: + if self.new: + muc_image = app.interface.jabber_state_images['32']['muc_inactive'] + else: + muc_image = app.interface.jabber_state_images['32']['muc_active'] + image = Gtk.Image.new_from_pixbuf(muc_image.get_pixbuf()) + else: + avatar = app.contacts.get_avatar(account, jid, AvatarSize.ROSTER) + if avatar is None: + image = Gtk.Image.new_from_icon_name( + 'avatar-default', Gtk.IconSize.DND) + else: + image = Gtk.Image.new_from_pixbuf(avatar) + self.add(image) + + middle_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + middle_box.set_hexpand(True) + + if self.name is None: + if self.groupchat: + self.name = _('New Groupchat') + else: + self.name = _('New Contact') + + self.name_label = Gtk.Label(self.name) + self.name_label.set_halign(Gtk.Align.START) + self.name_label.get_style_context().add_class('bold16') + + status = contact.show if contact else 'offline' + css_class = helpers.get_css_show_color(status) + if css_class is not None: + self.name_label.get_style_context().add_class(css_class) + middle_box.add(self.name_label) + + self.jid_label = Gtk.Label(jid) + self.jid_label.set_halign(Gtk.Align.START) + middle_box.add(self.jid_label) + + self.add(middle_box) + + if show_account: + account_label = Gtk.Label(self.account_label) + account_label.set_halign(Gtk.Align.START) + account_label.set_valign(Gtk.Align.START) + + right_box = Gtk.Box() + right_box.set_vexpand(True) + right_box.add(account_label) + self.add(right_box) + + self.show_all() + + def update_jid(self, jid): + self.jid = jid + self.jid_label.set_text(jid) + + def get_search_text(self): + if self.contact is None and not self.groupchat: + return self.jid + if self.show_account: + return '%s %s %s' % (self.name, self.jid, self.account_label) + return '%s %s' % (self.name, self.jid) class ChangePasswordDialog: def __init__(self, account, on_response, transient_for=None): diff --git a/gajim/gajim.py b/gajim/gajim.py index 21f015749..3e51221f4 100644 --- a/gajim/gajim.py +++ b/gajim/gajim.py @@ -307,7 +307,6 @@ class GajimApplication(Gtk.Application): self.account_actions = [ ('-start-single-chat', action.on_single_message, 'online', 's'), - ('-start-chat', action.on_new_chat, 'online', 's'), ('-join-groupchat', action.on_join_gc, 'online', 's'), ('-add-contact', action.on_add_contact, 'online', 's'), ('-services', action.on_service_disco, 'online', 's'), @@ -348,6 +347,7 @@ class GajimApplication(Gtk.Application): ('accounts', action.on_accounts), ('add-account', action.on_add_account), ('manage-proxies', action.on_manage_proxies), + ('start-chat', action.on_new_chat), ('bookmarks', action.on_manage_bookmarks), ('history-manager', action.on_history_manager), ('preferences', action.on_preferences), diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index bb51dcb5f..b70460fe4 100644 --- a/gajim/gui_menu_builder.py +++ b/gajim/gui_menu_builder.py @@ -722,7 +722,6 @@ def get_account_menu(account): ('-join-groupchat', _('Join Group Chat')), ('-profile', _('Profile')), ('-services', _('Discover Services')), - ('-start-chat', _('Start Chat...')), ('-start-single-chat', _('Send Single Message...')), ('Advanced', [ ('-archive', _('Archiving Preferences')), diff --git a/gajim/remote_control.py b/gajim/remote_control.py index 0e3abcfa8..b1eff7350 100644 --- a/gajim/remote_control.py +++ b/gajim/remote_control.py @@ -35,7 +35,7 @@ import mimetypes from gajim.common import app from gajim.common import helpers from time import time -from gajim.dialogs import AddNewContactWindow, NewChatDialog, JoinGroupchatWindow +from gajim.dialogs import AddNewContactWindow, JoinGroupchatWindow from gajim.common import ged from gajim.common.connection_handlers_events import MessageOutgoingEvent from gajim.common.connection_handlers_events import GcMessageOutgoingEvent @@ -849,7 +849,7 @@ class SignalObject(dbus.service.Object): if not account: # error is shown in gajim-remote check_arguments(..) return DBUS_BOOLEAN(False) - NewChatDialog(account) + app.app.activate_action('start-chat') return DBUS_BOOLEAN(True) @dbus.service.method(INTERFACE, in_signature='ss', out_signature='') diff --git a/gajim/roster_window.py b/gajim/roster_window.py index d11f29305..807066892 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -3677,9 +3677,6 @@ class RosterWindow: app.interface.instances[account]['join_gc'] = \ dialogs.JoinGroupchatWindow(account, None) - def on_new_chat_menuitem_activate(self, widget, account): - dialogs.NewChatDialog(account) - def on_show_transports_action(self, action, param): app.config.set('show_transports_group', param.get_boolean()) action.set_state(param) @@ -4927,7 +4924,6 @@ class RosterWindow: account_context_menu = xml.get_object('account_context_menu') status_menuitem = xml.get_object('status_menuitem') - start_chat_menuitem = xml.get_object('start_chat_menuitem') join_group_chat_menuitem = xml.get_object( 'join_group_chat_menuitem') add_contact_menuitem = xml.get_object('add_contact_menuitem') @@ -5015,9 +5011,6 @@ class RosterWindow: execute_command_menuitem.connect('activate', self.on_execute_command, contact, account) - start_chat_menuitem.connect('activate', - self.on_new_chat_menuitem_activate, account) - gc_sub_menu = Gtk.Menu() # gc is always a submenu join_group_chat_menuitem.set_submenu(gc_sub_menu) self.add_bookmarks_list(gc_sub_menu, account) @@ -5026,7 +5019,7 @@ class RosterWindow: if app.connections[account].connected < 2: for widget in (add_contact_menuitem, service_discovery_menuitem, join_group_chat_menuitem, execute_command_menuitem, - pep_menuitem, start_chat_menuitem): + pep_menuitem): widget.set_sensitive(False) else: xml = gtkgui_helpers.get_gtk_builder('zeroconf_context_menu.ui') diff --git a/gajim/statusicon.py b/gajim/statusicon.py index 74e98473e..a321c1f3f 100644 --- a/gajim/statusicon.py +++ b/gajim/statusicon.py @@ -185,7 +185,7 @@ class StatusIcon: dialogs.SingleMessageWindow(account, action='send') def on_new_chat(self, widget, account): - dialogs.NewChatDialog(account) + app.app.activate_action('start-chat') def make_menu(self, event_button, event_time): """ diff --git a/plugins/dbus_plugin/plugin.py b/plugins/dbus_plugin/plugin.py index 074b9326f..5c86cfb38 100644 --- a/plugins/dbus_plugin/plugin.py +++ b/plugins/dbus_plugin/plugin.py @@ -628,7 +628,7 @@ if dbus_support.supported: if not account: # error is shown in gajim-remote check_arguments(..) return DBUS_BOOLEAN(False) - NewChatDialog(account) + app.app.activate_action('start-chat') return DBUS_BOOLEAN(True) @dbus.service.method(INTERFACE, in_signature='ss', out_signature='') @@ -658,7 +658,7 @@ if dbus_support.supported: from gajim.common import app from gajim.common import helpers from time import time -from gajim.dialogs import AddNewContactWindow, NewChatDialog, JoinGroupchatWindow +from gajim.dialogs import AddNewContactWindow, JoinGroupchatWindow from gajim.plugins import GajimPlugin from gajim.plugins.helpers import log_calls, log