diff options
author | Elliott Sales de Andrade <quantum.analyst@gmail.com> | 2023-04-03 23:11:34 -0500 |
---|---|---|
committer | Elliott Sales de Andrade <quantum.analyst@gmail.com> | 2023-04-03 23:11:34 -0500 |
commit | bee99bb8f844575c7fdb938f998f859ee03fa23a (patch) | |
tree | 5a9bc591ab6af266976256704f37ccbb9162b60c | |
parent | 18ae233e91311db8807987be3c5953cda43d5113 (diff) | |
download | pidgin-bee99bb8f844575c7fdb938f998f859ee03fa23a.tar.gz |
Convert PidginStatusBox to a GtkMenuButton
This moves to a menu system to populate the list of statuses, and the new `PidginStatusDisplay` to show them. I tried a `GtkDropDown` previously, but that was unable to show the separators that we previously had. Consequently, it uses actions a bit more then before.
However, since menus don't support icons+text at the same time, and doesn't have a factory property, we do need to manually create custom widgets a bit, but it's mostly only a button + the `PidginStatusDisplay`.
The main display is now centred, but there seems to be nothing we can do about that, as the box with that alignment is internal to the `GtkMenuButton`. If we don't want that, we could switch to managing a `GtkToggleButton` and `GtkPopoverMenu` ourselves.
Testing Done:
Opened status box and confirmed that all primitives and saved statuses were there as expected.
Modified a status, and confirmed that the menu updated to match.
Added/deleted statuses from the manager and confirmed they menu updated to match.
Chose the New Status/Saved Statuses menu items and confirmed they opened the correct dialogs.
Picked a bunch of statuses from the menu items and confirmed the main display updated, and some updates were sent out to protocols (e.g. Demo disconnecting when offline, etc.)
Reviewed at https://reviews.imfreedom.org/r/2386/
-rw-r--r-- | pidgin/pidginstatusbox.c | 407 | ||||
-rw-r--r-- | pidgin/resources/Status/box.ui | 89 |
2 files changed, 217 insertions, 279 deletions
diff --git a/pidgin/pidginstatusbox.c b/pidgin/pidginstatusbox.c index ff92d57246..e96d25ba29 100644 --- a/pidgin/pidginstatusbox.c +++ b/pidgin/pidginstatusbox.c @@ -28,153 +28,112 @@ #include "pidginstatusbox.h" #include "pidginiconname.h" +#include "pidginstatusdisplay.h" -#define PRIMITIVE_FORMAT "primitive_%d" #define SAVEDSTATUS_FORMAT "savedstatus_%lu" -typedef enum { - PIDGIN_STATUS_BOX_TYPE_SEPARATOR, - PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PIDGIN_STATUS_BOX_TYPE_POPULAR, - PIDGIN_STATUS_BOX_TYPE_ACTION, - PIDGIN_STATUS_BOX_NUM_TYPES -} PidginStatusBoxItemType; - struct _PidginStatusBox { GtkBox parent; - GtkListStore *model; - GtkWidget *combo; - - /* This is used to flipback to the correct status when one of the actions - * items is selected. - */ - gchar *active_id; -}; + GtkWidget *button; + PidginStatusDisplay *display; + GtkPopover *popover; -enum { - ID_COLUMN, - TYPE_COLUMN, /* PidginStatusBoxItemType */ - ICON_NAME_COLUMN, - PRIMITIVE_COLUMN, - TEXT_COLUMN, - /* This value depends on TYPE_COLUMN. For POPULAR types, this is the - * creation time. - */ - DATA_COLUMN, - EMBLEM_VISIBLE_COLUMN, - NUM_COLUMNS + GMenu *primitives; + GMenu *saved_statuses; + GList *custom_widgets; }; G_DEFINE_TYPE(PidginStatusBox, pidgin_status_box, GTK_TYPE_BOX) -/* This prototype is necessary so we can block this signal handler when we are - * manually updating the combobox. - */ -static void pidgin_status_box_combo_changed_cb(GtkComboBox *combo, gpointer user_data); - /****************************************************************************** * Helpers *****************************************************************************/ -static void -pidgin_status_box_update_to_status(PidginStatusBox *status_box, - PurpleSavedStatus *status) -{ - gchar *id = NULL; - gboolean set = FALSE; - time_t creation_time = 0; - - /* Try to set the combo box to the saved status. */ - creation_time = purple_savedstatus_get_creation_time(status); - id = g_strdup_printf(SAVEDSTATUS_FORMAT, creation_time); +static GtkWidget * +pidgin_status_box_make_primitive_widget(const char *action, const char *id) { + GtkWidget *button = NULL; + GtkWidget *display = NULL; + PurpleStatusPrimitive primitive = PURPLE_STATUS_UNSET; - set = gtk_combo_box_set_active_id(GTK_COMBO_BOX(status_box->combo), id); - g_free(id); + primitive = purple_primitive_get_type_from_id(id); + display = pidgin_status_display_new_for_primitive(primitive); - /* If we failed to set via the savedstatus, fallback to the primitive. */ - if(!set) { - PurpleStatusPrimitive primitive; + button = gtk_button_new(); + gtk_widget_add_css_class(button, "flat"); + gtk_button_set_child(GTK_BUTTON(button), display); - primitive = purple_savedstatus_get_primitive_type(status); - id = g_strdup_printf(PRIMITIVE_FORMAT, primitive); + gtk_actionable_set_action_name(GTK_ACTIONABLE(button), action); + gtk_actionable_set_action_target(GTK_ACTIONABLE(button), + (const char *)G_VARIANT_TYPE_INT32, + primitive); - gtk_combo_box_set_active_id(GTK_COMBO_BOX(status_box->combo), id); - g_free(id); - } + return button; } static void -pidgin_status_box_add(PidginStatusBox *status_box, - PidginStatusBoxItemType type, - PurpleStatusPrimitive primitive, const gchar *text, - gpointer data) -{ - GtkTreeIter iter; - gchar *id = NULL, *escaped_text = NULL; - const gchar *icon_name = NULL; - gboolean emblem_visible = FALSE; - - escaped_text = g_markup_escape_text(text, -1); - - if(type == PIDGIN_STATUS_BOX_TYPE_POPULAR) { - PurpleSavedStatus *saved_status = NULL; - time_t creation_time = GPOINTER_TO_INT(data); - - saved_status = purple_savedstatus_find_by_creation_time(creation_time); - - if(saved_status != NULL) { - id = g_strdup_printf(SAVEDSTATUS_FORMAT, creation_time); - - if(!purple_savedstatus_is_transient(saved_status)) { - emblem_visible = TRUE; - } - } - } - - if(id == NULL && primitive != PURPLE_STATUS_UNSET) { - id = g_strdup_printf(PRIMITIVE_FORMAT, primitive); +pidgin_status_box_populate_primitives(PidginStatusBox *status_box) { + GtkPopoverMenu *popover = GTK_POPOVER_MENU(status_box->popover); + GMenuModel *menu = G_MENU_MODEL(status_box->primitives); + gint n_items = 0; + + n_items = g_menu_model_get_n_items(menu); + for(gint index = 0; index < n_items; index++) { + GtkWidget *button = NULL; + char *action = NULL; + char *target = NULL; + char *custom_id = NULL; + + g_menu_model_get_item_attribute(menu, index, G_MENU_ATTRIBUTE_ACTION, + "s", &action); + g_menu_model_get_item_attribute(menu, index, G_MENU_ATTRIBUTE_TARGET, + "s", &target); + g_menu_model_get_item_attribute(menu, index, "custom", "s", &custom_id); + + button = pidgin_status_box_make_primitive_widget(action, target); + gtk_popover_menu_add_child(popover, button, custom_id); + + g_free(action); + g_free(target); + g_free(custom_id); } - - icon_name = pidgin_icon_name_from_status_primitive(primitive, NULL); - - gtk_list_store_append(status_box->model, &iter); - gtk_list_store_set(status_box->model, &iter, - ID_COLUMN, id, - TYPE_COLUMN, type, - ICON_NAME_COLUMN, icon_name, - PRIMITIVE_COLUMN, primitive, - TEXT_COLUMN, escaped_text, - DATA_COLUMN, data, - EMBLEM_VISIBLE_COLUMN, emblem_visible, - -1); - - g_free(escaped_text); - g_free(id); } -static gboolean -pidgin_status_box_row_separator_func(GtkTreeModel *model, GtkTreeIter *iter, - G_GNUC_UNUSED gpointer data) +static char * +pidgin_status_box_make_savedstatus_widget(PurpleSavedStatus *saved_status, + GtkWidget **widget) { - PidginStatusBoxItemType type; + GtkWidget *button = NULL; + GtkWidget *display = NULL; + time_t creation_time = 0; - gtk_tree_model_get(model, iter, TYPE_COLUMN, &type, -1); + display = pidgin_status_display_new_for_saved_status(saved_status); - return type == PIDGIN_STATUS_BOX_TYPE_SEPARATOR; -} + if(!purple_savedstatus_is_transient(saved_status)) { + GtkWidget *image = gtk_image_new_from_icon_name("document-save"); + gtk_widget_set_halign(image, GTK_ALIGN_END); + gtk_widget_set_hexpand(image, TRUE); + gtk_box_append(GTK_BOX(display), image); + } -static void -pidgin_status_box_add_separator(PidginStatusBox *status_box) { - GtkTreeIter iter; + button = gtk_button_new(); + gtk_widget_add_css_class(button, "flat"); + gtk_button_set_child(GTK_BUTTON(button), display); + + creation_time = purple_savedstatus_get_creation_time(saved_status); + gtk_actionable_set_action_name(GTK_ACTIONABLE(button), "status.set-saved"); + gtk_actionable_set_action_target(GTK_ACTIONABLE(button), + (const char *)G_VARIANT_TYPE_INT64, + creation_time); + *widget = button; - gtk_list_store_append(status_box->model, &iter); - gtk_list_store_set(status_box->model, &iter, - TYPE_COLUMN, PIDGIN_STATUS_BOX_TYPE_SEPARATOR, - -1); + return g_strdup_printf(SAVEDSTATUS_FORMAT, creation_time); } static void -pidgin_status_box_update_saved_statuses(PidginStatusBox *status_box) { +pidgin_status_box_populate_saved_statuses(PidginStatusBox *status_box) +{ + GtkPopoverMenu *popover_menu = NULL; + GMenu *menu = NULL; GList *list, *cur; list = purple_savedstatuses_get_popular(6); @@ -183,124 +142,84 @@ pidgin_status_box_update_saved_statuses(PidginStatusBox *status_box) { return; } + popover_menu = GTK_POPOVER_MENU(status_box->popover); + menu = status_box->saved_statuses; for(cur = list; cur != NULL; cur = cur->next) { PurpleSavedStatus *saved = cur->data; - GString *text = NULL; - time_t creation_time; - - text = g_string_new(purple_savedstatus_get_title(saved)); - - if(!purple_savedstatus_is_transient(saved)) { - /* - * Transient statuses do not have a title, so the savedstatus - * API returns the message when purple_savedstatus_get_title() is - * called, so we don't need to get the message a second time. - */ - const gchar *message = NULL; - - message = purple_savedstatus_get_message(saved); - if(message != NULL) { - gchar *stripped = purple_markup_strip_html(message); - - purple_util_chrreplace(stripped, '\n', ' '); - g_string_append_printf(text, " - %s", stripped); - g_free(stripped); - } - } + GtkWidget *widget = NULL; + char *id = NULL; + GMenuItem *item = NULL; + + id = pidgin_status_box_make_savedstatus_widget(saved, &widget); + item = g_menu_item_new(NULL, NULL); + g_menu_item_set_attribute(item, "custom", "s", id); + g_menu_append_item(menu, item); + gtk_popover_menu_add_child(popover_menu, widget, id); + status_box->custom_widgets = g_list_prepend(status_box->custom_widgets, + widget); - creation_time = purple_savedstatus_get_creation_time(saved); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_POPULAR, - purple_savedstatus_get_primitive_type(saved), - text->str, GINT_TO_POINTER(creation_time)); - - g_string_free(text, TRUE); + g_free(id); } g_list_free(list); - - pidgin_status_box_add_separator(status_box); -} - -static void -pidgin_status_box_populate(PidginStatusBox *status_box) { - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PURPLE_STATUS_AVAILABLE, _("Available"), NULL); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PURPLE_STATUS_AWAY, _("Away"), NULL); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PURPLE_STATUS_UNAVAILABLE, _("Do not disturb"), - NULL); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PURPLE_STATUS_INVISIBLE, _("Invisible"), NULL); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, - PURPLE_STATUS_OFFLINE, _("Offline"), NULL); - - pidgin_status_box_add_separator(status_box); - - pidgin_status_box_update_saved_statuses(status_box); - - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_ACTION, - PURPLE_STATUS_UNSET, _("New Status..."), - "new-status"); - pidgin_status_box_add(status_box, PIDGIN_STATUS_BOX_TYPE_ACTION, - PURPLE_STATUS_UNSET, _("Saved Statuses..."), - "status-manager"); } /****************************************************************************** * Callbacks *****************************************************************************/ static void -pidgin_status_box_combo_changed_cb(GtkComboBox *combo, gpointer user_data) { - PidginStatusBox *status_box = user_data; - PidginStatusBoxItemType type; +pidgin_status_box_set_primitive(G_GNUC_UNUSED GSimpleAction *action, + GVariant *parameter, gpointer data) +{ + PidginStatusBox *status_box = data; PurpleSavedStatus *saved_status = NULL; PurpleStatusPrimitive primitive; - GtkTreeIter iter; - gchar *id = NULL; - gpointer data; - if(!gtk_combo_box_get_active_iter(combo, &iter)) { + gtk_menu_button_popdown(GTK_MENU_BUTTON(status_box->button)); + + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_INT32)) { + g_critical("status.set-primitive action parameter is of incorrect type %s", + g_variant_get_type_string(parameter)); return; } - gtk_tree_model_get(GTK_TREE_MODEL(status_box->model), &iter, - ID_COLUMN, &id, - TYPE_COLUMN, &type, - PRIMITIVE_COLUMN, &primitive, - DATA_COLUMN, &data, - -1); - - if(type == PIDGIN_STATUS_BOX_TYPE_PRIMITIVE) { - saved_status = purple_savedstatus_find_transient_by_type_and_message(primitive, NULL); - if(saved_status == NULL) { - saved_status = purple_savedstatus_new(NULL, primitive); - } - } else if(type == PIDGIN_STATUS_BOX_TYPE_POPULAR) { - time_t creation_time = GPOINTER_TO_INT(data); + primitive = g_variant_get_int32(parameter); - saved_status = purple_savedstatus_find_by_creation_time(creation_time); - } else if(type == PIDGIN_STATUS_BOX_TYPE_ACTION) { - GApplication *application = NULL; - const gchar *action_name = (const gchar *)data; + saved_status = purple_savedstatus_find_transient_by_type_and_message(primitive, NULL); + if(saved_status == NULL) { + saved_status = purple_savedstatus_new(NULL, primitive); + } - application = g_application_get_default(); + if(saved_status != NULL) { + if(saved_status != purple_savedstatus_get_current()) { + purple_savedstatus_activate(saved_status); + } + } +} + +static void +pidgin_status_box_set_saved_status(G_GNUC_UNUSED GSimpleAction *action, + GVariant *parameter, gpointer data) +{ + PidginStatusBox *status_box = data; + PurpleSavedStatus *saved_status = NULL; + time_t creation_time = 0; - g_action_group_activate_action(G_ACTION_GROUP(application), - action_name, NULL); + gtk_menu_button_popdown(GTK_MENU_BUTTON(status_box->button)); - gtk_combo_box_set_active_id(combo, status_box->active_id); + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_INT64)) { + g_critical("status.set-saved action parameter is of incorrect type %s", + g_variant_get_type_string(parameter)); + return; } + creation_time = (time_t)g_variant_get_int64(parameter); + saved_status = purple_savedstatus_find_by_creation_time(creation_time); + if(saved_status != NULL) { if(saved_status != purple_savedstatus_get_current()) { purple_savedstatus_activate(saved_status); } - - g_free(status_box->active_id); - status_box->active_id = id; - } else { - g_free(id); } } @@ -316,7 +235,14 @@ pidgin_status_box_savedstatus_changed_cb(PurpleSavedStatus *now, return; } - pidgin_status_box_update_to_status(status_box, now); + pidgin_status_display_set_saved_status(status_box->display, now); +} + +static void +pidgin_status_box_remove_custom_widget(GtkWidget *widget, gpointer user_data) { + GtkPopoverMenu *popover_menu = user_data; + + gtk_popover_menu_remove_child(popover_menu, widget); } static void @@ -324,27 +250,14 @@ pidgin_status_box_savedstatus_updated_cb(G_GNUC_UNUSED PurpleSavedStatus *status gpointer data) { PidginStatusBox *status_box = data; - PurpleSavedStatus *current = NULL; - static gboolean getting_current = FALSE; - - /* purple_status_get_current will create a new status if this is a brand - * new install or the setting wasn't found. This leads to this handler - * getting stuck in a loop until it segfaults because the stack smashed - * into the heap. Anyways, we use this static boolean to check when this - * function is called by purple_status_get_current so we can bail out and - * break the loop. - */ - if(getting_current) { - return; - } - gtk_list_store_clear(status_box->model); - pidgin_status_box_populate(status_box); + g_list_foreach(status_box->custom_widgets, + (GFunc)pidgin_status_box_remove_custom_widget, + status_box->popover); + g_clear_list(&status_box->custom_widgets, NULL); + g_menu_remove_all(status_box->saved_statuses); - getting_current = TRUE; - current = purple_savedstatus_get_current(); - pidgin_status_box_update_to_status(status_box, current); - getting_current = FALSE; + pidgin_status_box_populate_saved_statuses(status_box); } /****************************************************************************** @@ -356,7 +269,7 @@ pidgin_status_box_finalize(GObject *obj) { purple_signals_disconnect_by_handle(status_box); - g_free(status_box->active_id); + g_clear_list(&status_box->custom_widgets, NULL); G_OBJECT_CLASS(pidgin_status_box_parent_class)->finalize(obj); } @@ -364,14 +277,33 @@ pidgin_status_box_finalize(GObject *obj) { static void pidgin_status_box_init(PidginStatusBox *status_box) { gpointer handle; + GSimpleActionGroup *action_group = NULL; + GActionEntry actions[] = { + { + .name = "set-primitive", + .activate = pidgin_status_box_set_primitive, + .parameter_type = (const char *)G_VARIANT_TYPE_INT32, + }, { + .name = "set-saved", + .activate = pidgin_status_box_set_saved_status, + .parameter_type = (const char *)G_VARIANT_TYPE_INT64, + }, + }; gtk_widget_init_template(GTK_WIDGET(status_box)); - gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(status_box->combo), - pidgin_status_box_row_separator_func, - NULL, NULL); + action_group = g_simple_action_group_new(); + g_action_map_add_action_entries(G_ACTION_MAP(action_group), + actions, G_N_ELEMENTS(actions), + status_box); + gtk_widget_insert_action_group(GTK_WIDGET(status_box), "status", + G_ACTION_GROUP(action_group)); - pidgin_status_box_populate(status_box); + status_box->popover = gtk_menu_button_get_popover(GTK_MENU_BUTTON(status_box->button)); + gtk_popover_set_has_arrow(status_box->popover, FALSE); + + pidgin_status_box_populate_primitives(status_box); + pidgin_status_box_populate_saved_statuses(status_box); handle = purple_savedstatuses_get_handle(); purple_signal_connect(handle, "savedstatus-changed", status_box, @@ -390,13 +322,13 @@ pidgin_status_box_init(PidginStatusBox *status_box) { static void pidgin_status_box_constructed(GObject *obj) { + PidginStatusBox *status_box = PIDGIN_STATUS_BOX(obj); PurpleSavedStatus *status = NULL; G_OBJECT_CLASS(pidgin_status_box_parent_class)->constructed(obj); status = purple_savedstatus_get_current(); - - pidgin_status_box_update_to_status(PIDGIN_STATUS_BOX(obj), status); + pidgin_status_display_set_saved_status(status_box->display, status); } static void @@ -413,12 +345,13 @@ pidgin_status_box_class_init(PidginStatusBoxClass *klass) { ); gtk_widget_class_bind_template_child(widget_class, PidginStatusBox, - model); + button); gtk_widget_class_bind_template_child(widget_class, PidginStatusBox, - combo); - - gtk_widget_class_bind_template_callback(widget_class, - pidgin_status_box_combo_changed_cb); + display); + gtk_widget_class_bind_template_child(widget_class, PidginStatusBox, + primitives); + gtk_widget_class_bind_template_child(widget_class, PidginStatusBox, + saved_statuses); } /****************************************************************************** diff --git a/pidgin/resources/Status/box.ui b/pidgin/resources/Status/box.ui index 7c142b8f9b..1423701cb7 100644 --- a/pidgin/resources/Status/box.ui +++ b/pidgin/resources/Status/box.ui @@ -24,54 +24,59 @@ License along with this library; if not, see <https://www.gnu.org/licenses/>. <!-- interface-name Pidgin --> <!-- interface-description Internet Messenger --> <!-- interface-copyright Pidgin Developers <devel@pidgin.im> --> - <object class="GtkListStore" id="model"> - <columns> - <!-- column-name id --> - <column type="gchararray"/> - <!-- column-name type --> - <column type="gint"/> - <!-- column-name icon-name --> - <column type="gchararray"/> - <!-- column-name primitive --> - <column type="gint"/> - <!-- column-name text --> - <column type="gchararray"/> - <!-- column-name data --> - <column type="gpointer"/> - <!-- column-name emblem-visible --> - <column type="gboolean"/> - </columns> - </object> <template class="PidginStatusBox" parent="GtkBox"> <property name="focusable">1</property> <property name="orientation">vertical</property> <child> - <object class="GtkComboBox" id="combo"> + <object class="GtkMenuButton" id="button"> <property name="focusable">1</property> - <property name="model">model</property> - <property name="id-column">0</property> - <signal name="changed" handler="pidgin_status_box_combo_changed_cb" object="PidginStatusBox" swapped="no"/> - <child> - <object class="GtkCellRendererPixbuf" id="icon"/> - <attributes> - <attribute name="icon-name">2</attribute> - </attributes> - </child> - <child> - <object class="GtkCellRendererText" id="text"/> - <attributes> - <attribute name="markup">4</attribute> - </attributes> - </child> - <child> - <object class="GtkCellRendererPixbuf" id="emblem"> - <property name="icon-name">document-save</property> - </object> - <attributes> - <attribute name="visible">6</attribute> - </attributes> - </child> + <property name="always-show-arrow">1</property> + <property name="child"> + <object class="PidginStatusDisplay" id="display"/> + </property> + <property name="menu-model">menu</property> </object> </child> </template> + <menu id="menu"> + <section id="primitives"> + <!-- NOTE: labels are automatically set for primitives from the target primitive. --> + <item> + <attribute name="action">status.set-primitive</attribute> + <attribute name="target">available</attribute> + <attribute name="custom">available</attribute> + </item> + <item> + <attribute name="action">status.set-primitive</attribute> + <attribute name="target">away</attribute> + <attribute name="custom">away</attribute> + </item> + <item> + <attribute name="action">status.set-primitive</attribute> + <attribute name="target">unavailable</attribute> + <attribute name="custom">unavailable</attribute> + </item> + <item> + <attribute name="action">status.set-primitive</attribute> + <attribute name="target">invisible</attribute> + <attribute name="custom">invisible</attribute> + </item> + <item> + <attribute name="action">status.set-primitive</attribute> + <attribute name="target">offline</attribute> + <attribute name="custom">offline</attribute> + </item> + </section> + <section id="saved_statuses"/> + <section id="actions"> + <item> + <attribute name="label" translatable="1">New Status...</attribute> + <attribute name="action">app.new-status</attribute> + </item> + <item> + <attribute name="label" translatable="1">Saved Statuses...</attribute> + <attribute name="action">app.status-manager</attribute> + </item> + </section> + </menu> </interface> |