1 /*
2  * vala-panel-appmenu
3  * Copyright (C) 2018 Konstantin Pugin <ria.freelander@gmail.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <inttypes.h>
20 
21 #include "debug.h"
22 #include "definitions.h"
23 #include "item.h"
24 #include "model.h"
25 #include "section.h"
26 
27 struct _DBusMenuModel
28 {
29 	GMenuModel parent_instance;
30 
31 	uint parent_id;
32 	uint current_revision;
33 	GCancellable *cancellable;
34 	DBusMenuXml *xml;
35 	GActionGroup *received_action_group;
36 	GSequence *items;
37 	bool layout_update_required;
38 	bool layout_update_in_progress;
39 };
40 
41 static const char *property_names[] = { "accessible-desc",
42 	                                "children-display",
43 	                                "disposition",
44 	                                "enabled",
45 	                                "icon-data",
46 	                                "icon-name",
47 	                                "label",
48 	                                "shortcut",
49 	                                "toggle-type",
50 	                                "toggle-state",
51 	                                "type",
52 	                                "visible",
53 	                                NULL };
54 enum
55 {
56 	PROP_NULL         = 0,
57 	PROP_XML          = 1,
58 	PROP_ACTION_GROUP = 2,
59 	PROP_PARENT_ID    = 3,
60 	NUM_PROPS
61 };
62 
63 static GParamSpec *properties[NUM_PROPS] = { NULL };
64 
65 static DBusMenuItem *dbus_menu_model_find(DBusMenuModel *menu, uint item_id);
66 static DBusMenuItem *dbus_menu_model_find_section(DBusMenuModel *menu, uint section_num);
67 static GSequenceIter *dbus_menu_model_find_place(DBusMenuModel *menu, uint section_num, int place);
68 
G_DEFINE_TYPE(DBusMenuModel,dbus_menu_model,G_TYPE_MENU_MODEL)69 G_DEFINE_TYPE(DBusMenuModel, dbus_menu_model, G_TYPE_MENU_MODEL)
70 
71 static gint dbus_menu_model_get_n_items(GMenuModel *model)
72 {
73 	DBusMenuModel *menu      = (DBusMenuModel *)(model);
74 	GSequenceIter *last_iter = g_sequence_iter_prev(g_sequence_get_end_iter(menu->items));
75 	DBusMenuItem *last       = g_sequence_get(last_iter);
76 	return last->section_num + 1;
77 }
78 
dbus_menu_model_get_item_attributes(GMenuModel * model,gint position,GHashTable ** table)79 static void dbus_menu_model_get_item_attributes(GMenuModel *model, gint position,
80                                                 GHashTable **table)
81 {
82 	DBusMenuModel *menu = DBUS_MENU_MODEL(model);
83 	for (GSequenceIter *iter = g_sequence_get_begin_iter(menu->items);
84 	     !g_sequence_iter_is_end(iter);
85 	     iter = g_sequence_iter_next(iter))
86 	{
87 		DBusMenuItem *item = (DBusMenuItem *)g_sequence_get(iter);
88 		if (item->section_num == position && item->place == -1)
89 		{
90 			*table = g_hash_table_ref(item->attrs);
91 			return;
92 		}
93 	}
94 }
95 
dbus_menu_model_get_item_links(GMenuModel * model,gint position,GHashTable ** table)96 static void dbus_menu_model_get_item_links(GMenuModel *model, gint position, GHashTable **table)
97 {
98 	DBusMenuModel *menu = DBUS_MENU_MODEL(model);
99 	for (GSequenceIter *iter = g_sequence_get_begin_iter(menu->items);
100 	     !g_sequence_iter_is_end(iter);
101 	     iter = g_sequence_iter_next(iter))
102 	{
103 		DBusMenuItem *item = (DBusMenuItem *)g_sequence_get(iter);
104 		if (item->section_num == position && item->place == -1)
105 		{
106 			*table = g_hash_table_ref(item->links);
107 			return;
108 		}
109 	}
110 }
111 
dbus_menu_model_is_mutable(GMenuModel * model)112 static int dbus_menu_model_is_mutable(GMenuModel *model)
113 {
114 	return true;
115 }
116 
117 struct layout_data
118 {
119 	GMenuModel *model;
120 	int section_num;
121 	uint pos;
122 	uint old_num;
123 	uint new_num;
124 };
125 
dbus_menu_model_items(DBusMenuModel * model)126 GSequence *dbus_menu_model_items(DBusMenuModel *model)
127 {
128 	return model->items;
129 }
130 
queue_compare_func(const struct layout_data * a,const struct layout_data * b)131 int queue_compare_func(const struct layout_data *a, const struct layout_data *b)
132 {
133 	if (a->model != b->model)
134 		return DBUS_MENU_IS_MODEL(a->model) ? -1 : 1;
135 	else if (a->old_num != b->old_num)
136 		return b->old_num - a->old_num;
137 	else if (a->new_num != b->new_num)
138 		return b->new_num - a->new_num;
139 	else if (a->pos != b->pos)
140 		return b->pos - a->pos;
141 	return 0;
142 }
143 
dbus_menu_model_sort_func(gconstpointer a,gconstpointer b,G_GNUC_UNUSED void * user_data)144 static int dbus_menu_model_sort_func(gconstpointer a, gconstpointer b,
145                                      G_GNUC_UNUSED void *user_data)
146 {
147 	DBusMenuItem *aitem = (DBusMenuItem *)a;
148 	DBusMenuItem *bitem = (DBusMenuItem *)b;
149 
150 	if (bitem->section_num != aitem->section_num)
151 		return aitem->section_num - bitem->section_num;
152 
153 	return aitem->place - bitem->place;
154 }
155 
add_signal_to_queue(DBusMenuModel * model,GQueue * queue,int sect_num,int pos,int removed,int added)156 static void add_signal_to_queue(DBusMenuModel *model, GQueue *queue, int sect_num, int pos,
157                                 int removed, int added)
158 {
159 	struct layout_data *data = g_new0(struct layout_data, 1);
160 	if (sect_num >= 0)
161 	{
162 		DBusMenuItem *item = dbus_menu_model_find_section(model, sect_num);
163 		data->model = G_MENU_MODEL(g_hash_table_lookup(item->links, G_MENU_LINK_SECTION));
164 	}
165 	else
166 	{
167 		data->model = G_MENU_MODEL(model);
168 	}
169 	data->section_num = sect_num;
170 	data->pos         = pos;
171 	data->old_num     = removed;
172 	data->new_num     = added;
173 	gpointer l        = g_queue_find_custom(queue, data, (GCompareFunc)queue_compare_func);
174 	if (!l)
175 		g_queue_push_head(queue, data);
176 }
177 
queue_emit_all(GQueue * queue)178 static bool queue_emit_all(GQueue *queue)
179 {
180 	struct layout_data *index = NULL;
181 	while ((index = (struct layout_data *)g_queue_pop_head(queue)))
182 	{
183 		g_menu_model_items_changed(index->model,
184 		                           index->pos,
185 		                           index->old_num,
186 		                           index->new_num);
187 		g_free(index);
188 	}
189 	return G_SOURCE_REMOVE;
190 }
191 
preload_idle(DBusMenuItem * item)192 static bool preload_idle(DBusMenuItem *item)
193 {
194 	dbus_menu_item_preload(item);
195 	return G_SOURCE_REMOVE;
196 }
197 
menu_item_copy_and_load(DBusMenuModel * menu,DBusMenuItem * old,DBusMenuItem * new_item)198 static void menu_item_copy_and_load(DBusMenuModel *menu, DBusMenuItem *old, DBusMenuItem *new_item)
199 {
200 	dbus_menu_item_copy_submenu(old, new_item, menu);
201 	dbus_menu_item_generate_action(new_item, menu);
202 	// It is a preload hack. If this is a toplevel menu, we need to fetch menu under toplevel to
203 	// avoid menu jumping bug. Now we need to use it for all menus - no menus are preloaded,
204 	// AFAIK
205 	dbus_menu_item_update_enabled(new_item, true);
206 	new_item->toggled = true;
207 	g_timeout_add_full(100, 300, (GSourceFunc)preload_idle, new_item, NULL);
208 }
209 
210 // We deal only with layouts with depth 1 (not all)
layout_parse(DBusMenuModel * menu,GVariant * layout)211 static void layout_parse(DBusMenuModel *menu, GVariant *layout)
212 {
213 	guint id;
214 	GVariant *props;
215 	GVariant *items;
216 	if (!g_variant_is_of_type(layout, G_VARIANT_TYPE("(ia{sv}av)")))
217 	{
218 		g_warning(
219 		    "Type of return value for 'layout' property in "
220 		    "'GetLayout' call should be '(ia{sv}av)' but got '%s'",
221 		    g_variant_get_type_string(layout));
222 
223 		return;
224 	}
225 	if (menu->layout_update_in_progress)
226 		return;
227 	menu->layout_update_required    = false;
228 	menu->layout_update_in_progress = true;
229 	g_variant_get(layout, "(i@a{sv}@av)", &id, &props, &items);
230 	g_variant_unref(props);
231 	g_autoptr(GQueue) signal_queue = g_queue_new();
232 	GVariantIter iter;
233 	GVariant *child;
234 	// Start parsing. We need to track section number, and also GSequenceIter to
235 	// current section. Also we track change position, number of added and removed
236 	// items for current section
237 	uint section_num            = 0;
238 	uint place                  = 0;
239 	uint old_sections           = g_menu_model_get_n_items(G_MENU_MODEL(menu));
240 	uint added                  = 0;
241 	int change_pos              = -1;
242 	GSequenceIter *current_iter = g_sequence_get_begin_iter(menu->items);
243 	g_variant_iter_init(&iter, items);
244 	while ((child = g_variant_iter_next_value(&iter)))
245 	{
246 		GVariant *value = g_variant_get_variant(child);
247 		guint cid;
248 		GVariant *cprops;
249 		GVariant *citems;
250 		g_variant_get(value, "(i@a{sv}@av)", &cid, &cprops, &citems);
251 		g_variant_unref(citems);
252 
253 		DBusMenuItem *old      = NULL;
254 		DBusMenuItem *new_item = dbus_menu_item_new(cid, menu, cprops);
255 		// We receive a section (separator or x-kde-title)
256 		if (new_item->action_type == DBUS_MENU_ACTION_SECTION)
257 		{
258 			bool is_valid_section = !new_item->toggled && place > 0;
259 			// Section is valid, so, parse it
260 			if (is_valid_section)
261 			{
262 				// Do some common tasks: increment section_num and iter
263 				++section_num;
264 				place--;
265 				new_item->section_num = section_num;
266 				new_item->place       = -1;
267 				GSequenceIter *old_iter =
268 				    g_sequence_lookup(menu->items,
269 				                      new_item,
270 				                      dbus_menu_model_sort_func,
271 				                      NULL);
272 				if (!old_iter)
273 				{
274 					g_hash_table_insert(
275 					    new_item->links,
276 					    G_MENU_LINK_SECTION,
277 					    dbus_menu_section_model_new(menu, section_num));
278 					old_iter =
279 					    g_sequence_insert_sorted(menu->items,
280 					                             new_item,
281 					                             dbus_menu_model_sort_func,
282 					                             NULL);
283 				}
284 				else
285 					dbus_menu_item_free(new_item);
286 				old =
287 				    (DBusMenuItem *)g_sequence_get(g_sequence_iter_prev(old_iter));
288 				int delta                 = old->place - place;
289 				GSequenceIter *place_iter = g_sequence_iter_move(old_iter, -delta);
290 				// Cleanup all items in prev section (if there is more items than
291 				// current)
292 				if (delta > 0)
293 					g_sequence_remove_range(place_iter, old_iter);
294 				// If we already have this section in old layout, and items to this
295 				// section was added and/or removed, we add a signal to signal_queue
296 				// about this change. Else do nothing, section signal will do it for
297 				// us
298 				if ((delta > 0 || added > 0) && section_num <= old_sections)
299 				{
300 					add_signal_to_queue(menu,
301 					                    signal_queue,
302 					                    section_num - 1,
303 					                    change_pos < 0 ? place + 1 : change_pos,
304 					                    MAX(0, delta),
305 					                    MAX(0, added));
306 				}
307 				// Update current_section and reset current_iter and added to new
308 				// section
309 				current_iter = g_sequence_iter_next(old_iter);
310 				added        = 0;
311 				change_pos   = -1;
312 				place        = 0;
313 			}
314 			// If section was invalid, just free received item.
315 			else
316 				dbus_menu_item_free(new_item);
317 		}
318 		else if (!dbus_menu_item_is_firefox_stub(new_item))
319 		{
320 			new_item->section_num   = section_num;
321 			new_item->place         = place;
322 			GSequenceIter *old_iter = g_sequence_lookup(menu->items,
323 			                                            new_item,
324 			                                            dbus_menu_model_sort_func,
325 			                                            NULL);
326 			// There is no old item on this place
327 			if (!old_iter)
328 			{
329 				if (!added)
330 					change_pos = change_pos < 0 ? place : change_pos;
331 				menu_item_copy_and_load(menu, NULL, new_item);
332 				current_iter = g_sequence_insert_sorted(menu->items,
333 				                                        new_item,
334 				                                        dbus_menu_model_sort_func,
335 				                                        NULL);
336 				added++;
337 			}
338 			// If there is an old item exists, we need to check this properties
339 			else
340 			{
341 				old = (DBusMenuItem *)g_sequence_get(old_iter);
342 				// We should compare properties of old and new item
343 				bool diff    = !dbus_menu_item_compare_immutable(old, new_item);
344 				bool updated = dbus_menu_item_update_props(old, cprops);
345 				if (diff)
346 				{
347 					// Immutable properties was different, replace menu item
348 					menu_item_copy_and_load(menu, old, new_item);
349 					g_sequence_remove(old_iter);
350 					current_iter =
351 					    g_sequence_insert_sorted(menu->items,
352 					                             new_item,
353 					                             dbus_menu_model_sort_func,
354 					                             NULL);
355 				}
356 				else
357 				{
358 					// Just free unneeded item
359 					dbus_menu_item_free(new_item);
360 				}
361 				// If item was updated - add a signal to queue about it, but only if
362 				// section was in old layout
363 				if ((diff || updated) && section_num < old_sections)
364 				{
365 					add_signal_to_queue(menu,
366 					                    signal_queue,
367 					                    section_num,
368 					                    place,
369 					                    1,
370 					                    1);
371 				}
372 			}
373 			current_iter = g_sequence_iter_next(current_iter);
374 			place++;
375 		}
376 		else
377 			// Just free unnedede item
378 			dbus_menu_item_free(new_item);
379 		g_variant_unref(cprops);
380 		g_variant_unref(value);
381 		g_variant_unref(child);
382 	}
383 	section_num++;
384 	int secdiff = old_sections - section_num;
385 	if (secdiff > 0)
386 	{
387 		GSequenceIter *section_iter = dbus_menu_model_find_place(menu, section_num, -1);
388 		g_sequence_remove_range(section_iter, g_sequence_get_end_iter(menu->items));
389 	}
390 	// We need to manage last section's changes. And check its validity
391 	if (secdiff >= 0)
392 	{
393 		place--;
394 		DBusMenuItem *old = (DBusMenuItem *)g_sequence_get(
395 		    g_sequence_iter_prev(g_sequence_get_end_iter(menu->items)));
396 		int delta                 = old->place - place;
397 		GSequenceIter *last_iter  = g_sequence_get_end_iter(menu->items);
398 		GSequenceIter *place_iter = g_sequence_iter_move(last_iter, -delta);
399 
400 		// Cleanup all items in prev section (if there is more items than
401 		// current)
402 		if (delta > 0)
403 			g_sequence_remove_range(place_iter, last_iter);
404 		// If section number is not changed, emit a signal about last section.
405 		// Because if we emit it and section will be a part of sections signal, this can
406 		// duplicate menu items
407 		if ((delta > 0 || added > 0))
408 		{
409 			add_signal_to_queue(menu,
410 			                    signal_queue,
411 			                    section_num - 1,
412 			                    change_pos < 0 ? place + 1 : change_pos,
413 			                    MAX(0, delta),
414 			                    MAX(0, added));
415 		}
416 	}
417 	// If sections was changed, add change signal to queue
418 	if (secdiff != 0)
419 	{
420 		add_signal_to_queue(menu,
421 		                    signal_queue,
422 		                    -1,
423 		                    MIN(old_sections, section_num),
424 		                    (secdiff) > 0 ? ABS(secdiff) : 0,
425 		                    (secdiff) < 0 ? ABS(secdiff) : 0);
426 	}
427 	g_variant_unref(items);
428 	// Emit all signals from queus by LIFO order
429 	queue_emit_all(signal_queue);
430 }
431 
get_layout_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)432 static void get_layout_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
433 {
434 	g_autoptr(GVariant) layout = NULL;
435 	guint revision;
436 	if (!DBUS_MENU_IS_MODEL(user_data))
437 		return;
438 	DBusMenuModel *menu     = DBUS_MENU_MODEL(user_data);
439 	g_autoptr(GError) error = NULL;
440 	dbus_menu_xml_call_get_layout_finish((DBusMenuXml *)(source_object),
441 	                                     &revision,
442 	                                     &layout,
443 	                                     res,
444 	                                     &error);
445 	if (error != NULL)
446 	{
447 		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
448 			g_warning("%s", error->message);
449 		return;
450 	}
451 	layout_parse(menu, layout);
452 	menu->layout_update_in_progress = false;
453 	if (menu->layout_update_required)
454 		dbus_menu_model_update_layout(menu);
455 }
456 
dbus_menu_update_item_properties_from_layout_sync(DBusMenuModel * menu,DBusMenuItem * item,int sect_n,int pos)457 static void dbus_menu_update_item_properties_from_layout_sync(DBusMenuModel *menu,
458                                                               DBusMenuItem *item, int sect_n,
459                                                               int pos)
460 {
461 	g_return_if_fail(DBUS_MENU_IS_MODEL(menu));
462 	g_autoptr(GVariant) props      = NULL;
463 	g_autoptr(GVariant) items      = NULL;
464 	g_autoptr(GVariant) layout     = NULL;
465 	g_autoptr(GError) error        = NULL;
466 	g_autoptr(GQueue) signal_queue = g_queue_new();
467 	guint id, revision;
468 	dbus_menu_xml_call_get_layout_sync(menu->xml,
469 	                                   item->id,
470 	                                   0,
471 	                                   property_names,
472 	                                   &revision,
473 	                                   &layout,
474 	                                   menu->cancellable,
475 	                                   &error);
476 	if (error != NULL)
477 	{
478 		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
479 			g_warning("%s", error->message);
480 		return;
481 	}
482 	g_variant_get(layout, "(i@a{sv}@av)", &id, &props, &items);
483 	bool is_item_updated = dbus_menu_item_update_props(item, props);
484 	if (is_item_updated)
485 		add_signal_to_queue(menu, signal_queue, sect_n, pos, 1, 1);
486 	queue_emit_all(signal_queue);
487 }
488 
dbus_menu_model_update_layout_sync(DBusMenuModel * menu)489 G_GNUC_INTERNAL void dbus_menu_model_update_layout_sync(DBusMenuModel *menu)
490 {
491 	g_return_if_fail(DBUS_MENU_IS_MODEL(menu));
492 	g_autoptr(GVariant) layout = NULL;
493 	g_autoptr(GError) error    = NULL;
494 	guint revision;
495 	if (menu->layout_update_in_progress)
496 		menu->layout_update_required = true;
497 	else
498 		dbus_menu_xml_call_get_layout_sync(menu->xml,
499 		                                   menu->parent_id,
500 		                                   1,
501 		                                   property_names,
502 		                                   &revision,
503 		                                   &layout,
504 		                                   menu->cancellable,
505 		                                   &error);
506 	if (error != NULL)
507 	{
508 		if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
509 			g_warning("%s", error->message);
510 		return;
511 	}
512 	layout_parse(menu, layout);
513 	menu->layout_update_in_progress = false;
514 	if (menu->layout_update_required)
515 		dbus_menu_model_update_layout_sync(menu);
516 }
517 
dbus_menu_model_update_layout(DBusMenuModel * menu)518 G_GNUC_INTERNAL void dbus_menu_model_update_layout(DBusMenuModel *menu)
519 {
520 	g_return_if_fail(DBUS_MENU_IS_MODEL(menu));
521 	if (menu->layout_update_in_progress)
522 		menu->layout_update_required = true;
523 	else
524 		dbus_menu_xml_call_get_layout(menu->xml,
525 		                              menu->parent_id,
526 		                              1,
527 		                              property_names,
528 		                              menu->cancellable,
529 		                              get_layout_cb,
530 		                              menu);
531 }
532 
layout_updated_cb(DBusMenuXml * proxy,guint revision,gint parent,DBusMenuModel * menu)533 static void layout_updated_cb(DBusMenuXml *proxy, guint revision, gint parent, DBusMenuModel *menu)
534 {
535 	if (!DBUS_MENU_IS_XML(proxy))
536 		return;
537 	if (((uint)parent == menu->parent_id) && menu->current_revision < revision)
538 	{
539 		g_debug("Remote attempt to update %u with rev %u\n", parent, revision);
540 		dbus_menu_model_update_layout(menu);
541 		menu->current_revision = revision;
542 		return;
543 	}
544 	DBusMenuItem *item = dbus_menu_model_find(menu, (uint)parent);
545 	if (item != NULL)
546 		dbus_menu_update_item_properties_from_layout_sync(menu,
547 		                                                  item,
548 		                                                  item->section_num,
549 		                                                  item->place);
550 }
551 
item_activation_requested_cb(DBusMenuXml * proxy,gint id,guint timestamp,DBusMenuModel * menu)552 static void item_activation_requested_cb(DBusMenuXml *proxy, gint id, guint timestamp,
553                                          DBusMenuModel *menu)
554 {
555 	if (!DBUS_MENU_IS_XML(proxy))
556 		return;
557 	g_autofree char *ordinary_name = g_strdup_printf(ACTION_PREFIX "%u", id);
558 	g_action_group_activate_action(menu->received_action_group, ordinary_name, NULL);
559 	g_debug("activation requested: id - %d, timestamp - %d", id, timestamp);
560 }
561 
items_properties_loop(DBusMenuModel * menu,GVariant * up_props,GQueue * signal_queue,bool is_removal)562 static void items_properties_loop(DBusMenuModel *menu, GVariant *up_props, GQueue *signal_queue,
563                                   bool is_removal)
564 {
565 	GVariantIter iter;
566 	guint id;
567 	GVariant *props;
568 	g_variant_iter_init(&iter, up_props);
569 	while (g_variant_iter_loop(&iter, !is_removal ? "(i@a{sv})" : "(i@as)", &id, &props))
570 	{
571 		DBusMenuItem *item   = (DBusMenuItem *)dbus_menu_model_find(menu, id);
572 		bool is_item_updated = false;
573 		if (item != NULL)
574 		{
575 			// It is the best what we can do to update a section
576 			if (item->action_type == DBUS_MENU_ACTION_SECTION)
577 			{
578 				//                            dbus_menu_model_update_layout(menu);
579 			}
580 			else
581 			{
582 				is_item_updated = !is_removal
583 				                      ? dbus_menu_item_update_props(item, props)
584 				                      : dbus_menu_item_remove_props(item, props);
585 				if (is_item_updated)
586 					add_signal_to_queue(menu,
587 					                    signal_queue,
588 					                    item->section_num,
589 					                    item->place,
590 					                    1,
591 					                    1);
592 			}
593 		}
594 	}
595 }
596 
items_properties_updated_cb(DBusMenuXml * proxy,GVariant * updated_props,GVariant * removed_props,DBusMenuModel * menu)597 static void items_properties_updated_cb(DBusMenuXml *proxy, GVariant *updated_props,
598                                         GVariant *removed_props, DBusMenuModel *menu)
599 {
600 	if (!DBUS_MENU_IS_XML(proxy))
601 		return;
602 	if (menu->layout_update_in_progress == true)
603 		return;
604 	g_autoptr(GQueue) signal_queue = g_queue_new();
605 	items_properties_loop(menu, updated_props, signal_queue, false);
606 	items_properties_loop(menu, removed_props, signal_queue, true);
607 	queue_emit_all(signal_queue);
608 }
609 
on_xml_property_changed(DBusMenuModel * model)610 static void on_xml_property_changed(DBusMenuModel *model)
611 {
612 	if (!DBUS_MENU_IS_XML(model->xml))
613 		return;
614 	g_signal_connect(model->xml,
615 	                 "items-properties-updated",
616 	                 G_CALLBACK(items_properties_updated_cb),
617 	                 model);
618 	g_signal_connect(model->xml, "layout-updated", G_CALLBACK(layout_updated_cb), model);
619 	g_signal_connect(model->xml,
620 	                 "item-activation-requested",
621 	                 G_CALLBACK(item_activation_requested_cb),
622 	                 model);
623 	if (model->parent_id == 0)
624 		dbus_menu_model_update_layout(model);
625 }
626 
dbus_menu_model_new(uint parent_id,DBusMenuModel * parent,DBusMenuXml * xml,GActionGroup * action_group)627 G_GNUC_INTERNAL DBusMenuModel *dbus_menu_model_new(uint parent_id, DBusMenuModel *parent,
628                                                    DBusMenuXml *xml, GActionGroup *action_group)
629 {
630 	DBusMenuModel *ret = (DBusMenuModel *)g_object_new(dbus_menu_model_get_type(),
631 	                                                   "parent-id",
632 	                                                   parent_id,
633 	                                                   "xml",
634 	                                                   xml,
635 	                                                   "action-group",
636 	                                                   action_group,
637 	                                                   NULL);
638 	if (parent != NULL)
639 		g_object_bind_property(parent, "xml", ret, "xml", G_BINDING_SYNC_CREATE);
640 	return ret;
641 }
642 
dbus_menu_model_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)643 static void dbus_menu_model_set_property(GObject *object, guint property_id, const GValue *value,
644                                          GParamSpec *pspec)
645 {
646 	DBusMenuModel *menu = (DBusMenuModel *)(object);
647 	void *old_xml       = menu->xml;
648 
649 	switch (property_id)
650 	{
651 	case PROP_XML:
652 		menu->xml = DBUS_MENU_XML(g_value_get_object(value));
653 		if (menu->xml != NULL && old_xml != menu->xml)
654 		{
655 			if (old_xml != NULL)
656 				g_signal_handlers_disconnect_by_data(old_xml, menu);
657 			on_xml_property_changed(menu);
658 		}
659 		break;
660 	case PROP_ACTION_GROUP:
661 		menu->received_action_group = G_ACTION_GROUP(g_value_get_object(value));
662 		break;
663 	case PROP_PARENT_ID:
664 		menu->layout_update_required = true;
665 		menu->parent_id              = g_value_get_uint(value);
666 		break;
667 	default:
668 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
669 		break;
670 	}
671 }
672 
dbus_menu_model_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)673 static void dbus_menu_model_get_property(GObject *object, guint property_id, GValue *value,
674                                          GParamSpec *pspec)
675 {
676 	DBusMenuModel *menu;
677 
678 	menu = (DBusMenuModel *)(object);
679 
680 	switch (property_id)
681 	{
682 	case PROP_XML:
683 		g_value_set_object(value, menu->xml);
684 		break;
685 	case PROP_PARENT_ID:
686 		g_value_set_uint(value, menu->parent_id);
687 		break;
688 	case PROP_ACTION_GROUP:
689 		g_value_set_object(value, menu->received_action_group);
690 		break;
691 	default:
692 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
693 		break;
694 	}
695 }
696 
dbus_menu_model_is_layout_update_required(DBusMenuModel * model)697 G_GNUC_INTERNAL bool dbus_menu_model_is_layout_update_required(DBusMenuModel *model)
698 {
699 	return model->layout_update_required;
700 }
701 
dbus_menu_model_set_layout_update_required(DBusMenuModel * model,bool required)702 G_GNUC_INTERNAL void dbus_menu_model_set_layout_update_required(DBusMenuModel *model, bool required)
703 {
704 	model->layout_update_required = required;
705 }
706 
dbus_menu_model_find(DBusMenuModel * menu,uint item_id)707 static DBusMenuItem *dbus_menu_model_find(DBusMenuModel *menu, uint item_id)
708 {
709 	for (GSequenceIter *iter = g_sequence_get_begin_iter(menu->items);
710 	     !g_sequence_iter_is_end(iter);
711 	     iter = g_sequence_iter_next(iter))
712 	{
713 		DBusMenuItem *item = (DBusMenuItem *)g_sequence_get(iter);
714 		if (item->id == item_id)
715 			return item;
716 	}
717 	return NULL;
718 }
719 
dbus_menu_model_find_place(DBusMenuModel * menu,uint section_num,int place)720 static GSequenceIter *dbus_menu_model_find_place(DBusMenuModel *menu, uint section_num, int place)
721 {
722 	for (GSequenceIter *iter = g_sequence_get_begin_iter(menu->items);
723 	     !g_sequence_iter_is_end(iter);
724 	     iter = g_sequence_iter_next(iter))
725 	{
726 		DBusMenuItem *item = (DBusMenuItem *)g_sequence_get(iter);
727 		if (item->section_num == section_num && item->place == place)
728 			return iter;
729 	}
730 	return NULL;
731 }
732 
dbus_menu_model_find_section(DBusMenuModel * menu,uint section_num)733 static DBusMenuItem *dbus_menu_model_find_section(DBusMenuModel *menu, uint section_num)
734 {
735 	return (DBusMenuItem *)g_sequence_get(dbus_menu_model_find_place(menu, section_num, -1));
736 }
737 
dbus_menu_model_init(DBusMenuModel * menu)738 static void dbus_menu_model_init(DBusMenuModel *menu)
739 {
740 	menu->cancellable               = g_cancellable_new();
741 	menu->parent_id                 = UINT_MAX;
742 	menu->items                     = g_sequence_new(dbus_menu_item_free);
743 	menu->layout_update_required    = true;
744 	menu->layout_update_in_progress = false;
745 	menu->current_revision          = 0;
746 }
747 
dbus_menu_model_constructed(GObject * object)748 static void dbus_menu_model_constructed(GObject *object)
749 {
750 	G_OBJECT_CLASS(dbus_menu_model_parent_class)->constructed(object);
751 	DBusMenuModel *menu = DBUS_MENU_MODEL(object);
752 
753 	DBusMenuItem *first_section =
754 	    dbus_menu_item_new_first_section(menu->parent_id, menu->received_action_group);
755 	first_section->section_num = 0;
756 	first_section->place       = -1;
757 	g_hash_table_insert(first_section->links,
758 	                    G_MENU_LINK_SECTION,
759 	                    dbus_menu_section_model_new(menu, 0));
760 	g_sequence_insert_sorted(menu->items, first_section, dbus_menu_model_sort_func, NULL);
761 }
762 
dbus_menu_model_finalize(GObject * object)763 static void dbus_menu_model_finalize(GObject *object)
764 {
765 	DBusMenuModel *menu = (DBusMenuModel *)(object);
766 	if (menu->xml)
767 		g_signal_handlers_disconnect_by_data(menu->xml, menu);
768 	g_cancellable_cancel(menu->cancellable);
769 	g_clear_object(&menu->cancellable);
770 	g_clear_pointer(&menu->items, g_sequence_free);
771 
772 	G_OBJECT_CLASS(dbus_menu_model_parent_class)->finalize(object);
773 }
774 
install_properties(GObjectClass * object_class)775 static void install_properties(GObjectClass *object_class)
776 {
777 	properties[PROP_XML] =
778 	    g_param_spec_object("xml",
779 	                        "xml",
780 	                        "xml",
781 	                        dbus_menu_xml_get_type(),
782 	                        (GParamFlags)(G_PARAM_CONSTRUCT | G_PARAM_READABLE |
783 	                                      G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
784 
785 	properties[PROP_ACTION_GROUP] =
786 	    g_param_spec_object("action-group",
787 	                        "action-group",
788 	                        "action-group",
789 	                        g_action_group_get_type(),
790 	                        (GParamFlags)(G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
791 	                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
792 	properties[PROP_PARENT_ID] =
793 	    g_param_spec_uint("parent-id",
794 	                      "parent-id",
795 	                      "parent-id",
796 	                      0,
797 	                      UINT_MAX,
798 	                      0,
799 	                      (GParamFlags)(G_PARAM_CONSTRUCT | G_PARAM_WRITABLE |
800 	                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
801 
802 	g_object_class_install_properties(object_class, NUM_PROPS, properties);
803 }
804 
dbus_menu_model_class_init(DBusMenuModelClass * klass)805 static void dbus_menu_model_class_init(DBusMenuModelClass *klass)
806 {
807 	GMenuModelClass *model_class = G_MENU_MODEL_CLASS(klass);
808 	GObjectClass *object_class   = G_OBJECT_CLASS(klass);
809 
810 	object_class->finalize     = dbus_menu_model_finalize;
811 	object_class->set_property = dbus_menu_model_set_property;
812 	object_class->get_property = dbus_menu_model_get_property;
813 	object_class->constructed  = dbus_menu_model_constructed;
814 
815 	model_class->is_mutable          = dbus_menu_model_is_mutable;
816 	model_class->get_n_items         = dbus_menu_model_get_n_items;
817 	model_class->get_item_attributes = dbus_menu_model_get_item_attributes;
818 	model_class->get_item_links      = dbus_menu_model_get_item_links;
819 	install_properties(object_class);
820 }
821