Daniel Brötzmann pushed to branch mainwindow at gajim / gajim
Commits:
-
ec1afbee
by wurstsalat at 2021-03-02T20:47:43+01:00
-
0fb26922
by wurstsalat at 2021-03-02T20:56:00+01:00
5 changed files:
- + gajim/data/gui/roster.ui
- gajim/gtk/account_page.py
- gajim/gtk/main.py
- + gajim/gtk/roster.py
- gajim/gui_menu_builder.py
Changes:
| 1 |
+<?xml version="1.0" encoding="UTF-8"?>
|
|
| 2 |
+<!-- Generated with glade 3.38.2 -->
|
|
| 3 |
+<interface>
|
|
| 4 |
+ <requires lib="gtk+" version="3.20"/>
|
|
| 5 |
+ <object class="GtkTreeStore" id="contact_store">
|
|
| 6 |
+ <columns>
|
|
| 7 |
+ <!-- column-name Avatar -->
|
|
| 8 |
+ <column type="CairoGObjectSurface"/>
|
|
| 9 |
+ <!-- column-name Text -->
|
|
| 10 |
+ <column type="gchararray"/>
|
|
| 11 |
+ <!-- column-name Event -->
|
|
| 12 |
+ <column type="gboolean"/>
|
|
| 13 |
+ <!-- column-name IsContact -->
|
|
| 14 |
+ <column type="gboolean"/>
|
|
| 15 |
+ <!-- column-name GroupOrNickname -->
|
|
| 16 |
+ <column type="gchararray"/>
|
|
| 17 |
+ </columns>
|
|
| 18 |
+ </object>
|
|
| 19 |
+ <object class="GtkTreeView" id="roster_treeview">
|
|
| 20 |
+ <property name="visible">True</property>
|
|
| 21 |
+ <property name="can-focus">True</property>
|
|
| 22 |
+ <property name="model">contact_store</property>
|
|
| 23 |
+ <property name="headers-visible">False</property>
|
|
| 24 |
+ <property name="expander-column">expander</property>
|
|
| 25 |
+ <property name="search-column">1</property>
|
|
| 26 |
+ <signal name="button-press-event" handler="_on_roster_button_press_event" swapped="no"/>
|
|
| 27 |
+ <signal name="focus-out-event" handler="_on_focus_out" swapped="no"/>
|
|
| 28 |
+ <signal name="row-activated" handler="_on_roster_row_activated" swapped="no"/>
|
|
| 29 |
+ <child internal-child="selection">
|
|
| 30 |
+ <object class="GtkTreeSelection"/>
|
|
| 31 |
+ </child>
|
|
| 32 |
+ <child>
|
|
| 33 |
+ <object class="GtkTreeViewColumn" id="contact_column">
|
|
| 34 |
+ <property name="sizing">fixed</property>
|
|
| 35 |
+ <property name="fixed-width">210</property>
|
|
| 36 |
+ <property name="title">avatar</property>
|
|
| 37 |
+ <property name="expand">True</property>
|
|
| 38 |
+ <child>
|
|
| 39 |
+ <object class="GtkCellRendererPixbuf" id="avatar_renderer">
|
|
| 40 |
+ <property name="width">40</property>
|
|
| 41 |
+ <property name="ypad">3</property>
|
|
| 42 |
+ <property name="xalign">0</property>
|
|
| 43 |
+ </object>
|
|
| 44 |
+ <attributes>
|
|
| 45 |
+ <attribute name="visible">3</attribute>
|
|
| 46 |
+ <attribute name="surface">0</attribute>
|
|
| 47 |
+ </attributes>
|
|
| 48 |
+ </child>
|
|
| 49 |
+ <child>
|
|
| 50 |
+ <object class="GtkCellRendererText" id="text_renderer">
|
|
| 51 |
+ <property name="ellipsize">end</property>
|
|
| 52 |
+ </object>
|
|
| 53 |
+ <attributes>
|
|
| 54 |
+ <attribute name="markup">1</attribute>
|
|
| 55 |
+ </attributes>
|
|
| 56 |
+ </child>
|
|
| 57 |
+ </object>
|
|
| 58 |
+ </child>
|
|
| 59 |
+ <child>
|
|
| 60 |
+ <object class="GtkTreeViewColumn" id="event_column">
|
|
| 61 |
+ <property name="sizing">fixed</property>
|
|
| 62 |
+ <property name="fixed-width">35</property>
|
|
| 63 |
+ <property name="title">event</property>
|
|
| 64 |
+ <child>
|
|
| 65 |
+ <object class="GtkCellRendererPixbuf" id="icon">
|
|
| 66 |
+ <property name="xalign">0</property>
|
|
| 67 |
+ <property name="icon-name">gajim-event</property>
|
|
| 68 |
+ </object>
|
|
| 69 |
+ <attributes>
|
|
| 70 |
+ <attribute name="visible">2</attribute>
|
|
| 71 |
+ </attributes>
|
|
| 72 |
+ </child>
|
|
| 73 |
+ </object>
|
|
| 74 |
+ </child>
|
|
| 75 |
+ <child>
|
|
| 76 |
+ <object class="GtkTreeViewColumn" id="expander">
|
|
| 77 |
+ <property name="visible">False</property>
|
|
| 78 |
+ <property name="sizing">fixed</property>
|
|
| 79 |
+ <property name="title">expander</property>
|
|
| 80 |
+ </object>
|
|
| 81 |
+ </child>
|
|
| 82 |
+ </object>
|
|
| 83 |
+</interface>
|
| ... | ... | @@ -17,6 +17,7 @@ |
| 17 | 17 |
from gajim.common import app
|
| 18 | 18 |
from gajim.common.i18n import _
|
| 19 | 19 |
|
| 20 |
+from .roster import Roster
|
|
| 20 | 21 |
from .status_selector import StatusSelector
|
| 21 | 22 |
from .util import open_window
|
| 22 | 23 |
|
| ... | ... | @@ -46,7 +47,12 @@ def __init__(self, account): |
| 46 | 47 |
box.add(settings_button)
|
| 47 | 48 |
box.add(self._status_selector)
|
| 48 | 49 |
|
| 49 |
- self.add(box)
|
|
| 50 |
+ self._roster = Roster(account)
|
|
| 51 |
+ hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
|
| 52 |
+ hbox.add(self._roster)
|
|
| 53 |
+ hbox.add(box)
|
|
| 54 |
+ |
|
| 55 |
+ self.add(hbox)
|
|
| 50 | 56 |
self.show_all()
|
| 51 | 57 |
self.update()
|
| 52 | 58 |
|
| ... | ... | @@ -59,3 +65,6 @@ def update(self): |
| 59 | 65 |
self._account, 'account_label')
|
| 60 | 66 |
self._account_label.set_text(account_label)
|
| 61 | 67 |
self._status_selector.update()
|
| 68 |
+ |
|
| 69 |
+ def process_event(self, event):
|
|
| 70 |
+ self._roster.process_event(event)
|
| ... | ... | @@ -54,20 +54,22 @@ def __init__(self): |
| 54 | 54 |
|
| 55 | 55 |
self.add(self._ui.main_grid)
|
| 56 | 56 |
|
| 57 |
+ surface = load_icon('org.gajim.Gajim', self, 40)
|
|
| 58 |
+ self._ui.app_image.set_from_surface(surface)
|
|
| 59 |
+ |
|
| 57 | 60 |
self._chat_stack = ChatStack()
|
| 61 |
+ self._ui.right_grid.add(self._chat_stack)
|
|
| 62 |
+ |
|
| 58 | 63 |
self._chat_list_stack = ChatListStack(self, self._ui, self._chat_stack)
|
| 59 | 64 |
self._ui.chat_list_scrolled.add(self._chat_list_stack)
|
| 60 | 65 |
|
| 61 |
- self._account_side_bar = AccountSideBar()
|
|
| 62 | 66 |
self._workspace_side_bar = WorkspaceSideBar(self._chat_list_stack)
|
| 63 |
- |
|
| 64 |
- surface = load_icon('org.gajim.Gajim', self, 40)
|
|
| 65 |
- self._ui.app_image.set_from_surface(surface)
|
|
| 66 | 67 |
self._ui.workspace_scrolled.add(self._workspace_side_bar)
|
| 67 | 68 |
|
| 69 |
+ self._account_side_bar = AccountSideBar()
|
|
| 68 | 70 |
self._ui.account_box.add(self._account_side_bar)
|
| 69 | 71 |
|
| 70 |
- self._ui.right_grid.add(self._chat_stack)
|
|
| 72 |
+ self._account_pages = {}
|
|
| 71 | 73 |
|
| 72 | 74 |
workspace_menu = Gio.Menu()
|
| 73 | 75 |
for action, label in WORKSPACE_MENU_DICT.items():
|
| ... | ... | @@ -419,6 +421,8 @@ def move_chat_to_workspace(self, new_workspace_id, account, jid): |
| 419 | 421 |
current_chatlist.remove_chat(account, jid)
|
| 420 | 422 |
new_chatlist = self._chat_list_stack.get_chatlist(new_workspace_id)
|
| 421 | 423 |
new_chatlist.add_chat(account, jid, type_)
|
| 424 |
+ self._chat_list_stack.store_open_chats(current_chatlist.workspace_id)
|
|
| 425 |
+ self._chat_list_stack.store_open_chats(new_workspace_id)
|
|
| 422 | 426 |
|
| 423 | 427 |
def _remove_chat(self, _action, param):
|
| 424 | 428 |
workspace_id, account, jid = param.unpack()
|
| ... | ... | @@ -445,8 +449,9 @@ def get_active_control(self): |
| 445 | 449 |
|
| 446 | 450 |
def _add_accounts(self):
|
| 447 | 451 |
for account in list(app.connections.keys()):
|
| 448 |
- self._ui.main_stack.add_named(
|
|
| 449 |
- AccountPage(account), account)
|
|
| 452 |
+ account_page = AccountPage(account)
|
|
| 453 |
+ self._account_pages[account] = account_page
|
|
| 454 |
+ self._ui.main_stack.add_named(account_page, account)
|
|
| 450 | 455 |
|
| 451 | 456 |
def get_controls(self, account=None):
|
| 452 | 457 |
return self._chat_stack.get_controls(account)
|
| ... | ... | @@ -484,6 +489,9 @@ def _on_event(self, event): |
| 484 | 489 |
if event.name == 'update-roster-avatar':
|
| 485 | 490 |
return
|
| 486 | 491 |
|
| 492 |
+ for page in self._account_pages.values():
|
|
| 493 |
+ page.process_event(event)
|
|
| 494 |
+ |
|
| 487 | 495 |
if not self.chat_exists(event.account, event.jid):
|
| 488 | 496 |
if event.name == 'message-received':
|
| 489 | 497 |
if event.properties.is_muc_pm:
|
| 1 |
+import logging
|
|
| 2 |
+from typing import Optional
|
|
| 3 |
+from enum import IntEnum
|
|
| 4 |
+ |
|
| 5 |
+from gi.repository import GLib
|
|
| 6 |
+from gi.repository import Gtk
|
|
| 7 |
+from gi.repository import GObject
|
|
| 8 |
+ |
|
| 9 |
+from gajim.common import app
|
|
| 10 |
+from gajim.common.const import AvatarSize
|
|
| 11 |
+from gajim.common.i18n import _
|
|
| 12 |
+ |
|
| 13 |
+from .util import get_builder
|
|
| 14 |
+ |
|
| 15 |
+log = logging.getLogger('gajim.gui.roster')
|
|
| 16 |
+ |
|
| 17 |
+ |
|
| 18 |
+HANDLED_EVENTS = [
|
|
| 19 |
+ 'presence-received',
|
|
| 20 |
+]
|
|
| 21 |
+ |
|
| 22 |
+ |
|
| 23 |
+class Column(IntEnum):
|
|
| 24 |
+ AVATAR = 0
|
|
| 25 |
+ TEXT = 1
|
|
| 26 |
+ EVENT = 2
|
|
| 27 |
+ IS_CONTACT = 3
|
|
| 28 |
+ NICK_OR_GROUP = 4
|
|
| 29 |
+ |
|
| 30 |
+ |
|
| 31 |
+class Roster(Gtk.ScrolledWindow):
|
|
| 32 |
+ |
|
| 33 |
+ __gsignals__ = {
|
|
| 34 |
+ 'row-activated': (
|
|
| 35 |
+ GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
|
|
| 36 |
+ None, # return value
|
|
| 37 |
+ (str, )) # arguments
|
|
| 38 |
+ }
|
|
| 39 |
+ |
|
| 40 |
+ def __init__(self, account):
|
|
| 41 |
+ Gtk.ScrolledWindow.__init__(self)
|
|
| 42 |
+ self.set_size_request(300, -1)
|
|
| 43 |
+ |
|
| 44 |
+ self._account = account
|
|
| 45 |
+ |
|
| 46 |
+ self._handler_ids = {}
|
|
| 47 |
+ |
|
| 48 |
+ self._ui = get_builder('roster.ui')
|
|
| 49 |
+ self._ui.roster_treeview.set_model(None)
|
|
| 50 |
+ self.add(self._ui.roster_treeview)
|
|
| 51 |
+ |
|
| 52 |
+ # Holds the Gtk.TreeRowReference for each contact
|
|
| 53 |
+ self._contact_refs = {}
|
|
| 54 |
+ # Holds the Gtk.TreeRowReference for each group
|
|
| 55 |
+ self._group_refs = {}
|
|
| 56 |
+ |
|
| 57 |
+ self._store = self._ui.contact_store
|
|
| 58 |
+ |
|
| 59 |
+ self._roster = self._ui.roster_treeview
|
|
| 60 |
+ |
|
| 61 |
+ self.connect('destroy', self._on_destroy)
|
|
| 62 |
+ self._ui.connect_signals(self)
|
|
| 63 |
+ |
|
| 64 |
+ self._initial_draw()
|
|
| 65 |
+ |
|
| 66 |
+ def _on_roster_row_activated(self, _treeview, path, _column):
|
|
| 67 |
+ iter_ = self._store.get_iter(path)
|
|
| 68 |
+ if self._store.iter_parent(iter_) is None:
|
|
| 69 |
+ # This is a group row
|
|
| 70 |
+ return
|
|
| 71 |
+ |
|
| 72 |
+ nick = self._store[iter_][Column.NICK_OR_GROUP]
|
|
| 73 |
+ self.emit('row-activated', nick)
|
|
| 74 |
+ |
|
| 75 |
+ def _on_roster_button_press_event(self, treeview, event):
|
|
| 76 |
+ if event.button not in (2, 3):
|
|
| 77 |
+ return
|
|
| 78 |
+ |
|
| 79 |
+ pos = treeview.get_path_at_pos(int(event.x), int(event.y))
|
|
| 80 |
+ if pos is None:
|
|
| 81 |
+ return
|
|
| 82 |
+ |
|
| 83 |
+ path, _, _, _ = pos
|
|
| 84 |
+ iter_ = self._store.get_iter(path)
|
|
| 85 |
+ if self._store.iter_parent(iter_) is None:
|
|
| 86 |
+ # Group row
|
|
| 87 |
+ return
|
|
| 88 |
+ |
|
| 89 |
+ nick = self._store[iter_][Column.NICK_OR_GROUP]
|
|
| 90 |
+ |
|
| 91 |
+ if event.button == 3: # right click
|
|
| 92 |
+ self._show_contact_menu(nick)
|
|
| 93 |
+ |
|
| 94 |
+ if event.button == 2: # middle click
|
|
| 95 |
+ self.emit('row-activated', nick)
|
|
| 96 |
+ |
|
| 97 |
+ @staticmethod
|
|
| 98 |
+ def _on_focus_out(treeview, _param):
|
|
| 99 |
+ treeview.get_selection().unselect_all()
|
|
| 100 |
+ |
|
| 101 |
+ def _show_contact_menu(self, nick):
|
|
| 102 |
+ pass
|
|
| 103 |
+ |
|
| 104 |
+ def _on_destroy(self, _roster):
|
|
| 105 |
+ for id_ in list(self._handler_ids.keys()):
|
|
| 106 |
+ if self._handler_ids[id_].handler_is_connected(id_):
|
|
| 107 |
+ self._handler_ids[id_].disconnect(id_)
|
|
| 108 |
+ del self._handler_ids[id_]
|
|
| 109 |
+ |
|
| 110 |
+ self._contact_refs = {}
|
|
| 111 |
+ self._group_refs = {}
|
|
| 112 |
+ self._roster.set_model(None)
|
|
| 113 |
+ self._roster = None
|
|
| 114 |
+ self._store.clear()
|
|
| 115 |
+ self._store = None
|
|
| 116 |
+ # self._tooltip.destroy()
|
|
| 117 |
+ # self._tooltip = None
|
|
| 118 |
+ |
|
| 119 |
+ def set_model(self):
|
|
| 120 |
+ self._roster.set_model(self._store)
|
|
| 121 |
+ |
|
| 122 |
+ def _initial_draw(self):
|
|
| 123 |
+ client = app.get_client(self._account)
|
|
| 124 |
+ for contact in client.get_module('Roster').iter_contacts():
|
|
| 125 |
+ contact.connect('presence-update', self._on_presence_update)
|
|
| 126 |
+ contact.connect('avatar-update', self._on_avatar_update)
|
|
| 127 |
+ self._add_contact(contact)
|
|
| 128 |
+ |
|
| 129 |
+ self.set_model()
|
|
| 130 |
+ self._roster.expand_all()
|
|
| 131 |
+ |
|
| 132 |
+ def _on_presence_update(self, contact, _signal_name):
|
|
| 133 |
+ self.draw_avatar(contact)
|
|
| 134 |
+ |
|
| 135 |
+ def _on_avatar_update(self, contact, _signal_name):
|
|
| 136 |
+ self.draw_avatar(contact)
|
|
| 137 |
+ |
|
| 138 |
+ def _add_contact(self, contact):
|
|
| 139 |
+ main_group = _('Contacts')
|
|
| 140 |
+ |
|
| 141 |
+ if contact.groups:
|
|
| 142 |
+ for group_name in contact.groups:
|
|
| 143 |
+ group_iter = self._get_group_iter(group_name)
|
|
| 144 |
+ if not group_iter:
|
|
| 145 |
+ group_iter = self._store.append(
|
|
| 146 |
+ None, (None, group_name, None, False, group_name))
|
|
| 147 |
+ group_path = self._store.get_path(group_iter)
|
|
| 148 |
+ group_ref = Gtk.TreeRowReference(self._store, group_path)
|
|
| 149 |
+ self._group_refs[group_name] = group_ref
|
|
| 150 |
+ else:
|
|
| 151 |
+ group_iter = self._get_group_iter(main_group)
|
|
| 152 |
+ if not group_iter:
|
|
| 153 |
+ group_iter = self._store.append(
|
|
| 154 |
+ None, (None, main_group, None, False, main_group))
|
|
| 155 |
+ group_path = self._store.get_path(group_iter)
|
|
| 156 |
+ group_ref = Gtk.TreeRowReference(self._store, group_path)
|
|
| 157 |
+ self._group_refs[main_group] = group_ref
|
|
| 158 |
+ |
|
| 159 |
+ # Avatar
|
|
| 160 |
+ surface = contact.get_avatar(AvatarSize.CHAT, self.get_scale_factor())
|
|
| 161 |
+ |
|
| 162 |
+ iter_ = self._store.append(
|
|
| 163 |
+ group_iter, (surface, contact.name, None, True, contact.name))
|
|
| 164 |
+ self._contact_refs[contact.jid] = Gtk.TreeRowReference(
|
|
| 165 |
+ self._store, self._store.get_path(iter_))
|
|
| 166 |
+ |
|
| 167 |
+ self.draw_groups()
|
|
| 168 |
+ self.draw_contact(contact.jid)
|
|
| 169 |
+ |
|
| 170 |
+ if (group_path is not None and
|
|
| 171 |
+ self._roster.get_model() is not None):
|
|
| 172 |
+ self._roster.expand_row(group_path, False)
|
|
| 173 |
+ |
|
| 174 |
+ def draw_groups(self):
|
|
| 175 |
+ for group in self._group_refs:
|
|
| 176 |
+ self.draw_group(group)
|
|
| 177 |
+ |
|
| 178 |
+ def draw_group(self, group):
|
|
| 179 |
+ group_iter = self._get_group_iter(group)
|
|
| 180 |
+ if not group_iter:
|
|
| 181 |
+ return
|
|
| 182 |
+ |
|
| 183 |
+ self._store[group_iter][Column.TEXT] = group
|
|
| 184 |
+ |
|
| 185 |
+ def draw_contacts(self):
|
|
| 186 |
+ for jid in self._contact_refs:
|
|
| 187 |
+ self.draw_contact(jid)
|
|
| 188 |
+ |
|
| 189 |
+ def draw_contact(self, jid):
|
|
| 190 |
+ iter_ = self._get_contact_iter(jid)
|
|
| 191 |
+ if not iter_:
|
|
| 192 |
+ return
|
|
| 193 |
+ |
|
| 194 |
+ client = app.get_client(self._account)
|
|
| 195 |
+ contact = client.get_module('Contacts').get_contact(jid)
|
|
| 196 |
+ |
|
| 197 |
+ self.draw_avatar(contact)
|
|
| 198 |
+ self._store[iter_][Column.TEXT] = GLib.markup_escape_text(contact.name)
|
|
| 199 |
+ |
|
| 200 |
+ def draw_avatar(self, contact):
|
|
| 201 |
+ iter_ = self._get_contact_iter(contact.jid)
|
|
| 202 |
+ if iter_ is None:
|
|
| 203 |
+ return
|
|
| 204 |
+ |
|
| 205 |
+ surface = contact.get_avatar(
|
|
| 206 |
+ AvatarSize.ROSTER, self.get_scale_factor())
|
|
| 207 |
+ self._store[iter_][Column.AVATAR] = surface
|
|
| 208 |
+ |
|
| 209 |
+ def _get_group_iter(self, group_name: str) -> Optional[Gtk.TreeIter]:
|
|
| 210 |
+ try:
|
|
| 211 |
+ ref = self._group_refs[group_name]
|
|
| 212 |
+ except KeyError:
|
|
| 213 |
+ return None
|
|
| 214 |
+ |
|
| 215 |
+ path = ref.get_path()
|
|
| 216 |
+ if path is None:
|
|
| 217 |
+ return None
|
|
| 218 |
+ return self._store.get_iter(path)
|
|
| 219 |
+ |
|
| 220 |
+ def _get_contact_iter(self, jid: str) -> Optional[Gtk.TreeIter]:
|
|
| 221 |
+ try:
|
|
| 222 |
+ ref = self._contact_refs[jid]
|
|
| 223 |
+ except KeyError:
|
|
| 224 |
+ return None
|
|
| 225 |
+ |
|
| 226 |
+ path = ref.get_path()
|
|
| 227 |
+ if path is None:
|
|
| 228 |
+ return None
|
|
| 229 |
+ |
|
| 230 |
+ return self._store.get_iter(path)
|
|
| 231 |
+ |
|
| 232 |
+ def clear(self):
|
|
| 233 |
+ self._contact_refs = {}
|
|
| 234 |
+ self._group_refs = {}
|
|
| 235 |
+ self._store.clear()
|
|
| 236 |
+ |
|
| 237 |
+ def _on_presence_received(self, event):
|
|
| 238 |
+ pass
|
|
| 239 |
+ |
|
| 240 |
+ def process_event(self, event):
|
|
| 241 |
+ if event.name not in HANDLED_EVENTS:
|
|
| 242 |
+ return
|
|
| 243 |
+ |
|
| 244 |
+ if event.name == 'presence-received':
|
|
| 245 |
+ self._on_presence_received(event)
|
|
| 246 |
+ else:
|
|
| 247 |
+ log.warning('Unhandled Event: %s', event.name)
|
| ... | ... | @@ -809,12 +809,13 @@ def get_chat_list_row_menu(workspace_id, account, jid, pinned): |
| 809 | 809 |
action, label = item
|
| 810 | 810 |
action = f'win.{action}'
|
| 811 | 811 |
menuitem = Gio.MenuItem.new(label, action)
|
| 812 |
- variant_list = GLib.Variant('as', [workspace_id, account, jid])
|
|
| 812 |
+ variant_list = GLib.Variant(
|
|
| 813 |
+ 'as', [workspace_id, account, str(jid)])
|
|
| 813 | 814 |
menuitem.set_action_and_target_value(action, variant_list)
|
| 814 | 815 |
menu.append_item(menuitem)
|
| 815 | 816 |
else:
|
| 816 | 817 |
# This is a submenu
|
| 817 |
- submenu = build_workspaces_submenu(workspace_id, account, jid)
|
|
| 818 |
+ submenu = build_workspaces_submenu(workspace_id, account, str(jid))
|
|
| 818 | 819 |
menu.append_submenu(item[0], submenu)
|
| 819 | 820 |
|
| 820 | 821 |
return menu
|