gui_menu_builder.py 36.3 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Copyright (C) 2009-2014 Yann Leboulanger <asterix AT lagaule.org>
#
# 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/>.
16

17
from gi.repository import Gtk, Gio, GLib
Philipp Hörist's avatar
Philipp Hörist committed
18
from nbxmpp.namespaces import Namespace
19 20 21 22

from gajim import gtkgui_helpers
from gajim.common import app
from gajim.common import helpers
Philipp Hörist's avatar
Philipp Hörist committed
23 24
from gajim.common.helpers import is_affiliation_change_allowed
from gajim.common.helpers import is_role_change_allowed
25
from gajim.common.i18n import ngettext
26
from gajim.common.i18n import _
27 28
from gajim.common.const import URIType
from gajim.common.const import URIAction
29

30
from gajim.gtk.util import get_builder
31
from gajim.gtk.const import ControlType
32

33 34

def build_resources_submenu(contacts, account, action, room_jid=None,
35 36 37 38 39
                room_account=None, cap=None):
    """
    Build a submenu with contact's resources. room_jid and room_account are for
    action self.on_invite_to_room
    """
40
    roster = app.interface.roster
41
    sub_menu = Gtk.Menu()
42 43

    for c in contacts:
44
        item = Gtk.MenuItem.new_with_label(
45
            '%s (%s)' % (c.resource, str(c.priority)))
46
        sub_menu.append(item)
47
        if action == roster.on_invite_to_room:  # pylint: disable=comparison-with-callable
48 49 50 51 52 53 54 55 56
            item.connect('activate', action, [(c, account)], room_jid,
                    room_account, c.resource)
        else: # start_chat, execute_command, send_file
            item.connect('activate', action, c, account, c.resource)

        if cap and not c.supports(cap):
            item.set_sensitive(False)

    return sub_menu
57

58
def build_invite_submenu(invite_menuitem, list_, ignore_rooms=None,
59
show_bookmarked=False, force_resource=False):
60 61
    """
    list_ in a list of (contact, account)
62 63
    force_resource means we want to send invitation even if there is only one
        resource
64
    """
65 66
    if ignore_rooms is None:
        ignore_rooms = []
67
    roster = app.interface.roster
68 69 70 71
    # used if we invite only one contact with several resources
    contact_list = []
    if len(list_) == 1:
        contact, account = list_[0]
72
        contact_list = app.contacts.get_contacts(account, contact.jid)
73 74 75 76 77 78
    contacts_transport = -1
    connected_accounts = []
    # -1 is at start, False when not from the same, None when jabber
    for (contact, account) in list_:
        if not account in connected_accounts:
            connected_accounts.append(account)
79
        transport = app.get_transport_name_from_jid(contact.jid)
80 81
        if transport == 'jabber':
            transport = None
82 83 84 85 86
        if contacts_transport == -1:
            contacts_transport = transport
        elif contacts_transport != transport:
            contacts_transport = False

87
    if contacts_transport is False:
88 89 90
        # they are not all from the same transport
        invite_menuitem.set_sensitive(False)
        return
91
    invite_to_submenu = Gtk.Menu()
92 93 94 95
    invite_menuitem.set_submenu(invite_to_submenu)
    rooms = [] # a list of (room_jid, account) tuple
    minimized_controls = []
    for account in connected_accounts:
96
        minimized_controls += \
97 98
            list(app.interface.minimized_controls[account].values())
    for gc_control in app.interface.msg_win_mgr.get_controls(
99
            ControlType.GROUPCHAT) + minimized_controls:
100
        acct = gc_control.account
101 102
        if acct not in connected_accounts:
            continue
103
        room_jid = gc_control.room_jid
104 105
        if room_jid in ignore_rooms:
            continue
106 107
        if room_jid in app.gc_connected[acct] and \
        app.gc_connected[acct][room_jid] and \
Dicson's avatar
Dicson committed
108
        contacts_transport in ['jabber', None]:
109
            rooms.append((room_jid, acct))
110
    if rooms:
Dicson's avatar
Dicson committed
111
        item = Gtk.SeparatorMenuItem.new() # separator
112 113
        invite_to_submenu.append(item)
        for (room_jid, account) in rooms:
114
            menuitem = Gtk.MenuItem.new_with_label(room_jid.split('@')[0])
115 116
            if len(contact_list) > 1: # several resources
                menuitem.set_submenu(build_resources_submenu(
Dicson's avatar
Dicson committed
117 118
                    contact_list, account, roster.on_invite_to_room, room_jid,
                    account))
119 120
            else:
                # use resource if it's self contact
121
                if contact.jid == app.get_jid_from_account(account):
122 123 124 125
                    resource = contact.resource
                else:
                    resource = None
                menuitem.connect('activate', roster.on_invite_to_room, list_,
126
                    room_jid, account, resource)
127
            invite_to_submenu.append(menuitem)
128

129 130 131 132 133
    if not show_bookmarked:
        return
    rooms2 = [] # a list of (room_jid, account) tuple
    r_jids = [] # list of room jids
    for account in connected_accounts:
134
        con = app.connections[account]
Philipp Hörist's avatar
Philipp Hörist committed
135 136
        for bookmark in con.get_module('Bookmarks').bookmarks:
            if bookmark.jid in r_jids:
137
                continue
Philipp Hörist's avatar
Philipp Hörist committed
138 139 140 141
            if bookmark.jid not in app.gc_connected[account] or not \
            app.gc_connected[account][bookmark.jid]:
                rooms2.append((bookmark.jid, account))
                r_jids.append(bookmark.jid)
Dicson's avatar
Dicson committed
142

143 144 145 146 147
    if not rooms2:
        return
    item = Gtk.SeparatorMenuItem.new() # separator
    invite_to_submenu.append(item)
    for (room_jid, account) in rooms2:
Philipp Hörist's avatar
Philipp Hörist committed
148
        menuitem = Gtk.MenuItem.new_with_label(room_jid.getNode())
149 150
        if len(contact_list) > 1: # several resources
            menuitem.set_submenu(build_resources_submenu(
Philipp Hörist's avatar
Philipp Hörist committed
151
                contact_list, account, roster.on_invite_to_room, str(room_jid),
152 153 154
                account))
        else:
            # use resource if it's self contact
155
            if contact.jid == app.get_jid_from_account(account):
156 157 158 159
                resource = contact.resource
            else:
                resource = None
            menuitem.connect('activate', roster.on_invite_to_room, list_,
Philipp Hörist's avatar
Philipp Hörist committed
160
                str(room_jid), account, resource)
161 162
        invite_to_submenu.append(menuitem)

163
def get_contact_menu(contact, account, use_multiple_contacts=True,
164
show_start_chat=True, show_encryption=False, show_buttonbar_items=True,
165
control=None, gc_contact=None, is_anonymous=True):
166 167 168 169 170 171 172 173
    """
    Build contact popup menu for roster and chat window. If control is not set,
    we hide invite_contacts_menuitem
    """
    if not contact:
        return

    jid = contact.jid
174 175
    our_jid = jid == app.get_jid_from_account(account)
    roster = app.interface.roster
176

177
    xml = get_builder('contact_context_menu.ui')
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    contact_context_menu = xml.get_object('contact_context_menu')

    start_chat_menuitem = xml.get_object('start_chat_menuitem')
    execute_command_menuitem = xml.get_object('execute_command_menuitem')
    rename_menuitem = xml.get_object('rename_menuitem')
    edit_groups_menuitem = xml.get_object('edit_groups_menuitem')
    send_file_menuitem = xml.get_object('send_file_menuitem')
    information_menuitem = xml.get_object('information_menuitem')
    history_menuitem = xml.get_object('history_menuitem')
    send_single_message_menuitem = xml.get_object('send_single_message_menuitem')
    invite_menuitem = xml.get_object('invite_menuitem')
    block_menuitem = xml.get_object('block_menuitem')
    unblock_menuitem = xml.get_object('unblock_menuitem')
    ignore_menuitem = xml.get_object('ignore_menuitem')
    unignore_menuitem = xml.get_object('unignore_menuitem')
    # Subscription submenu
    subscription_menuitem = xml.get_object('subscription_menuitem')
    send_auth_menuitem, ask_auth_menuitem, revoke_auth_menuitem = \
            subscription_menuitem.get_submenu().get_children()
    add_to_roster_menuitem = xml.get_object('add_to_roster_menuitem')
    remove_from_roster_menuitem = xml.get_object(
            'remove_from_roster_menuitem')
    manage_contact_menuitem = xml.get_object('manage_contact')
    convert_to_gc_menuitem = xml.get_object('convert_to_groupchat_menuitem')
    last_separator = xml.get_object('last_separator')

    items_to_hide = []

206
    contacts = app.contacts.get_contacts(account, jid)
207
    if len(contacts) > 1 and use_multiple_contacts: # several resources
Philipp Hörist's avatar
Philipp Hörist committed
208 209 210 211
        send_file_menuitem.set_submenu(build_resources_submenu(
            contacts,
            account,
            roster.on_send_file_menuitem_activate,
Philipp Hörist's avatar
Philipp Hörist committed
212
            cap=Namespace.JINGLE_FILE_TRANSFER_5))
213
        execute_command_menuitem.set_submenu(build_resources_submenu(
Philipp Hörist's avatar
Philipp Hörist committed
214
                contacts, account, roster.on_execute_command, cap=Namespace.COMMANDS))
215
    else:
Philipp Hörist's avatar
Philipp Hörist committed
216
        if contact.supports(Namespace.JINGLE_FILE_TRANSFER_5):
217 218 219 220 221 222
            send_file_menuitem.set_sensitive(True)
            send_file_menuitem.connect('activate',
                    roster.on_send_file_menuitem_activate, contact, account)
        else:
            send_file_menuitem.set_sensitive(False)

Philipp Hörist's avatar
Philipp Hörist committed
223
        if contact.supports(Namespace.COMMANDS):
224
            execute_command_menuitem.set_sensitive(True)
225 226 227 228 229 230 231 232
            if gc_contact and gc_contact.jid and not is_anonymous:
                execute_command_menuitem.connect('activate',
                    roster.on_execute_command, gc_contact, account,
                    gc_contact.resource)
            else:
                execute_command_menuitem.connect('activate',
                    roster.on_execute_command, contact, account,
                    contact.resource)
233 234 235
        else:
            execute_command_menuitem.set_sensitive(False)

236 237 238
    start_chat_menuitem.connect(
        'activate', app.interface.on_open_chat_window, contact, account)

239
    rename_menuitem.connect('activate', roster.on_rename, 'contact', jid,
240
        account)
241 242 243 244 245 246

    history_menuitem.set_action_name('app.browse-history')
    dict_ = {'jid': GLib.Variant('s', contact.jid),
             'account': GLib.Variant('s', account)}
    variant = GLib.Variant('a{sv}', dict_)
    history_menuitem.set_action_target_value(variant)
247 248 249

    if control:
        convert_to_gc_menuitem.connect('activate',
250
            control._on_convert_to_gc_menuitem_activate)
251 252 253
    else:
        items_to_hide.append(convert_to_gc_menuitem)

254
    if _('Not in contact list') not in contact.get_shown_groups():
255 256 257 258
        # contact is in normal group
        edit_groups_menuitem.connect('activate', roster.on_edit_groups, [(contact,
                account)])
    else:
259
        # contact is in group 'Not in contact list'
260 261 262 263 264 265 266
        edit_groups_menuitem.set_sensitive(False)

    # Hide items when it's self contact row
    if our_jid:
        items_to_hide += [rename_menuitem, edit_groups_menuitem]

    # Unsensitive many items when account is offline
267
    if app.account_is_disconnected(account):
268 269 270
        for widget in (start_chat_menuitem, rename_menuitem,
        edit_groups_menuitem, send_file_menuitem, convert_to_gc_menuitem,
        information_menuitem):
271 272 273 274 275 276 277 278 279 280 281 282
            widget.set_sensitive(False)

    if not show_start_chat:
        items_to_hide.append(start_chat_menuitem)

    if not show_buttonbar_items:
        items_to_hide += [history_menuitem, send_file_menuitem,
                information_menuitem, convert_to_gc_menuitem, last_separator]

    if not control:
        items_to_hide.append(convert_to_gc_menuitem)

283 284 285 286 287
    # Hide items when it's a pm
    if gc_contact:
        items_to_hide += [rename_menuitem, edit_groups_menuitem,
        subscription_menuitem, remove_from_roster_menuitem]

288 289 290 291 292
    for item in items_to_hide:
        item.set_no_show_all(True)
        item.hide()

    # Zeroconf Account
293
    if app.config.get_per('accounts', account, 'is_zeroconf'):
294
        for item in (send_single_message_menuitem,
295
        invite_menuitem, block_menuitem, unblock_menuitem, ignore_menuitem,
Philipp Hörist's avatar
Philipp Hörist committed
296
        unignore_menuitem, subscription_menuitem,
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
        manage_contact_menuitem, convert_to_gc_menuitem):
            item.set_no_show_all(True)
            item.hide()

        if contact.show in ('offline', 'error'):
            information_menuitem.set_sensitive(False)
            send_file_menuitem.set_sensitive(False)
        else:
            information_menuitem.connect('activate', roster.on_info_zeroconf,
                    contact, account)

        contact_context_menu.connect('selection-done',
                gtkgui_helpers.destroy_widget)
        contact_context_menu.show_all()
        return contact_context_menu

    # normal account

315 316 317 318 319
    if gc_contact:
        if not gc_contact.jid:
            # it's a pm and we don't know real JID
            invite_menuitem.set_sensitive(False)
        else:
320
            bookmarked = False
321
            c_ = app.contacts.get_contact(account, gc_contact.jid,
322
                gc_contact.resource)
Philipp Hörist's avatar
Philipp Hörist committed
323
            if c_ and c_.supports(Namespace.CONFERENCE):
324
                bookmarked = True
325 326
            build_invite_submenu(invite_menuitem, [(gc_contact, account)],
                show_bookmarked=bookmarked)
327
    else:
328 329 330
        force_resource = False
        if control and control.resource:
            force_resource = True
331
        build_invite_submenu(invite_menuitem, [(contact, account)],
Philipp Hörist's avatar
Philipp Hörist committed
332
            show_bookmarked=contact.supports(Namespace.CONFERENCE),
333
            force_resource=force_resource)
334

335
    if app.account_is_disconnected(account):
336 337
        invite_menuitem.set_sensitive(False)

338
    send_single_message_menuitem.connect('activate',
339
        roster.on_send_single_message_menuitem_activate, account, contact)
340 341

    remove_from_roster_menuitem.connect('activate', roster.on_req_usub,
342
        [(contact, account)])
343 344
    information_menuitem.connect('activate', roster.on_info, contact, account)

345
    if _('Not in contact list') not in contact.get_shown_groups():
346 347 348 349 350 351 352 353 354 355 356 357
        # contact is in normal group
        add_to_roster_menuitem.hide()
        add_to_roster_menuitem.set_no_show_all(True)

        if contact.sub in ('from', 'both'):
            send_auth_menuitem.set_sensitive(False)
        else:
            send_auth_menuitem.connect('activate', roster.authorize, jid, account)
        if contact.sub in ('to', 'both'):
            ask_auth_menuitem.set_sensitive(False)
        else:
            ask_auth_menuitem.connect('activate', roster.req_sub, jid,
358
                    _('I would like to add you to my contact list'), account,
359
                    contact.groups, contact.name)
360
        transport = app.get_transport_name_from_jid(jid,
Dicson's avatar
Dicson committed
361 362
            use_config_setting=False)
        if contact.sub in ('to', 'none') or transport not in ['jabber', None]:
363 364 365 366 367
            revoke_auth_menuitem.set_sensitive(False)
        else:
            revoke_auth_menuitem.connect('activate', roster.revoke_auth, jid,
                    account)

368
    elif app.connections[account].roster_supported:
369
        # contact is in group 'Not in contact list'
370 371 372 373 374
        add_to_roster_menuitem.set_no_show_all(False)
        subscription_menuitem.set_sensitive(False)

        add_to_roster_menuitem.connect('activate', roster.on_add_to_roster,
                contact, account)
375 376 377 378
    else:
        add_to_roster_menuitem.hide()
        add_to_roster_menuitem.set_no_show_all(True)
        subscription_menuitem.set_sensitive(False)
379 380 381 382 383 384

    # Hide items when it's self contact row
    if our_jid:
        manage_contact_menuitem.set_sensitive(False)

    # Unsensitive items when account is offline
385
    if app.account_is_disconnected(account):
386 387
        for widget in (send_single_message_menuitem, subscription_menuitem,
        add_to_roster_menuitem, remove_from_roster_menuitem,
388
        execute_command_menuitem):
389 390
            widget.set_sensitive(False)

391 392 393 394

    con = app.connections[account]
    if con and (con.get_module('PrivacyLists').supported or
                con.get_module('Blocking').supported):
Philipp Hörist's avatar
Philipp Hörist committed
395
        transport = app.get_transport_name_from_jid(jid, use_config_setting=False)
396 397 398
        if helpers.jid_is_blocked(account, jid):
            block_menuitem.set_no_show_all(True)
            block_menuitem.hide()
Philipp Hörist's avatar
Philipp Hörist committed
399
            if transport != 'jabber':
400 401 402 403 404 405 406 407 408 409 410
                unblock_menuitem.set_no_show_all(True)
                unblock_menuitem.hide()
                unignore_menuitem.set_no_show_all(False)
                unignore_menuitem.connect('activate', roster.on_unblock, [(contact,
                        account)])
            else:
                unblock_menuitem.connect('activate', roster.on_unblock, [(contact,
                        account)])
        else:
            unblock_menuitem.set_no_show_all(True)
            unblock_menuitem.hide()
Philipp Hörist's avatar
Philipp Hörist committed
411
            if transport != 'jabber':
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
                block_menuitem.set_no_show_all(True)
                block_menuitem.hide()
                ignore_menuitem.set_no_show_all(False)
                ignore_menuitem.connect('activate', roster.on_block, [(contact,
                        account)])
            else:
                block_menuitem.connect('activate', roster.on_block, [(contact,
                        account)])
    else:
        unblock_menuitem.set_no_show_all(True)
        block_menuitem.set_sensitive(False)
        unblock_menuitem.hide()

    contact_context_menu.connect('selection-done', gtkgui_helpers.destroy_widget)
    contact_context_menu.show_all()
    return contact_context_menu
428 429

def get_transport_menu(contact, account):
430
    roster = app.interface.roster
431 432 433 434 435
    jid = contact.jid

    menu = Gtk.Menu()

    # Send single message
436
    item = Gtk.MenuItem.new_with_mnemonic(_('Send Single _Message…'))
437 438 439
    item.connect('activate', roster.on_send_single_message_menuitem_activate,
        account, contact)
    menu.append(item)
440
    if app.account_is_disconnected(account):
441 442 443 444 445 446 447 448 449 450
        item.set_sensitive(False)

    blocked = False
    if helpers.jid_is_blocked(account, jid):
        blocked = True

    item = Gtk.SeparatorMenuItem.new() # separator
    menu.append(item)

    # Execute Command
451
    item = Gtk.MenuItem.new_with_mnemonic(_('E_xecute Command…'))
452 453 454
    menu.append(item)
    item.connect('activate', roster.on_execute_command, contact, account,
        contact.resource)
455
    if app.account_is_disconnected(account):
456 457 458
        item.set_sensitive(False)

    # Manage Transport submenu
459
    item = Gtk.MenuItem.new_with_mnemonic(_('_Manage Transport'))
460 461 462 463 464
    manage_transport_submenu = Gtk.Menu()
    item.set_submenu(manage_transport_submenu)
    menu.append(item)

    # Modify Transport
465
    item = Gtk.MenuItem.new_with_mnemonic(_('_Modify Transport'))
466 467
    manage_transport_submenu.append(item)
    item.connect('activate', roster.on_edit_agent, contact, account)
468
    if app.account_is_disconnected(account):
469 470 471
        item.set_sensitive(False)

    # Rename
472
    item = Gtk.MenuItem.new_with_mnemonic(_('_Rename…'))
473 474
    manage_transport_submenu.append(item)
    item.connect('activate', roster.on_rename, 'agent', jid, account)
475
    if app.account_is_disconnected(account):
476 477 478 479 480 481 482
        item.set_sensitive(False)

    item = Gtk.SeparatorMenuItem.new() # separator
    manage_transport_submenu.append(item)

    # Block
    if blocked:
483
        item = Gtk.MenuItem.new_with_mnemonic(_('_Unblock'))
484 485
        item.connect('activate', roster.on_unblock, [(contact, account)])
    else:
486
        item = Gtk.MenuItem.new_with_mnemonic(_('_Block'))
487 488 489
        item.connect('activate', roster.on_block, [(contact, account)])

    manage_transport_submenu.append(item)
490
    if app.account_is_disconnected(account):
491 492 493
        item.set_sensitive(False)

    # Remove
494
    item = Gtk.MenuItem.new_with_mnemonic(_('Remo_ve'))
495 496
    manage_transport_submenu.append(item)
    item.connect('activate', roster.on_remove_agent, [(contact, account)])
497
    if app.account_is_disconnected(account):
498 499 500 501 502 503
        item.set_sensitive(False)

    item = Gtk.SeparatorMenuItem.new() # separator
    menu.append(item)

    # Information
504
    information_menuitem = Gtk.MenuItem.new_with_mnemonic(_('_Information'))
505 506
    menu.append(information_menuitem)
    information_menuitem.connect('activate', roster.on_info, contact, account)
507
    if app.account_is_disconnected(account):
508 509 510 511 512
        information_menuitem.set_sensitive(False)

    menu.connect('selection-done', gtkgui_helpers.destroy_widget)
    menu.show_all()
    return menu
513

514

515
def get_singlechat_menu(control_id, account, jid):
Philipp Hörist's avatar
Philipp Hörist committed
516
    singlechat_menu = [
517
        (_('Send File'), [
Yann Leboulanger's avatar
Yann Leboulanger committed
518
            ('win.send-file-httpupload-', _('Upload File…')),
Yann Leboulanger's avatar
typo  
Yann Leboulanger committed
519
            ('win.send-file-jingle-', _('Send File Directly…')),
520
            ]),
521
        (_('Send Chatstate'), ['chatstate']),
522 523
        ('win.invite-contacts-', _('Invite Contacts…')),
        ('win.add-to-roster-', _('Add to Contact List…')),
524 525
        ('win.toggle-audio-', _('Voice Chat')),
        ('win.toggle-video-', _('Video Chat')),
Philipp Hörist's avatar
Philipp Hörist committed
526
        ('win.information-', _('Information')),
527
        ('app.browse-history', _('History')),
Philipp Hörist's avatar
Philipp Hörist committed
528 529
        ]

530 531
    def build_chatstate_menu():
        menu = Gio.Menu()
532
        entries = [
533
            (_('Disabled'), 'disabled'),
534 535
            (_('Composing Only'), 'composing_only'),
            (_('All Chat States'), 'all')
536 537
        ]

538
        for entry in entries:
539 540 541 542 543
            label, setting = entry
            action = 'win.send-chatstate-%s::%s' % (control_id, setting)
            menu.append(label, action)
        return menu

Philipp Hörist's avatar
Philipp Hörist committed
544 545 546
    def build_menu(preset):
        menu = Gio.Menu()
        for item in preset:
547 548
            if isinstance(item[1], str):
                action_name, label = item
549 550 551 552 553 554 555 556
                if action_name == 'app.browse-history':
                    menuitem = Gio.MenuItem.new(label, action_name)
                    dict_ = {'account': GLib.Variant('s', account),
                             'jid': GLib.Variant('s', jid)}
                    variant_dict = GLib.Variant('a{sv}', dict_)
                    menuitem.set_action_and_target_value(action_name,
                                                         variant_dict)
                    menu.append_item(menuitem)
557 558
                else:
                    menu.append(label, action_name + control_id)
Philipp Hörist's avatar
Philipp Hörist committed
559
            else:
560
                label, sub_menu = item
561 562 563 564
                if 'chatstate' in sub_menu:
                    submenu = build_chatstate_menu()
                else:
                    submenu = build_menu(sub_menu)
565
                menu.append_submenu(label, submenu)
Philipp Hörist's avatar
Philipp Hörist committed
566 567 568 569 570
        return menu

    return build_menu(singlechat_menu)


571
def get_groupchat_menu(control_id, account, jid):
Philipp Hörist's avatar
Philipp Hörist committed
572
    groupchat_menu = [
Philipp Hörist's avatar
Philipp Hörist committed
573
        ('win.information-', _('Information')),
574
        (_('Manage Group Chat'), [
575 576
            ('win.rename-groupchat-', _('Rename…')),
            ('win.change-subject-', _('Change Subject…')),
577
            ('win.upload-avatar-', _('Upload Avatar…')),
578 579
            ('win.configure-', _('Configure…')),
            ('win.destroy-', _('Destroy…')),
580
        ]),
Philipp Hörist's avatar
Philipp Hörist committed
581
        (_('Chat Settings'), [
582 583 584 585
            ('win.print-join-left-', _('Show Join/Leave')),
            ('win.print-status-', _('Show Status Changes')),
            ('win.notify-on-message-', _('Notify on all Messages')),
            ('win.minimize-on-close-', _('Minimize on Close')),
586
            ('win.minimize-on-autojoin-',
587
             _('Minimize When Joining Automatically')),
588
            (_('Send Chatstate'), ['chatstate']),
Philipp Hörist's avatar
Philipp Hörist committed
589
        ]),
590
        (_('Sync Threshold'), ['sync']),
591
        ('win.change-nickname-', _('Change Nickname…')),
Philipp Hörist's avatar
Philipp Hörist committed
592
        ('win.request-voice-', _('Request Voice')),
593
        ('win.execute-command-', _('Execute Command…')),
594
        ('app.browse-history', _('History')),
595
        ('win.disconnect-', _('Leave')),
596
    ]
Philipp Hörist's avatar
Philipp Hörist committed
597 598 599 600 601 602

    def build_menu(preset):
        menu = Gio.Menu()
        for item in preset:
            if isinstance(item[1], str):
                action_name, label = item
603 604 605 606 607 608 609 610
                if action_name == 'app.browse-history':
                    menuitem = Gio.MenuItem.new(label, action_name)
                    dict_ = {'account': GLib.Variant('s', account),
                             'jid': GLib.Variant('s', jid)}
                    variant_dict = GLib.Variant('a{sv}', dict_)
                    menuitem.set_action_and_target_value(action_name,
                                                         variant_dict)
                    menu.append_item(menuitem)
Philipp Hörist's avatar
Philipp Hörist committed
611 612 613 614 615 616 617

                elif action_name == 'win.execute-command-':
                    action_name = action_name + control_id
                    menuitem = Gio.MenuItem.new(label, action_name)
                    menuitem.set_action_and_target_value(action_name,
                                                         GLib.Variant('s', ''))
                    menu.append_item(menuitem)
Philipp Hörist's avatar
Philipp Hörist committed
618 619 620 621
                else:
                    menu.append(label, action_name + control_id)
            else:
                label, sub_menu = item
622
                if 'sync' in sub_menu:
623 624
                    # Sync threshold menu
                    submenu = build_sync_menu()
625 626
                elif 'chatstate' in sub_menu:
                    submenu = build_chatstate_menu()
627 628 629
                else:
                    # This is a submenu
                    submenu = build_menu(sub_menu)
Philipp Hörist's avatar
Philipp Hörist committed
630 631 632
                menu.append_submenu(label, submenu)
        return menu

633 634 635 636 637 638 639 640 641 642 643 644 645
    def build_sync_menu():
        menu = Gio.Menu()
        days = app.config.get('threshold_options').split(',')
        days = [int(day) for day in days]
        action_name = 'win.choose-sync-%s::' % control_id
        for day in days:
            if day == 0:
                label = _('No threshold')
            else:
                label = ngettext('%i day', '%i days', day, day, day)
            menu.append(label, '%s%s' % (action_name, day))
        return menu

646 647
    def build_chatstate_menu():
        menu = Gio.Menu()
648
        entries = [
649 650 651 652 653
            (_('Disabled'), 'disabled'),
            (_('Composing only'), 'composing_only'),
            (_('All chat states'), 'all')
        ]

654
        for entry in entries:
655 656 657 658 659
            label, setting = entry
            action = 'win.send-chatstate-%s::%s' % (control_id, setting)
            menu.append(label, action)
        return menu

Philipp Hörist's avatar
Philipp Hörist committed
660 661 662
    return build_menu(groupchat_menu)


663 664 665 666 667 668 669 670
def get_account_menu(account):
    '''
    [(action, label/sub_menu)]
        action: string
        label: string
        sub menu: list
    '''
    account_menu = [
Philipp Hörist's avatar
Philipp Hörist committed
671 672 673
        ('-add-contact', _('Add Contact…')),
        ('-profile', _('Profile')),
        ('-start-single-chat', _('Send Single Message…')),
674
        ('-services', _('Discover Services…')),
675
        ('-server-info', _('Server Info')),
Philipp Hörist's avatar
Philipp Hörist committed
676 677
        (_('Advanced'), [
            ('-archive', _('Archiving Preferences')),
Philipp Hörist's avatar
Philipp Hörist committed
678
            ('-blocking', _('Blocking List')),
679
            ('-bookmarks', _('Bookmarks')),
680
            ('-pep-config', _('PEP Configuration')),
681
            ('-sync-history', _('Synchronise History…')),
Philipp Hörist's avatar
Philipp Hörist committed
682 683 684 685 686 687 688 689 690 691
            ('-privacylists', _('Privacy Lists')),
        ]),
        (_('Admin'), [
            ('-send-server-message', _('Send Server Message…')),
            ('-set-motd', _('Set MOTD…')),
            ('-update-motd', _('Update MOTD…')),
            ('-delete-motd', _('Delete MOTD…'))
        ]),
    ]

692 693 694 695 696 697 698
    def build_menu(preset):
        menu = Gio.Menu()
        for item in preset:
            if isinstance(item[1], str):
                action, label = item
                action = 'app.{}{}'.format(account, action)
                menuitem = Gio.MenuItem.new(label, action)
Philipp Hörist's avatar
Philipp Hörist committed
699
                if 'add-contact' in action:
Philipp Hörist's avatar
Philipp Hörist committed
700 701 702
                    variant = GLib.Variant('as', [account, ''])
                else:
                    variant = GLib.Variant('s', account)
703 704 705 706 707 708 709 710 711 712 713 714 715
                menuitem.set_action_and_target_value(action, variant)
                menu.append_item(menuitem)
            else:
                label, sub_menu = item
                # This is a submenu
                submenu = build_menu(sub_menu)
                menu.append_submenu(label, submenu)
        return menu

    return build_menu(account_menu)


def build_accounts_menu():
716
    menubar = app.app.get_menubar()
717
    # Accounts Submenu
718
    menu_position = 1
Philipp Hörist's avatar
Philipp Hörist committed
719 720

    acc_menu = menubar.get_item_link(menu_position, 'submenu')
721
    acc_menu.remove_all()
Philipp Hörist's avatar
Philipp Hörist committed
722

Philipp Hörist's avatar
Philipp Hörist committed
723
    accounts_list = sorted(app.contacts.get_accounts())
724
    if not accounts_list:
Philipp Hörist's avatar
Philipp Hörist committed
725 726 727
        modify_account_item = Gio.MenuItem.new(_('_Add Account…'),
                                               'app.accounts::')
        acc_menu.append_item(modify_account_item)
728
        return
Philipp Hörist's avatar
Philipp Hörist committed
729

730
    if len(accounts_list) > 1:
Philipp Hörist's avatar
Philipp Hörist committed
731 732 733
        modify_account_item = Gio.MenuItem.new(_('_Modify Accounts…'),
                                               'app.accounts::')
        acc_menu.append_item(modify_account_item)
734
        for acc in accounts_list:
735
            label = escape_mnemonic(app.get_account_label(acc))
736 737 738
            if acc != 'Local':
                acc_menu.append_submenu(
                    label, get_account_menu(acc))
739 740
    else:
        acc_menu = get_account_menu(accounts_list[0])
Philipp Hörist's avatar
Philipp Hörist committed
741 742 743
        modify_account_item = Gio.MenuItem.new(_('_Modify Account…'),
                                               'app.accounts::')
        acc_menu.insert_item(0, modify_account_item)
Philipp Hörist's avatar
Philipp Hörist committed
744
        menubar.remove(menu_position)
745
        menubar.insert_submenu(menu_position, _('Accounts'), acc_menu)
746 747


748
def get_encryption_menu(control_id, control_type, zeroconf=False):
Philipp Hörist's avatar
Philipp Hörist committed
749 750
    menu = Gio.Menu()
    menu.append(
751
        'Disabled', 'win.set-encryption-{}::{}'.format(control_id, 'disabled'))
752
    for name, plugin in app.plugin_manager.encryption_plugins.items():
753
        if control_type.is_groupchat:
754 755
            if not hasattr(plugin, 'allow_groupchat'):
                continue
756
        if control_type.is_privatechat:
757
            if not hasattr(plugin, 'allow_privatechat'):
758
                continue
759 760 761
        if zeroconf:
            if not hasattr(plugin, 'allow_zeroconf'):
                continue
762 763
        menu_action = 'win.set-encryption-{}::{}'.format(
            control_id, name)
764 765 766
        menu.append(name, menu_action)
    if menu.get_n_items() == 1:
        return None
Philipp Hörist's avatar
Philipp Hörist committed
767
    return menu
768 769


770 771 772
def get_conv_context_menu(account, uri):
    if uri.type == URIType.XMPP:
        if uri.action == URIAction.JOIN:
Philipp Hörist's avatar
Philipp Hörist committed
773
            context_menu = [
774
                ('copy-text', _('Copy XMPP Address')),
775
                ('groupchat-join', _('Join Groupchat')),
Philipp Hörist's avatar
Philipp Hörist committed
776 777 778
            ]
        else:
            context_menu = [
779
                ('copy-text', _('Copy XMPP Address')),
Philipp Hörist's avatar
Philipp Hörist committed
780
                ('-start-chat', _('Start Chat')),
781
                ('-add-contact', _('Add to Contact List…')),
Philipp Hörist's avatar
Philipp Hörist committed
782 783
            ]

784
    elif uri.type == URIType.WEB:
Philipp Hörist's avatar
Philipp Hörist committed
785
        context_menu = [
786
            ('copy-text', _('Copy Link Location')),
Philipp Hörist's avatar
Philipp Hörist committed
787 788 789
            ('open-link', _('Open Link in Browser')),
        ]

790
    elif uri.type == URIType.MAIL:
Philipp Hörist's avatar
Philipp Hörist committed
791
        context_menu = [
792
            ('copy-text', _('Copy Email Address')),
Philipp Hörist's avatar
Philipp Hörist committed
793
            ('open-mail', _('Open Email Composer')),
Philipp Hörist's avatar
Philipp Hörist committed
794 795
        ]

796 797 798 799 800 801 802
    elif uri.type == URIType.GEO:
        context_menu = [
            ('copy-text', _('Copy Location')),
            ('open-link', _('Show Location')),
        ]

    elif uri.type == URIType.AT:
Philipp Hörist's avatar
Philipp Hörist committed
803
        context_menu = [
804
            ('copy-text', _('Copy XMPP Address/Email')),
Philipp Hörist's avatar
Philipp Hörist committed
805
            ('open-mail', _('Open Email Composer')),
Philipp Hörist's avatar
Philipp Hörist committed
806
            ('-start-chat', _('Start Chat')),
807
            ('groupchat-join', _('Join Groupchat')),
808
            ('-add-contact', _('Add to Contact List…')),
Philipp Hörist's avatar
Philipp Hörist committed
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
        ]
    else:
        return

    menu = Gtk.Menu()
    for item in context_menu:
        action, label = item
        menuitem = Gtk.MenuItem()
        menuitem.set_label(label)

        if action.startswith('-'):
            action = 'app.%s%s' % (account, action)
        else:
            action = 'app.%s' % action
        menuitem.set_action_name(action)

825 826 827 828
        data = uri.data
        if uri.type == URIType.XMPP:
            data = uri.data['jid']

Philipp Hörist's avatar
Philipp Hörist committed
829
        if action in ('app.open-mail', 'app.copy-text'):
830
            value = GLib.Variant.new_string(data)
Philipp Hörist's avatar
Philipp Hörist committed
831
        else:
832
            value = GLib.Variant.new_strv([account, data])
Philipp Hörist's avatar
Philipp Hörist committed
833 834 835 836 837 838
        menuitem.set_action_target_value(value)
        menuitem.show()
        menu.append(menuitem)
    return menu


Philipp Hörist's avatar
Philipp Hörist committed
839 840 841 842 843 844 845 846
def get_groupchat_roster_menu(account, control_id, self_contact, contact):
    menu = Gtk.Menu()

    item = Gtk.MenuItem(label=_('Information'))
    action = 'win.contact-information-%s::%s' % (control_id, contact.name)
    item.set_detailed_action_name(action)
    menu.append(item)

847
    item = Gtk.MenuItem(label=_('Add to Contact List…'))
Philipp Hörist's avatar
Philipp Hörist committed
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
    action = 'app.{account}-add-contact(["{account}", "{jid}"])'.format(
        account=account, jid=contact.jid or '')
    if contact.jid is None:
        item.set_sensitive(False)
    else:
        item.set_detailed_action_name(action)
    menu.append(item)

    item = Gtk.MenuItem(label=_('Invite'))
    if contact.jid is not None:
        build_invite_submenu(item,
                             ((contact, account),),
                             show_bookmarked=True)
    else:
        item.set_sensitive(False)
    menu.append(item)

865
    item = Gtk.MenuItem(label=_('Execute Command…'))
Philipp Hörist's avatar
Philipp Hörist committed
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
    action = 'win.execute-command-%s::%s' % (control_id, contact.name)
    item.set_detailed_action_name(action)
    menu.append(item)

    menu.append(Gtk.SeparatorMenuItem())

    if helpers.jid_is_blocked(account, contact.get_full_jid()):
        action = 'win.unblock-%s::%s' % (control_id, contact.name)
        label = _('Unblock')
    else:
        action = 'win.block-%s::%s' % (control_id, contact.name)
        label = _('Block')

    item = Gtk.MenuItem(label=label)
    item.set_detailed_action_name(action)
    menu.append(item)

    item = Gtk.MenuItem(label=_('Kick'))
    action = 'win.kick-%s::%s' % (control_id, contact.name)
    if is_role_change_allowed(self_contact, contact):
        item.set_detailed_action_name(action)
    else:
        item.set_sensitive(False)
    menu.append(item)

    item = Gtk.MenuItem(label=_('Ban'))
    action = 'win.ban-%s::%s' % (control_id, contact.jid or '')
893
    if is_affiliation_change_allowed(self_contact, contact, 'outcast'):
Philipp Hörist's avatar
Philipp Hörist committed
894 895 896 897 898 899 900 901 902 903
        item.set_detailed_action_name(action)
    else:
        item.set_sensitive(False)
    menu.append(item)

    menu.append(Gtk.SeparatorMenuItem())

    item = Gtk.MenuItem(label=_('Make Owner'))
    action = 'win.change-affiliation-%s(["%s", "owner"])' % (control_id,
                                                             contact.jid)
904
    if is_affiliation_change_allowed(self_contact, contact, 'owner'):
Philipp Hörist's avatar
Philipp Hörist committed
905 906 907 908 909 910 911 912
        item.set_detailed_action_name(action)
    else:
        item.set_sensitive(False)
    menu.append(item)

    item = Gtk.MenuItem(label=_('Make Admin'))
    action = 'win.change-affiliation-%s(["%s", "admin"])' % (control_id,
                                                             contact.jid)
913
    if is_affiliation_change_allowed(self_contact, contact, 'admin'):
Philipp Hörist's avatar
Philipp Hörist committed
914 915 916 917 918
        item.set_detailed_action_name(action)
    else:
        item.set_sensitive(False)
    menu.append(item)