From 6c22327529e6aa031ff1cd85a1536cdf808a9296 Mon Sep 17 00:00:00 2001 From: Rory Fewell Date: Thu, 17 Apr 2025 12:40:58 +0100 Subject: [PATCH] Enhancement: Fixes #445, taskband - Start menu - All Programs needs to handle added/removed desktop entries --- .gitignore | 2 + packaging/cmake-inc/codegen/CMakeLists.txt | 42 + private/play/menutest/src/window.c | 200 ++- shared/comctl/src/menubind.c | 278 +++- shared/comgtk/public/container.h | 12 + shared/comgtk/public/memory.h | 34 + shared/comgtk/public/strings.h | 30 + shared/comgtk/src/container.c | 15 + shared/comgtk/src/memory.c | 15 + shared/comgtk/src/strings.c | 59 + shared/shcommon/CMakeLists.txt | 9 + shared/shcommon/public/fs.h | 2 + shared/shcommon/public/libapi.h.in | 1 + shared/shcommon/public/monitor.h | 31 + shared/shcommon/src/marshals.list | 1 + shared/shcommon/src/monitor.c | 537 +++++++ shell/taskband/src/start/personal.c | 12 +- shell/taskband/src/start/progmenu.c | 1659 ++++++++++++++++---- shell/taskband/src/start/progmenu.h | 20 +- shell/taskband/src/start/shared.h | 4 + shell/taskband/src/start/toolbar.c | 9 + shell/taskband/src/systray/power.c | 2 + 22 files changed, 2650 insertions(+), 324 deletions(-) create mode 100644 shared/shcommon/public/monitor.h create mode 100644 shared/shcommon/src/marshals.list create mode 100644 shared/shcommon/src/monitor.c diff --git a/.gitignore b/.gitignore index b63aab3..d7e1f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ mui-stuff/ [Bb]uild/ config.h libapi.h +marshal.c +marshal.h meta.h resources.c *.deb diff --git a/packaging/cmake-inc/codegen/CMakeLists.txt b/packaging/cmake-inc/codegen/CMakeLists.txt index 4c10df9..d019af8 100644 --- a/packaging/cmake-inc/codegen/CMakeLists.txt +++ b/packaging/cmake-inc/codegen/CMakeLists.txt @@ -53,3 +53,45 @@ function(wintc_gdbus_codegen XML_FILE OUT_FILE_NOEXT CP_PUBLIC) ) endif() endfunction() + +# Define function for glib-genmashal +# +function(wintc_glib_genmarshal) + find_program(GLIB_GENMARSHAL glib-genmarshal REQUIRED) + + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_SOURCE_DIR}/src/marshal.h + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src + COMMAND ${GLIB_GENMARSHAL} + ARGS + --header + --prefix wintc_cclosure_marshal + --output marshal.h + marshals.list + VERBATIM + DEPENDS + src/marshals.list + ) + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_SOURCE_DIR}/src/marshal.c + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src + COMMAND ${GLIB_GENMARSHAL} + ARGS + --body + --prefix wintc_cclosure_marshal + --output marshal.c + marshals.list + VERBATIM + DEPENDS + src/marshals.list + ) + + add_custom_target( + marshal-codegen + DEPENDS + src/marshal.c + src/marshal.h + ) +endfunction() diff --git a/private/play/menutest/src/window.c b/private/play/menutest/src/window.c index f8c005b..7c6c0a8 100644 --- a/private/play/menutest/src/window.c +++ b/private/play/menutest/src/window.c @@ -8,7 +8,7 @@ // Change this to use either our binding or GTK // -#define TEST_USE_CTL_BINDING 1 +#define TEST_USE_CTL_BINDING 0 // // FORWARD DECLARATIONS @@ -17,6 +17,20 @@ static void wintc_menu_test_window_dispose( GObject* object ); +static gint wintc_menu_test_window_find_item_index( + GMenuModel* model, + GMenuModel** target_model +); + +static void on_button_add_clicked( + GtkButton* button, + gpointer user_data +); +static void on_button_del_clicked( + GtkButton* button, + gpointer user_data +); + // // GTK OOP CLASS/INSTANCE DEFINITIONS // @@ -100,6 +114,43 @@ static void wintc_menu_test_window_init( gtk_container_add(GTK_CONTAINER(box_container), menu_bar); + // Add a strip for buttons for doing random additions/deletions + // + GtkWidget* box_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + GtkWidget* button_add = gtk_button_new_with_label("Random add"); + GtkWidget* button_del = gtk_button_new_with_label("Random del"); + + gtk_box_pack_start( + GTK_BOX(box_buttons), + button_add, + FALSE, + FALSE, + 0 + ); + gtk_box_pack_start( + GTK_BOX(box_buttons), + button_del, + FALSE, + FALSE, + 0 + ); + + gtk_container_add(GTK_CONTAINER(box_container), box_buttons); + + g_signal_connect( + button_add, + "clicked", + G_CALLBACK(on_button_add_clicked), + model + ); + g_signal_connect( + button_del, + "clicked", + G_CALLBACK(on_button_del_clicked), + model + ); + // Show! // gtk_container_add(GTK_CONTAINER(self), box_container); @@ -140,3 +191,150 @@ GtkWidget* wintc_menu_test_window_new( ) ); } + +// +// PRIVATE FUNCTIONS +// +static gint wintc_menu_test_window_find_item_index( + GMenuModel* model, + GMenuModel** target_model +) +{ + // We receive a top level model (the menu bar) - pick a menu to work on + // + gint n_items = g_menu_model_get_n_items(model); + + GMenuModel* menu = + g_menu_model_get_item_link( + model, + g_random_int_range(0, n_items), + G_MENU_LINK_SUBMENU + ); + + // Find an item + // + gint idx_rand; + GMenuModel* model_current = menu; + + while (TRUE) + { + n_items = g_menu_model_get_n_items(model_current); + + // 0 items? It's over + // + if (!n_items) + { + *target_model = model_current; + return 0; + } + + // Wahey we have items, let's pick one + // + idx_rand = g_random_int_range(0, n_items); + + // If we hit a submenu, enter it + // + GMenuModel* model_submenu = + g_menu_model_get_item_link( + model_current, + idx_rand, + G_MENU_LINK_SUBMENU + ); + + if (model_submenu) + { + model_current = model_submenu; + continue; + } + + // Otherwise return this index + // + *target_model = model_current; + return idx_rand; + } +} + +// +// CALLBACKS +// +static void on_button_add_clicked( + WINTC_UNUSED(GtkButton* button), + gpointer user_data +) +{ + GMenuModel* model = G_MENU_MODEL(user_data); + + gint target_idx; + GMenuModel* target_model = NULL; + + target_idx = + wintc_menu_test_window_find_item_index(model, &target_model); + + // Create and add the menu item + // + GMenuItem* menu_item = g_menu_item_new("A random item.", NULL); + + g_menu_item_set_icon( + menu_item, + g_themed_icon_new("emblem-favorite") + ); + + g_menu_insert_item(G_MENU(target_model), target_idx, menu_item); + + g_object_unref(menu_item); +} + +static void on_button_del_clicked( + WINTC_UNUSED(GtkButton* button), + gpointer user_data +) +{ + GMenuModel* model = G_MENU_MODEL(user_data); + + gint target_idx; + GMenuModel* target_model = NULL; + + // We shouldn't delete section items... this is really rubbish but I'm also + // REALLY lazy so just try a few times to find a normal item otherwise give + // up + // + gint attempts = 0; + + while (attempts < 3) + { + attempts++; + + target_idx = + wintc_menu_test_window_find_item_index(model, &target_model); + + // Skip if this item is a section + // + if ( + g_menu_model_get_item_link( + target_model, + target_idx, + G_MENU_LINK_SECTION + ) + ) + { + continue; + } + + // Don't delete if there's actually no items in this model + // + if (!g_menu_model_get_n_items(target_model)) + { + continue; + } + + // All good, bin the item + // + g_menu_remove(G_MENU(target_model), target_idx); + return; + } + + g_message( + "%s", + "Didn't land on a normal item to bin, better luck next time." + ); +} diff --git a/shared/comctl/src/menubind.c b/shared/comctl/src/menubind.c index 5877385..8056b7a 100644 --- a/shared/comctl/src/menubind.c +++ b/shared/comctl/src/menubind.c @@ -24,6 +24,8 @@ typedef struct _WinTCCtlMenuBindingMenu GMenuModel* menu_model; GList* sections; + + gulong sigid_items_changed; } WinTCCtlMenuBindingMenu; typedef struct _WinTCCtlMenuBindingSection @@ -70,6 +72,11 @@ static void wintc_ctl_menu_binding_insert_item( gint src_pos, gint dst_pos ); +static void wintc_ctl_menu_binding_remove_item( + WinTCCtlMenuBindingMenu* menu, + GMenuModel* menu_model, + gint src_pos +); static void wintc_ctl_menu_binding_track_menu( WinTCCtlMenuBinding* menu_binding, GtkMenuShell* menu_shell, @@ -80,6 +87,14 @@ static void wintc_ctl_menu_binding_menu_free( WinTCCtlMenuBindingMenu* menu ); +static void on_menu_model_menu_items_changed( + GMenuModel* model, + gint position, + gint removed, + gint added, + gpointer user_data +); + // // GTK OOP CLASS/INSTANCE DEFINITIONS // @@ -481,6 +496,11 @@ static void wintc_ctl_menu_binding_insert_item( gtk_widget_set_margin_end(img_icon, 6); // Got this from Mousepad gtk_widget_set_size_request(img_icon, 16, 16); + gtk_menu_item_set_reserve_indicator( + GTK_MENU_ITEM(menu_item), + TRUE + ); + if (icon) { gtk_image_set_from_gicon( @@ -537,6 +557,8 @@ static void wintc_ctl_menu_binding_insert_item( g_free(label); } + gtk_widget_show_all(menu_item); + // Do we have a submenu for this item? // GMenuModel* submenu_model = @@ -645,7 +667,7 @@ static void wintc_ctl_menu_binding_insert_item( // gint i = 0; - while (i < dst_pos) + while (i < dst_pos && iter) { WinTCCtlMenuBindingSection* subsection = (WinTCCtlMenuBindingSection*) iter->data; @@ -661,13 +683,6 @@ static void wintc_ctl_menu_binding_insert_item( break; } - // No where else to go? - // - if (!(iter->next)) - { - break; - } - // Moving on... // iter = iter->next; @@ -683,10 +698,55 @@ static void wintc_ctl_menu_binding_insert_item( } } + // If we found where to insert, then get that sorted + // + if (iter) + { + WinTCCtlMenuBindingSection* found_section = + (WinTCCtlMenuBindingSection*) iter->data; + + // If this is a real section, look for a fake section before it to + // insert into instead + // + if (found_section->menu_model && iter->prev) + { + WinTCCtlMenuBindingSection* prev_section = + (WinTCCtlMenuBindingSection*) iter->prev->data; + + if (!(prev_section->menu_model)) + { + found_section = prev_section; + } + } + + // Do we have a fake section? If not, create a new one before the real + // section + // + if (found_section->menu_model) + { + found_section = g_new(WinTCCtlMenuBindingSection, 1); + + found_section->parent_menu = menu; + found_section->menu_model = NULL; + found_section->item_count = 0; // Will be incremented in a moment + + menu->sections = + g_list_insert_before( + menu->sections, + iter, + found_section + ); + } + + found_section->item_count++; + gtk_menu_shell_insert(menu->menu_shell, menu_item, real_pos); + return; + } + // If we fell out, we need to add the item to the end of the menu // WinTCCtlMenuBindingSection* last_section = - (WinTCCtlMenuBindingSection*) iter->data; + (WinTCCtlMenuBindingSection*) (g_list_last(menu->sections))->data; if (last_section->menu_model) { @@ -709,6 +769,158 @@ static void wintc_ctl_menu_binding_insert_item( gtk_menu_shell_append(menu->menu_shell, menu_item); } +static void wintc_ctl_menu_binding_remove_item( + WinTCCtlMenuBindingMenu* menu, + GMenuModel* menu_model, + gint src_pos +) +{ + gboolean is_subsection = menu_model != menu->menu_model; + + // Let's fine the position of the item to bin! + // + gint real_pos = 0; + + if (is_subsection) + { + for (GList* iter = menu->sections; iter; iter = iter->next) + { + WinTCCtlMenuBindingSection* check_section = + (WinTCCtlMenuBindingSection*) iter->data; + + if (!(check_section->menu_model)) + { + real_pos += check_section->item_count; + continue; + } + + if (check_section->menu_model == menu_model) + { + real_pos += src_pos; + + check_section->item_count--; + + gtk_widget_destroy( + wintc_container_get_nth_child( + GTK_CONTAINER(menu->menu_shell), + real_pos + ) + ); + + // Handle when we just destroyed the last item in a section + // + if (!(check_section->item_count)) + { + menu->sections = + g_list_delete_link( + menu->sections, + iter + ); + } + + return; + } + } + } + else + { + gint i = 0; + + for (GList* iter = menu->sections; iter; iter = iter->next) + { + WinTCCtlMenuBindingSection* check_section = + (WinTCCtlMenuBindingSection*) iter->data; + + if (i == src_pos) + { + // If the item referred to is the section itself, we need to + // delete the entire section + // + if (check_section->menu_model) + { + for (gint j = 0; j < check_section->item_count; j++) + { + gtk_widget_destroy( + wintc_container_get_nth_child( + GTK_CONTAINER(menu->menu_shell), + real_pos + ) + ); + } + + menu->sections = + g_list_delete_link( + menu->sections, + iter + ); + + return; + } + else + { + check_section->item_count--; + + gtk_widget_destroy( + wintc_container_get_nth_child( + GTK_CONTAINER(menu->menu_shell), + real_pos + ) + ); + + if (!(check_section->item_count)) + { + menu->sections = + g_list_delete_link( + menu->sections, + iter + ); + } + } + + return; + } + + // Keep looking... + // + // If this section is real, then no fancy stuff is required - if + // it's a 'fake' section then we need to check whether src_pos is + // inside it + // + if (check_section->menu_model) + { + i++; + real_pos += check_section->item_count; + } + else + { + if (i + check_section->item_count > src_pos) + { + real_pos += src_pos - i; + + check_section->item_count--; + + gtk_widget_destroy( + wintc_container_get_nth_child( + GTK_CONTAINER(menu->menu_shell), + real_pos + ) + ); + + return; + } + + i += check_section->item_count; + real_pos += check_section->item_count; + } + } + } + + g_critical( + "%s", + "comctl - menu binding - somehow failed to find menu item to delete?" + ); +} + static void wintc_ctl_menu_binding_track_menu( WinTCCtlMenuBinding* menu_binding, GtkMenuShell* menu_shell, @@ -725,6 +937,19 @@ static void wintc_ctl_menu_binding_track_menu( tracker->menu_model = menu_model; tracker->sections = NULL; + WINTC_LOG_DEBUG( + "comctl - menu binding - new menu tracker: %p", + (void*) tracker + ); + + tracker->sigid_items_changed = + g_signal_connect( + menu_model, + "items-changed", + G_CALLBACK(on_menu_model_menu_items_changed), + tracker + ); + menu_binding->tracked_menus = g_slist_append( menu_binding->tracked_menus, @@ -753,3 +978,38 @@ static void wintc_ctl_menu_binding_menu_free( g_clear_list(&(menu->sections), (GDestroyNotify) g_free); g_free(menu); } + +// +// CALLBACKS +// +static void on_menu_model_menu_items_changed( + GMenuModel* model, + gint position, + gint removed, + gint added, + gpointer user_data +) +{ + WinTCCtlMenuBindingMenu* menu = (WinTCCtlMenuBindingMenu*) user_data; + + WINTC_LOG_DEBUG( + "comctl - menubind - update pos %d, remove %d, add %d", + position, + removed, + added + ); + + // Removed items + // + for (gint i = position; i < position + removed; i++) + { + wintc_ctl_menu_binding_remove_item(menu, model, i); + } + + // Added items + // + for (gint i = position; i < position + added; i++) + { + wintc_ctl_menu_binding_insert_item(menu, model, i, i); + } +} diff --git a/shared/comgtk/public/container.h b/shared/comgtk/public/container.h index 1870c72..31e8d99 100644 --- a/shared/comgtk/public/container.h +++ b/shared/comgtk/public/container.h @@ -18,4 +18,16 @@ void wintc_container_clear( GtkContainer* container ); +/** + * Gets the nth child widget of a container. + * + * @param container The container. + * @param pos The index of the widget. + * @return The child widget at position N in the container. + */ +GtkWidget* wintc_container_get_nth_child( + GtkContainer* container, + gint pos +); + #endif diff --git a/shared/comgtk/public/memory.h b/shared/comgtk/public/memory.h index e349eaf..7ddfbef 100644 --- a/shared/comgtk/public/memory.h +++ b/shared/comgtk/public/memory.h @@ -1,3 +1,5 @@ +/** @file */ + #ifndef __COMGTK_MEMORY_H__ #define __COMGTK_MEMORY_H__ @@ -6,15 +8,47 @@ // // PUBLIC FUNCTIONS // + +/** + * Frees a null-terminated array - the provided function will be called to free + * each of the array elements. + * + * @param mem The array. + * @param destroy The function for freeing an individual element. + */ void wintc_freev( gpointer mem, GDestroyNotify destroy ); +/** + * Frees an array of the specified size - the provided function will be called + * to free each of the array elements. + * + * @param mem The array. + * @param n_elements The number of elements in the array. + * @param destroy The function for freeing an individual element. + */ void wintc_freenv( gpointer mem, guint n_elements, GDestroyNotify destroy ); +/** + * Convenience function for using memcpy where potentially the destination + * buffer is NULL, in which case nothing will be done. + * + * @param dst_buf The reference to the buffer to copy into. + * @param offset The offset into the destination buffer to copy at. + * @param src_buf The source buffer. + * @param n The number of bytes to be copied. + */ +void wintc_memcpy_ref( + void* dst_buf, + glong offset, + const void* src_buf, + size_t n +); + #endif diff --git a/shared/comgtk/public/strings.h b/shared/comgtk/public/strings.h index 65bb209..deef8a9 100644 --- a/shared/comgtk/public/strings.h +++ b/shared/comgtk/public/strings.h @@ -69,6 +69,24 @@ gchar* wintc_str_set_suffix( const gchar* suffix ); +/** + * Duplicates part of a string from the start and ending at the first + * occurrence of the specified delimiter (exclusive, so the delimiter will not + * be included). + * + * @param str The string. + * @param len The maximum length of str to use, -1 for null terminated string. + * @param c The character to look for. + * @param pos If not NULL, a storage location for position after the delimiter. + * @return The new string, a complete copy of the string if c wasn't found. + */ +gchar* wintc_strdup_nextchr( + const gchar* str, + gssize len, + gunichar c, + const gchar** pos +); + /** * Duplicates a string and replaces the string at the destination with it - if * there was a string at the destination pointer, it will be freed. @@ -130,4 +148,16 @@ guint wintc_strv_length( const gchar** str_array ); +/** + * Extracts a substring from one string to create a new string. + * + * @param start A pointer to the start of the substring. + * @param end A pointer to the end of the substring. + * @return A copy of the substring. + */ +gchar* wintc_substr( + const gchar* start, + const gchar* end +); + #endif diff --git a/shared/comgtk/src/container.c b/shared/comgtk/src/container.c index ad34ad9..4208c09 100644 --- a/shared/comgtk/src/container.c +++ b/shared/comgtk/src/container.c @@ -22,3 +22,18 @@ void wintc_container_clear( g_list_free(children); } + +GtkWidget* wintc_container_get_nth_child( + GtkContainer* container, + gint pos +) +{ + GList* children = gtk_container_get_children(container); + + GtkWidget* nth_child = + GTK_WIDGET(g_list_nth_data(children, pos)); + + g_list_free(children); + + return nth_child; +} diff --git a/shared/comgtk/src/memory.c b/shared/comgtk/src/memory.c index c551dcf..4e378c2 100644 --- a/shared/comgtk/src/memory.c +++ b/shared/comgtk/src/memory.c @@ -35,3 +35,18 @@ void wintc_freenv( g_free(mem); } + +void wintc_memcpy_ref( + void* dst_buf, + glong offset, + const void* src_buf, + size_t n +) +{ + if (!dst_buf) + { + return; + } + + memcpy(dst_buf + offset, src_buf, n); +} diff --git a/shared/comgtk/src/strings.c b/shared/comgtk/src/strings.c index 9e9a205..23784d4 100644 --- a/shared/comgtk/src/strings.c +++ b/shared/comgtk/src/strings.c @@ -1,6 +1,7 @@ #include #include +#include "../public/shorthand.h" #include "../public/strings.h" // @@ -59,6 +60,43 @@ gchar* wintc_str_set_suffix( } } +gchar* wintc_strdup_nextchr( + const gchar* str, + gssize len, + gunichar c, + const gchar** pos +) +{ + const gchar* end; + + if (!str) + { + WINTC_SAFE_REF_SET(pos, NULL); + return NULL; + } + + end = g_utf8_strchr(str, len, c); + + if (pos) + { + WINTC_SAFE_REF_SET(pos, end ? end + 1 : NULL); + } + + if (!end) + { + return g_strdup(str); + } + + // Allocate new string + // + gint diff = end - str; + gchar* buf = g_malloc0(diff + 1); + + memcpy(buf, str, diff); + + return buf; +} + void wintc_strdup_replace( gchar** dest, const gchar* src @@ -180,3 +218,24 @@ guint wintc_strv_length( return i; } + +gchar* wintc_substr( + const gchar* start, + const gchar* end +) +{ + if (end < start) + { + g_critical("substr: invalid substring requested"); + return NULL; + } + + gchar* buf = g_malloc0(end - start + 1); + + if (start != end) + { + memcpy(buf, start, end - start); + } + + return buf; +} diff --git a/shared/shcommon/CMakeLists.txt b/shared/shcommon/CMakeLists.txt index f12505e..8f68ddd 100644 --- a/shared/shcommon/CMakeLists.txt +++ b/shared/shcommon/CMakeLists.txt @@ -13,9 +13,12 @@ set(PROJECT_MAINTAINER "Rory Fewell ") set(PROJECT_ROOT ${CMAKE_CURRENT_LIST_DIR}) +set(WINTC_NO_PEDANTIC_COMPILE true) # Necessary for glib-genmarshal + include(GNUInstallDirs) include(../../packaging/cmake-inc/common/CMakeLists.txt) +include(../../packaging/cmake-inc/codegen/CMakeLists.txt) include(../../packaging/cmake-inc/libraries/CMakeLists.txt) include(../../packaging/cmake-inc/linking/CMakeLists.txt) include(../../packaging/cmake-inc/packaging/CMakeLists.txt) @@ -24,10 +27,16 @@ wintc_resolve_library(glib-2.0 GLIB) wintc_resolve_library(gtk+-3.0 GTK3) wintc_resolve_library(wintc-comgtk WINTC_COMGTK) +wintc_glib_genmarshal() + add_library( libwintc-shcommon src/fs.c public/fs.h + src/marshal.c + src/marshal.h + src/monitor.c + public/monitor.h src/path.c public/path.h src/places.c diff --git a/shared/shcommon/public/fs.h b/shared/shcommon/public/fs.h index d916b3a..2e87b5e 100644 --- a/shared/shcommon/public/fs.h +++ b/shared/shcommon/public/fs.h @@ -1,6 +1,8 @@ #ifndef __SHCOMMON_FS_H__ #define __SHCOMMON_FS_H__ +#include + // // PUBLIC FUNCTIONS // diff --git a/shared/shcommon/public/libapi.h.in b/shared/shcommon/public/libapi.h.in index 2272b38..a872686 100644 --- a/shared/shcommon/public/libapi.h.in +++ b/shared/shcommon/public/libapi.h.in @@ -2,6 +2,7 @@ #define __WINTC_SHCOMMON_H__ #include "@LIB_HEADER_DIR@/fs.h" +#include "@LIB_HEADER_DIR@/monitor.h" #include "@LIB_HEADER_DIR@/path.h" #include "@LIB_HEADER_DIR@/places.h" diff --git a/shared/shcommon/public/monitor.h b/shared/shcommon/public/monitor.h new file mode 100644 index 0000000..31fc389 --- /dev/null +++ b/shared/shcommon/public/monitor.h @@ -0,0 +1,31 @@ +#ifndef __SHCOMMON_MONITOR_H__ +#define __SHCOMMON_MONITOR_H__ + +#include +#include + +// +// GTK OOP BOILERPLATE +// +#define WINTC_TYPE_SH_DIR_MONITOR_RECURSIVE (wintc_sh_dir_monitor_recursive_get_type()) + +G_DECLARE_FINAL_TYPE( + WinTCShDirMonitorRecursive, + wintc_sh_dir_monitor_recursive, + WINTC, + SH_DIR_MONITOR_RECURSIVE, + GObject +) + + +// +// PUBLIC FUNCTIONS +// +WinTCShDirMonitorRecursive* wintc_sh_fs_monitor_directory_recursive( + GFile* file, + GFileMonitorFlags flags, + GCancellable* cancellable, + GError** error +); + +#endif diff --git a/shared/shcommon/src/marshals.list b/shared/shcommon/src/marshals.list new file mode 100644 index 0000000..b257897 --- /dev/null +++ b/shared/shcommon/src/marshals.list @@ -0,0 +1 @@ +VOID:OBJECT,OBJECT,INT diff --git a/shared/shcommon/src/monitor.c b/shared/shcommon/src/monitor.c new file mode 100644 index 0000000..a246bd5 --- /dev/null +++ b/shared/shcommon/src/monitor.c @@ -0,0 +1,537 @@ +#include +#include +#include + +#include "../public/fs.h" +#include "../public/monitor.h" +#include "marshal.h" + +// +// PRIVATE ENUMS +// +enum +{ + PROP_FILE = 1, + PROP_FILE_MONITOR +}; + +enum +{ + SIGNAL_CHANGED = 0, + N_SIGNALS +}; + +// +// FORWARD DECLARATIONS +// +static void wintc_sh_dir_monitor_recursive_constructed( + GObject* object +); +static void wintc_sh_dir_monitor_recursive_dispose( + GObject* object +); +static void wintc_sh_dir_monitor_recursive_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec +); +static void wintc_sh_dir_monitor_recursive_set_property( + GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec +); + +static void wintc_sh_dir_monitor_recursive_delete_monitor( + WinTCShDirMonitorRecursive* monitor_recursive, + GFile* file +); +static void wintc_sh_dir_monitor_recursive_new_monitor( + WinTCShDirMonitorRecursive* monitor_recursive, + GFile* file +); + +static void on_file_monitor_root_changed( + GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer user_data +); +static void on_file_monitor_subdir_changed( + GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer user_data +); + +// +// STATIC DATA +// +static gint wintc_sh_dir_monitor_recursive_signals[N_SIGNALS] = { 0 }; + +// +// GTK OOP CLASS/INSTANCE DEFINITIONS +// +struct _WinTCShDirMonitorRecursive +{ + GObject __parent__; + + GFile* file_root; + GFileMonitor* monitor_root; + + GHashTable* map_rel_path_to_monitor; +}; + +// +// GTK TYPE DEFINITIONS & CTORS +// +G_DEFINE_TYPE( + WinTCShDirMonitorRecursive, + wintc_sh_dir_monitor_recursive, + G_TYPE_OBJECT +) + +static void wintc_sh_dir_monitor_recursive_class_init( + WinTCShDirMonitorRecursiveClass* klass +) +{ + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + object_class->constructed = wintc_sh_dir_monitor_recursive_constructed; + object_class->dispose = wintc_sh_dir_monitor_recursive_dispose; + object_class->get_property = wintc_sh_dir_monitor_recursive_get_property; + object_class->set_property = wintc_sh_dir_monitor_recursive_set_property; + + g_object_class_install_property( + object_class, + PROP_FILE, + g_param_spec_object( + "file", + "File", + "The root directory to monitor recursively.", + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY + ) + ); + g_object_class_install_property( + object_class, + PROP_FILE_MONITOR, + g_param_spec_object( + "file-monitor", + "FileMonitor", + "The file monitor for the root directory.", + G_TYPE_FILE_MONITOR, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY + ) + ); + + wintc_sh_dir_monitor_recursive_signals[SIGNAL_CHANGED] = + g_signal_new( + "changed", + G_TYPE_FROM_CLASS(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + wintc_cclosure_marshal_VOID__OBJECT_OBJECT_INT, + G_TYPE_NONE, + 3, + G_TYPE_OBJECT, + G_TYPE_OBJECT, + G_TYPE_INT + ); +} + +static void wintc_sh_dir_monitor_recursive_init( + WINTC_UNUSED(WinTCShDirMonitorRecursive* self) +) {} + +// +// CLASS VIRTUAL METHODS +// +static void wintc_sh_dir_monitor_recursive_constructed( + GObject* object +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(object); + + if ( + !(monitor_recursive->file_root) || + !(monitor_recursive->monitor_root) + ) + { + g_critical("%s", "shcommon: invalid dir monitor created"); + return; + } + + // Set up signal for the root monitor + // + g_signal_connect( + monitor_recursive->monitor_root, + "changed", + G_CALLBACK(on_file_monitor_root_changed), + monitor_recursive + ); + + // Pull the root path details + // + const gchar* root_path = g_file_peek_path(monitor_recursive->file_root); + gint root_len = g_utf8_strlen(root_path, -1); + + // Build monitors recursively in the map + // + GList* files = + wintc_sh_fs_get_names_as_list( + root_path, + TRUE, + G_FILE_TEST_IS_DIR, + TRUE, + NULL // FIXME: Error handling + ); + + monitor_recursive->map_rel_path_to_monitor = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref + ); + + for (GList* iter = files; iter; iter = iter->next) + { + GFile* file = + g_file_new_for_path( + (gchar*) iter->data + ); + + GFileMonitor* monitor = + g_file_monitor_directory( + file, + G_FILE_MONITOR_NONE, + NULL, + NULL // FIXME: Error handling + ); + + + if (monitor) + { + g_signal_connect( + monitor, + "changed", + G_CALLBACK(on_file_monitor_subdir_changed), + monitor_recursive + ); + + g_hash_table_insert( + monitor_recursive->map_rel_path_to_monitor, + g_strdup(root_path + root_len), + monitor + ); + } + + g_object_unref(file); + } + + g_list_free_full(files, (GDestroyNotify) g_free); +} + +static void wintc_sh_dir_monitor_recursive_dispose( + GObject* object +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(object); + + g_clear_object(&(monitor_recursive->file_root)); + g_clear_object(&(monitor_recursive->monitor_root)); + + g_hash_table_destroy( + g_steal_pointer(&(monitor_recursive->map_rel_path_to_monitor)) + ); + + (G_OBJECT_CLASS(wintc_sh_dir_monitor_recursive_parent_class)) + ->dispose(object); +} + +static void wintc_sh_dir_monitor_recursive_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(object); + + switch (prop_id) + { + case PROP_FILE: + g_value_set_object(value, monitor_recursive->file_root); + break; + + case PROP_FILE_MONITOR: + g_value_set_object(value, monitor_recursive->monitor_root); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void wintc_sh_dir_monitor_recursive_set_property( + GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(object); + + switch (prop_id) + { + case PROP_FILE: + monitor_recursive->file_root = g_value_dup_object(value); + break; + + case PROP_FILE_MONITOR: + monitor_recursive->monitor_root = g_value_dup_object(value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +// +// PUBLIC FUNCTIONS +// +WinTCShDirMonitorRecursive* wintc_sh_fs_monitor_directory_recursive( + GFile* file, + GFileMonitorFlags flags, + GCancellable* cancellable, + GError** error +) +{ + GFileMonitor* monitor = + g_file_monitor_directory( + file, + flags, + cancellable, + error + ); + + if (!monitor) + { + return NULL; + } + + return WINTC_SH_DIR_MONITOR_RECURSIVE( + g_object_new( + WINTC_TYPE_SH_DIR_MONITOR_RECURSIVE, + "file", file, + "file-monitor", monitor, + NULL + ) + ); +} + +// +// PRIVATE FUNCTIONS +// +static void wintc_sh_dir_monitor_recursive_delete_monitor( + WinTCShDirMonitorRecursive* monitor_recursive, + GFile* file +) +{ + const gchar* rel_path = + g_file_peek_path(file) + + g_utf8_strlen( + g_file_peek_path(monitor_recursive->file_root), + -1 + ); + + if ( + !g_hash_table_lookup( + monitor_recursive->map_rel_path_to_monitor, + rel_path + ) + ) + { + return; + } + + g_hash_table_remove( + monitor_recursive->map_rel_path_to_monitor, + rel_path + ); +} + +static void wintc_sh_dir_monitor_recursive_new_monitor( + WinTCShDirMonitorRecursive* monitor_recursive, + GFile* file +) +{ + const gchar* rel_path = + g_file_peek_path(file) + + g_utf8_strlen( + g_file_peek_path(monitor_recursive->file_root), + -1 + ); + + if ( + g_hash_table_lookup( + monitor_recursive->map_rel_path_to_monitor, + rel_path + ) + ) + { + return; + } + + // Attempt to create a directory monitor + // + GFileMonitor* monitor = + g_file_monitor_directory( + file, + G_FILE_MONITOR_NONE, + NULL, + NULL // FIXME: Error handling + ); + + if (!monitor) + { + return; + } + + g_hash_table_insert( + monitor_recursive->map_rel_path_to_monitor, + g_strdup(rel_path), + monitor + ); +} + +// +// CALLBACKS +// +static void on_file_monitor_root_changed( + WINTC_UNUSED(GFileMonitor* monitor), + GFile* file, + WINTC_UNUSED(GFile* other_file), + GFileMonitorEvent event_type, + gpointer user_data +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(user_data); + + switch (event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + WINTC_LOG_DEBUG( + "shcommon: new item in root: %s", + g_file_peek_path(file) + ); + + wintc_sh_dir_monitor_recursive_new_monitor( + monitor_recursive, + file + ); + + break; + + case G_FILE_MONITOR_EVENT_DELETED: + WINTC_LOG_DEBUG( + "shcommon: deleted item in root: %s", + g_file_peek_path(file) + ); + + wintc_sh_dir_monitor_recursive_delete_monitor( + monitor_recursive, + file + ); + + break; + + default: + WINTC_LOG_DEBUG( + "shcommon: unhandled event in root: %s (%d)", + g_file_peek_path(file), + event_type + ); + break; + } + + g_signal_emit( + monitor_recursive, + wintc_sh_dir_monitor_recursive_signals[SIGNAL_CHANGED], + 0, + file, + other_file, + event_type + ); +} + +static void on_file_monitor_subdir_changed( + WINTC_UNUSED(GFileMonitor* monitor), + GFile* file, + WINTC_UNUSED(GFile* other_file), + GFileMonitorEvent event_type, + gpointer user_data +) +{ + WinTCShDirMonitorRecursive* monitor_recursive = + WINTC_SH_DIR_MONITOR_RECURSIVE(user_data); + + switch (event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + WINTC_LOG_DEBUG( + "shcommon: new item in subdir: %s", + g_file_peek_path(file) + ); + + wintc_sh_dir_monitor_recursive_new_monitor( + monitor_recursive, + file + ); + + break; + + case G_FILE_MONITOR_EVENT_DELETED: + WINTC_LOG_DEBUG( + "shcommon: deleted item in subdir: %s", + g_file_peek_path(file) + ); + + wintc_sh_dir_monitor_recursive_delete_monitor( + monitor_recursive, + file + ); + + break; + + default: + WINTC_LOG_DEBUG( + "shcommon: unhandled event in subdir: %s (%d)", + g_file_peek_path(file), + event_type + ); + break; + } + + g_signal_emit( + monitor_recursive, + wintc_sh_dir_monitor_recursive_signals[SIGNAL_CHANGED], + 0, + file, + other_file, + event_type + ); +} diff --git a/shell/taskband/src/start/personal.c b/shell/taskband/src/start/personal.c index 75e5a53..d8ada6a 100644 --- a/shell/taskband/src/start/personal.c +++ b/shell/taskband/src/start/personal.c @@ -127,14 +127,6 @@ void create_personal_menu( GtkBuilder* builder; WinTCTaskbandToolbar* toolbar = WINTC_TASKBAND_TOOLBAR(toolbar_start); - GError* error = NULL; - - if (!wintc_toolbar_start_progmenu_init(&error)) - { - wintc_display_error_and_clear(&error, NULL); - return; - } - // Set default states // toolbar_start->personal.sync_menu_refresh = TRUE; @@ -266,6 +258,7 @@ void create_personal_menu( gtk_menu_item_set_submenu( GTK_MENU_ITEM(toolbar_start->personal.menuitem_all_programs), wintc_toolbar_start_progmenu_new_gtk_menu( + toolbar_start->progmenu, &(toolbar_start->personal.all_programs_binding) ) ); @@ -858,6 +851,9 @@ static void refresh_personal_menu( ) ); + g_object_unref(entry_internet); + g_object_unref(entry_email); + // Add separator between defaults & MFU // gtk_menu_shell_append( diff --git a/shell/taskband/src/start/progmenu.c b/shell/taskband/src/start/progmenu.c index 281be96..a074f49 100644 --- a/shell/taskband/src/start/progmenu.c +++ b/shell/taskband/src/start/progmenu.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,8 @@ #define WINTC_COMPONENT_START_MENU "start-menu" +#define PROGMENU_MAP_FILENAME "progmenu-map" + #define K_DIR_ROOT "" #define K_DIR_ACCESSORIES "/Accessories" #define K_DIR_ACCESSIBILITY "/Accessories/Accessibility" @@ -31,6 +34,17 @@ #define K_DIR_LIBREOFFICE "/LibreOffice" #define K_DIR_QT_DEV_TOOLS "/Qt Developer Tools" +// +// PRIVATE ENUMS +// +typedef enum +{ + WINTC_PROGMENU_SRC_HOME = 0, + WINTC_PROGMENU_SRC_LOCAL = 5, + WINTC_PROGMENU_SRC_SYSTEM = 10, + WINTC_PROGMENU_SRC_WINE = 17 +} WinTCProgMenuSource; + // // LOCAL TYPEDEFS // @@ -38,21 +52,72 @@ typedef const gchar* (*DesktopAppInfoFilterFunc) ( GDesktopAppInfo* entry ); +// +// PRIVATE STRUCTURES +// +typedef struct _WinTCProgMenuMapping +{ + WinTCProgMenuSource src_id; + gchar* mapped_path; +} WinTCProgMenuMapping; + // // FORWARD DECLARATIONS // -static void wintc_toolbar_start_progmenu_build_fs(void); +static void wintc_toolbar_start_progmenu_dispose( + GObject* object +); + +static void wintc_toolbar_start_progmenu_build_fs( + WinTCToolbarStartProgmenu* progmenu +); + +static gboolean wintc_toolbar_start_progmenu_delete_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path, + WinTCProgMenuSource src_id +); static const gchar* wintc_toolbar_start_progmenu_filter_entry( GDesktopAppInfo* entry ); +static const gchar* wintc_toolbar_start_progmenu_get_src_path( + WinTCProgMenuSource src_id +); +static void wintc_toolbar_start_progmenu_load_mappings( + WinTCToolbarStartProgmenu* progmenu +); +static void wintc_toolbar_start_progmenu_mapping_free( + WinTCProgMenuMapping* mapping +); +static gsize wintc_toolbar_start_progmenu_mappings_to_text( + GHashTable* map, + gchar* buf +); +static void wintc_toolbar_start_progmenu_new_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path, + WinTCProgMenuSource src_id +); +static void wintc_toolbar_start_progmenu_save_mappings( + WinTCToolbarStartProgmenu* progmenu +); + +static void wintc_toolbar_start_progmenu_menu_delete_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path +); static GMenu* wintc_toolbar_start_progmenu_menu_from_filelist( - GList* files, - GHashTable** map_dir_to_menu + WinTCToolbarStartProgmenu* progmenu, + GList* files ); static void wintc_toolbar_start_progmenu_menu_insert_sorted( GMenu* menu, GMenuItem* menu_item ); +static void wintc_toolbar_start_progmenu_menu_new_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path +); static gboolean create_symlink( const gchar* rel_path, @@ -76,10 +141,33 @@ static const gchar* filter_qt_dev_tools( GDesktopAppInfo* entry ); +static void on_file_monitor_dir_programs_changed( + GObject* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer user_data +); +static void on_file_monitor_dir_start_menu_changed( + WinTCShDirMonitorRecursive* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer user_data +); + // // STATIC DATA // -static gboolean S_INIT_DONE = FALSE; +static GActionEntry S_ACTIONS[] = { + { + .name = "launch", + .activate = action_launch, + .parameter_type = "s", + .state = NULL, + .change_state = NULL + } +}; static gchar* S_DIR_START_MENU = NULL; @@ -112,42 +200,50 @@ static DesktopAppInfoFilterFunc S_ENTRY_FILTERS[] = { &filter_qt_dev_tools }; -// // // // - -static GMenu* S_MENU_PROGRAMS = NULL; - -static GHashTable* S_MAP_DIR_TO_MENU = NULL; - -static GActionEntry S_ACTIONS[] = { - { - .name = "launch", - .activate = action_launch, - .parameter_type = "s", - .state = NULL, - .change_state = NULL - } -}; +static GQuark S_QUARK_PROGMENU_SRC = 0; // -// PUBLIC FUNCTIONS +// GTK OOP CLASS/INSTANCE DEFINITIONS // -gboolean wintc_toolbar_start_progmenu_init( - GError** error +typedef struct _WinTCToolbarStartProgmenu +{ + GObject __parent__; + + GMenu* menu_programs; + + GHashTable* map_dir_to_menu; + GHashTable* map_src_rel_path_to_mapping; + GHashTable* map_path_to_entry; + + WinTCShDirMonitorRecursive* monitor_start; + GSList* monitors; +} WinTCToolbarStartProgmenu; + +// +// GTK TYPE DEFINITIONS & CTORS +// +G_DEFINE_TYPE( + WinTCToolbarStartProgmenu, + wintc_toolbar_start_progmenu, + G_TYPE_OBJECT +) + +static void wintc_toolbar_start_progmenu_class_init( + WinTCToolbarStartProgmenuClass* klass ) { - if (S_INIT_DONE) - { - return TRUE; - } + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = wintc_toolbar_start_progmenu_dispose; // Sort out profile // S_DIR_START_MENU = wintc_profile_get_path(WINTC_COMPONENT_START_MENU, ""); - if (!wintc_profile_ensure_exists(WINTC_COMPONENT_START_MENU, error)) + if (!wintc_profile_ensure_exists(WINTC_COMPONENT_START_MENU, NULL)) { - return FALSE; + return; } // Set up known desktop entry mappings @@ -166,60 +262,201 @@ gboolean wintc_toolbar_start_progmenu_init( G_N_ELEMENTS(S_VENDOR_MAPPINGS) ); + // Set up quark used for FS monitors + // + S_QUARK_PROGMENU_SRC = g_quark_from_static_string("progmenu-src"); +} + +static void wintc_toolbar_start_progmenu_init( + WinTCToolbarStartProgmenu* self +) +{ + // Load existing mappings + // + wintc_toolbar_start_progmenu_load_mappings(self); + // Attempt to pull the structure from here // - GList* files = + GList* files; + + wintc_toolbar_start_progmenu_build_fs(self); + + files = wintc_sh_fs_get_names_as_list( S_DIR_START_MENU, TRUE, G_FILE_TEST_IS_REGULAR, TRUE, - error + NULL ); - if (!files) - { - wintc_toolbar_start_progmenu_build_fs(); - - files = - wintc_sh_fs_get_names_as_list( - S_DIR_START_MENU, - TRUE, - G_FILE_TEST_IS_REGULAR, - TRUE, - error - ); - } + // Create mapping for entries + // + self->map_path_to_entry = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref + ); // Construct the menu // - S_MENU_PROGRAMS = + self->menu_programs = wintc_toolbar_start_progmenu_menu_from_filelist( - files, - &S_MAP_DIR_TO_MENU + self, + files ); g_list_free_full(files, g_free); - S_INIT_DONE = TRUE; + // Set up file monitor for the start menu dir + // + GFile* start_menu_dir = g_file_new_for_path(S_DIR_START_MENU); - return TRUE; + self->monitor_start = + wintc_sh_fs_monitor_directory_recursive( + start_menu_dir, + G_FILE_MONITOR_NONE, + NULL, + NULL + ); + + if (self->monitor_start) + { + g_signal_connect( + self->monitor_start, + "changed", + G_CALLBACK(on_file_monitor_dir_start_menu_changed), + self + ); + } + + g_object_unref(start_menu_dir); + + // Set up file monitors for application directories + // + WinTCProgMenuSource locations[] = { + WINTC_PROGMENU_SRC_HOME, + WINTC_PROGMENU_SRC_LOCAL, + WINTC_PROGMENU_SRC_SYSTEM, + WINTC_PROGMENU_SRC_WINE + }; + + for (gsize i = 0; i < G_N_ELEMENTS(locations); i++) + { + WinTCProgMenuSource src_id = locations[i]; + + const gchar* monitor_path = + wintc_toolbar_start_progmenu_get_src_path(src_id); + + if (!monitor_path) + { + continue; + } + + GFile* monitor_file = g_file_new_for_path(monitor_path); + + GObject* monitor = NULL; + + if (src_id == WINTC_PROGMENU_SRC_WINE) + { + monitor = + G_OBJECT( + wintc_sh_fs_monitor_directory_recursive( + monitor_file, + G_FILE_MONITOR_NONE, + NULL, + NULL // FIXME: Error handling + ) + ); + } + else + { + monitor = + G_OBJECT( + g_file_monitor_directory( + monitor_file, + G_FILE_MONITOR_NONE, + NULL, + NULL // FIXME: Error handling + ) + ); + } + + g_object_set_qdata( + monitor, + S_QUARK_PROGMENU_SRC, + GINT_TO_POINTER(src_id) + ); + + self->monitors = g_slist_append(self->monitors, monitor); + + g_signal_connect( + monitor, + "changed", + G_CALLBACK(on_file_monitor_dir_programs_changed), + self + ); + + g_object_unref(monitor_file); + } +} + +// +// CLASS VIRTUAL METHODS +// +static void wintc_toolbar_start_progmenu_dispose( + GObject* object +) +{ + WinTCToolbarStartProgmenu* progmenu = + WINTC_TOOLBAR_START_PROGMENU(object); + + // Save mappings first + // + wintc_toolbar_start_progmenu_save_mappings(progmenu); + + // Continue with the disposal + // + // Note that we do not dispose menu_programs explicitly here, it is + // actually owned by map_dir_to_menu which tracks all GMenu objects + // + g_clear_object(&(progmenu->monitor_start)); + g_clear_slist(&(progmenu->monitors), (GDestroyNotify) g_object_unref); + + g_hash_table_destroy( + g_steal_pointer(&(progmenu->map_dir_to_menu)) + ); + g_hash_table_destroy( + g_steal_pointer(&(progmenu->map_src_rel_path_to_mapping)) + ); + g_hash_table_destroy( + g_steal_pointer(&(progmenu->map_path_to_entry)) + ); + + (G_OBJECT_CLASS(wintc_toolbar_start_progmenu_parent_class)) + ->dispose(object); +} + +// +// PUBLIC FUNCTIONS +// +WinTCToolbarStartProgmenu* wintc_toolbar_start_progmenu_new(void) +{ + return WINTC_TOOLBAR_START_PROGMENU( + g_object_new( + WINTC_TYPE_TOOLBAR_START_PROGMENU, + NULL + ) + ); } GtkWidget* wintc_toolbar_start_progmenu_new_gtk_menu( - WinTCCtlMenuBinding** menu_binding + WinTCToolbarStartProgmenu* progmenu, + WinTCCtlMenuBinding** menu_binding ) { - if (!S_INIT_DONE) - { - g_critical( - "%s", - "start menu - progmenu not ready to make a GtkMenu" - ); - - return NULL; - } - // Ensure we have an action map for the menu items to call // static GSimpleActionGroup* s_action_group = NULL; @@ -245,7 +482,7 @@ GtkWidget* wintc_toolbar_start_progmenu_new_gtk_menu( *menu_binding = wintc_ctl_menu_binding_new( GTK_MENU_SHELL(menu), - G_MENU_MODEL(S_MENU_PROGRAMS) + G_MENU_MODEL(progmenu->menu_programs) ); gtk_widget_insert_action_group( @@ -260,50 +497,149 @@ GtkWidget* wintc_toolbar_start_progmenu_new_gtk_menu( // // PRIVATE FUNCTIONS // -static void wintc_toolbar_start_progmenu_build_fs(void) +static void wintc_toolbar_start_progmenu_build_fs( + WinTCToolbarStartProgmenu* progmenu +) { - GList* all_entries = - wintc_sh_fs_get_names_as_list( - WINTC_RT_PREFIX "/share/applications", - TRUE, - G_FILE_TEST_IS_REGULAR, - TRUE, - NULL // FIXME: Error handling - ); + const WinTCProgMenuSource sources[] = { + WINTC_PROGMENU_SRC_HOME, + WINTC_PROGMENU_SRC_LOCAL, + WINTC_PROGMENU_SRC_SYSTEM, + WINTC_PROGMENU_SRC_WINE + }; - for (GList* iter = all_entries; iter; iter = iter->next) + GList* all_entries; + const gchar* src_path; + + for (gulong i = 0; i < G_N_ELEMENTS(sources); i++) { - gchar* entry_path = (gchar*) iter->data; + src_path = + wintc_toolbar_start_progmenu_get_src_path( + sources[i] + ); - WINTC_LOG_DEBUG("start menu - analyse %s", entry_path); - - GDesktopAppInfo* entry = - g_desktop_app_info_new_from_filename(entry_path); - - if (!entry) + if (!src_path) { - g_warning("start menu - failed to load %s", entry_path); continue; } - const gchar* rel_path = - wintc_toolbar_start_progmenu_filter_entry( - entry + all_entries = + wintc_sh_fs_get_names_as_list( + src_path, + TRUE, + G_FILE_TEST_IS_REGULAR, + sources[i] == WINTC_PROGMENU_SRC_WINE, + NULL // FIXME: Error handling ); - if (rel_path) + for (GList* iter = all_entries; iter; iter = iter->next) { - create_symlink( - rel_path, - wintc_basename(g_desktop_app_info_get_filename(entry)), - g_desktop_app_info_get_filename(entry) + gchar* entry_path = (gchar*) iter->data; + + wintc_toolbar_start_progmenu_new_entry( + progmenu, + entry_path, + sources[i] ); } - g_object_unref(entry); + g_list_free_full(all_entries, g_free); + } +} + +static gboolean wintc_toolbar_start_progmenu_delete_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path, + WinTCProgMenuSource src_id +) +{ + gboolean delete = FALSE; + const gchar* src_path = wintc_toolbar_start_progmenu_get_src_path(src_id); + + // Retrieve the mapping + // + const gchar* src_rel_path = entry_path + g_utf8_strlen(src_path, -1); + + WinTCProgMenuMapping* mapping = + g_hash_table_lookup( + progmenu->map_src_rel_path_to_mapping, + src_rel_path + ); + + if (!mapping) + { + return TRUE; } - g_list_free_full(all_entries, g_free); + // Validate mapping is ours + // + GError* error = NULL; + + gchar* dst_path = g_build_path( + G_DIR_SEPARATOR_S, + S_DIR_START_MENU, + mapping->mapped_path, + wintc_basename(entry_path), + NULL + ); + GFile* file = g_file_new_for_path(dst_path); + GFileInfo* file_info = g_file_query_info( + file, + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, + &error + ); + + if (!file_info) + { + // FIXME: Proper error handling here + // + wintc_log_error_and_clear(&error); + goto cleanup; + } + + // Do the comparison + // + const gchar* link = g_file_info_get_symlink_target(file_info); + + WINTC_LOG_DEBUG("start menu - delete mapping - source is %s", entry_path); + WINTC_LOG_DEBUG("start menu - delete mapping - symlink is %s", link); + + if (!link) + { + delete = TRUE; + } + else if (link == entry_path) + { + if (unlink(dst_path) < 0) + { + g_warning( + "start menu - delete mapping - couldn't unlink %s (%d)", + dst_path, + errno + ); + + goto cleanup; + } + + delete = TRUE; + } + + if (delete) + { + g_hash_table_remove( + progmenu->map_src_rel_path_to_mapping, + src_rel_path + ); + } + +cleanup: + if (file_info) { g_object_unref(file_info); } + g_object_unref(file); + g_free(dst_path); + + return delete; } static const gchar* wintc_toolbar_start_progmenu_filter_entry( @@ -325,6 +661,48 @@ static const gchar* wintc_toolbar_start_progmenu_filter_entry( return NULL; } + // Is this a WINE program? + // + if ( + g_str_has_prefix( + g_desktop_app_info_get_filename(entry), + wintc_toolbar_start_progmenu_get_src_path(WINTC_PROGMENU_SRC_WINE) + ) + ) + { + static gchar* rel_path = NULL; + + g_free(g_steal_pointer(&rel_path)); + + const gchar* full_path = g_desktop_app_info_get_filename(entry); + + const gchar* end_wine = + g_utf8_strrchr( + full_path, + -1, + G_DIR_SEPARATOR + ); + + const gchar* start_wine = + full_path + + g_utf8_strlen( + wintc_toolbar_start_progmenu_get_src_path( + WINTC_PROGMENU_SRC_WINE + ), + -1 + ); + + rel_path = wintc_substr(start_wine, end_wine); + + WINTC_LOG_DEBUG( + "start menu - filter - suggest (via WINE), %s/%s", + rel_path, + basename + ); + + return rel_path; + } + // Check if this entry has a direct mapping // const gchar* known_target = @@ -441,224 +819,624 @@ static const gchar* wintc_toolbar_start_progmenu_filter_entry( return K_DIR_ROOT; } +static const gchar* wintc_toolbar_start_progmenu_get_src_path( + WinTCProgMenuSource src_id +) +{ + switch (src_id) + { + case WINTC_PROGMENU_SRC_HOME: + { + static const char* s_src_home = NULL; + + if (!s_src_home) + { + s_src_home = + g_build_path( + G_DIR_SEPARATOR_S, + g_get_home_dir(), + ".local", + "share", + "applications", + NULL + ); + } + + return s_src_home; + } + + case WINTC_PROGMENU_SRC_LOCAL: + { +#ifdef WINTC_PKGMGR_BSDPKG + return NULL; +#else + static const gchar* s_src_local = NULL; + + if (!s_src_local) + { + s_src_local = + g_build_path( + G_DIR_SEPARATOR_S, + G_DIR_SEPARATOR_S, + "usr", + "local", + "share", + "applications", + NULL + ); + } + + return s_src_local; +#endif + } + + case WINTC_PROGMENU_SRC_SYSTEM: + { + static const gchar* s_src_system = NULL; + + if (!s_src_system) + { + s_src_system = + g_build_path( + G_DIR_SEPARATOR_S, + G_DIR_SEPARATOR_S, + "usr", +#ifdef WINTC_PKGMGR_BSDPKG + "local", +#endif + "share", + "applications", + NULL + ); + } + + return s_src_system; + } + + case WINTC_PROGMENU_SRC_WINE: + { + static const gchar* s_src_wine = NULL; + + if (!s_src_wine) + { + s_src_wine = + g_build_path( + G_DIR_SEPARATOR_S, + g_get_home_dir(), + ".local", + "share", + "applications", + "wine", + "Programs", + NULL + ); + } + + return s_src_wine; + } + + default: + g_critical( + "taskband - progmenu - unknown src dir %d", + src_id + ); + + return NULL; + } +} + +static void wintc_toolbar_start_progmenu_mapping_free( + WinTCProgMenuMapping* mapping +) +{ + g_free(mapping->mapped_path); + g_free(mapping); +} + +static void wintc_toolbar_start_progmenu_load_mappings( + WinTCToolbarStartProgmenu* progmenu +) +{ + GError* error = NULL; + gchar* map_text = NULL; + + progmenu->map_src_rel_path_to_mapping = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) wintc_toolbar_start_progmenu_mapping_free + ); + + if ( + !wintc_profile_get_file_contents( + WINTC_COMPONENT_SHELL, + PROGMENU_MAP_FILENAME, + &map_text, + NULL, + &error + ) + ) + { + // FIXME: Handle this in the UI properly + // Also, ignore 'file not found', because that could just be + // that this is the first time the taskband has started up + // + wintc_log_error_and_clear(&error); + return; + } + + // Read through the mappings + // + const gchar* line = map_text; + + while (line) + { + gchar* src_rel_path = wintc_strdup_nextchr(line, -1, ',', &line); + gchar* src_id_str = wintc_strdup_nextchr(line, -1, ';', &line); + gchar* dst_rel_path = wintc_strdup_nextchr(line, -1, '\n', &line); + + if ( + !src_rel_path || + !src_id_str || + !dst_rel_path + ) + { + g_free(src_rel_path); + g_free(src_id_str); + g_free(dst_rel_path); + + break; + } + + gint src_id = strtol(src_id_str, NULL, 10); + + WinTCProgMenuMapping* mapping = g_new(WinTCProgMenuMapping, 1); + + mapping->src_id = src_id; + mapping->mapped_path = dst_rel_path; + + g_hash_table_insert( + progmenu->map_src_rel_path_to_mapping, + src_rel_path, + mapping + ); + + g_free(src_id_str); + } + + g_free(map_text); +} + +static gsize wintc_toolbar_start_progmenu_mappings_to_text( + GHashTable* map, + gchar* buf +) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + + glong pos = 0; + + g_hash_table_iter_init(&iter, map); + + while (g_hash_table_iter_next(&iter, &key, &value)) + { + const WinTCProgMenuMapping* mapping = value; + const gchar* src_rel_path = key; + + gchar* src_id_str = g_strdup_printf("%d", mapping->src_id); + + glong len_src_rel_path = g_utf8_strlen(src_rel_path, -1); + glong len_src_id = g_utf8_strlen(src_id_str, -1); + glong len_mapped = g_utf8_strlen(mapping->mapped_path, -1); + + wintc_memcpy_ref((void*) buf, pos, src_rel_path, len_src_rel_path); + pos += len_src_rel_path; + + if (buf) { buf[pos] = ','; } + pos++; + + wintc_memcpy_ref((void*) buf, pos, src_id_str, len_src_id); + pos += len_src_id; + + if (buf) { buf[pos] = ';'; } + pos++; + + wintc_memcpy_ref((void*) buf, pos, mapping->mapped_path, len_mapped); + pos += len_mapped; + + if (buf) { buf[pos] = '\n'; } + pos++; + + g_free(src_id_str); + } + + // Null terminator + // + if (!pos) + { + pos++; + } + + if (buf) { buf[pos - 1] = '\0'; } + + return (gsize) pos; +} + +static void wintc_toolbar_start_progmenu_new_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path, + WinTCProgMenuSource src_id +) +{ + WINTC_LOG_DEBUG("start menu - analyse %s", entry_path); + + // Is this actually an entry? + // + if (!g_str_has_suffix(entry_path, ".desktop")) + { + WINTC_LOG_DEBUG( + "start menu - new entry - not a .desktop: %s (skipped)", + entry_path + ); + + return; + } + + // Check relative path - do we know of this entry already? + // + const gchar* src_path = wintc_toolbar_start_progmenu_get_src_path(src_id); + const gchar* rel_path = entry_path + g_utf8_strlen(src_path, -1); + + WinTCProgMenuMapping* mapping = + g_hash_table_lookup( + progmenu->map_src_rel_path_to_mapping, + rel_path + ); + + if (mapping) + { + // If the existing mapping is higher priority then drop out + // + if (mapping->src_id < src_id) + { + return; + } + + // If the mapping exists is lower priority, bin it + // + if (mapping->src_id > src_id) + { + if ( + !wintc_toolbar_start_progmenu_delete_entry( + progmenu, + entry_path, + src_id + ) + ) + { + g_warning( + "%s", + "start menu - new entry - couldn't bin superseded entry" + ); + + return; + } + } + } + + // Add the entry now + // + GDesktopAppInfo* entry = + g_desktop_app_info_new_from_filename(entry_path); + + if (!entry) + { + g_warning("start menu - failed to load %s", entry_path); + return; + } + + const gchar* dst_rel_path = + wintc_toolbar_start_progmenu_filter_entry( + entry + ); + + if (dst_rel_path) + { + if (mapping) + { + // If mapped already¬ + // - If the destination is different, delete the mapping + // - Otherwise, nothing to do! + // + if (g_strcmp0(mapping->mapped_path, dst_rel_path) != 0) + { + if ( + !wintc_toolbar_start_progmenu_delete_entry( + progmenu, + entry_path, + src_id + ) + ) + { + g_warning( + "%s", + "start menu - new entry - couldn't bin old entry" + ); + + goto cleanup; + } + } + else + { + goto cleanup; + } + } + + if ( + create_symlink( + dst_rel_path, + wintc_basename(g_desktop_app_info_get_filename(entry)), + g_desktop_app_info_get_filename(entry) + ) + ) + { + WinTCProgMenuMapping* new_map = g_new0(WinTCProgMenuMapping, 1); + + new_map->src_id = src_id; + new_map->mapped_path = g_strdup(dst_rel_path); + + g_hash_table_insert( + progmenu->map_src_rel_path_to_mapping, + g_strdup(rel_path), + new_map + ); + } + else + { + g_warning( + "start menu - new entry - could not make symlink for %s", + entry_path + ); + } + } + +cleanup: + g_object_unref(entry); +} + +static void wintc_toolbar_start_progmenu_save_mappings( + WinTCToolbarStartProgmenu* progmenu +) +{ + gchar* buf; + gsize buf_size; + GError* error = NULL; + + buf_size = + wintc_toolbar_start_progmenu_mappings_to_text( + progmenu->map_src_rel_path_to_mapping, + NULL + ); + + buf = g_malloc(buf_size); + + wintc_toolbar_start_progmenu_mappings_to_text( + progmenu->map_src_rel_path_to_mapping, + buf + ); + + if ( + !wintc_profile_set_file_contents( + WINTC_COMPONENT_SHELL, + PROGMENU_MAP_FILENAME, + buf, + -1, + &error + ) + ) + { + wintc_log_error_and_clear(&error); + } + + g_free(buf); +} + +static void wintc_toolbar_start_progmenu_menu_delete_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path +) +{ + // Do we even know about this entry? + // + GDesktopAppInfo* entry = + g_hash_table_lookup(progmenu->map_path_to_entry, entry_path); + + if (!entry) + { + return; + } + + // Pull the menu that owns the item + // + const gchar* dir_start = entry_path + g_utf8_strlen(S_DIR_START_MENU, -1); + const gchar* dir_end = g_utf8_strrchr(entry_path, -1, G_DIR_SEPARATOR); + + gchar* rel_dir = wintc_substr(dir_start, dir_end); + + GMenu* menu = g_hash_table_lookup(progmenu->map_dir_to_menu, rel_dir); + + g_free(rel_dir); + + if (!menu) + { + g_critical( + "start menu - lost track of %s, report this!", + entry_path + ); + + return; + } + + // Begin search after submenus + // + gint n_items = g_menu_model_get_n_items(G_MENU_MODEL(menu)); + + gint idx_end = n_items - 1; + gint idx_start = 0; + + for (; idx_start < n_items; idx_start++) + { + if ( + !g_menu_model_get_item_link( + G_MENU_MODEL(menu), + idx_start, + G_MENU_LINK_SUBMENU + ) + ) + { + break; + } + } + + // Search for the item now + // + gchar* cmp_name = NULL; + gchar* our_name = NULL; + gint idx_mid; + gint res; + + our_name = + g_utf8_casefold( + g_app_info_get_display_name(G_APP_INFO(entry)), + -1 + ); + + while(idx_end - idx_start > 1) + { + idx_mid = idx_end - ((idx_end - idx_start) / 2); + + g_menu_model_get_item_attribute( + G_MENU_MODEL(menu), + idx_mid, + G_MENU_ATTRIBUTE_LABEL, + "s", + &cmp_name + ); + + WINTC_UTF8_TRANSFORM(cmp_name, g_utf8_casefold); + + res = g_utf8_collate(our_name, cmp_name); + + if (res < 0) + { + idx_end = idx_mid; + } + else if (res > 0) + { + idx_start = idx_mid; + } + else + { + idx_start = idx_mid; + idx_end = idx_mid; + } + + g_free(cmp_name); + } + + // Confirm the index + // + gint idx_found = -1; + + g_menu_model_get_item_attribute( + G_MENU_MODEL(menu), + idx_start, + G_MENU_ATTRIBUTE_LABEL, + "s", + &cmp_name + ); + + WINTC_UTF8_TRANSFORM(cmp_name, g_utf8_casefold); + + if (g_utf8_collate(our_name, cmp_name) == 0) + { + idx_found = idx_start; + } + else + { + g_free(cmp_name); + + g_menu_model_get_item_attribute( + G_MENU_MODEL(menu), + idx_end, + G_MENU_ATTRIBUTE_LABEL, + "s", + &cmp_name + ); + + WINTC_UTF8_TRANSFORM(cmp_name, g_utf8_casefold); + + if (g_utf8_collate(our_name, cmp_name) == 0) + { + idx_found = idx_end; + } + } + + g_free(cmp_name); + g_free(our_name); + + // Destroy the item + // + if (idx_found < 0) + { + g_critical("start menu - failed to locate %s in menu", entry_path); + return; + } + + g_menu_remove(menu, idx_found); + g_hash_table_remove(progmenu->map_path_to_entry, entry_path); +} + static GMenu* wintc_toolbar_start_progmenu_menu_from_filelist( - GList* files, - GHashTable** map_dir_to_menu + WinTCToolbarStartProgmenu* progmenu, + GList* files ) { GMenu* menu = g_menu_new(); - *map_dir_to_menu = g_hash_table_new(g_str_hash, g_str_equal); + progmenu->map_dir_to_menu = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_object_unref + ); // Add the top level menu to start with... // g_hash_table_insert( - *map_dir_to_menu, - "", + progmenu->map_dir_to_menu, + g_strdup(""), menu ); // Construct the menus from this file list // - gint len_root = g_utf8_strlen(S_DIR_START_MENU, -1); - for (GList* iter = files; iter; iter = iter->next) { - const gchar* entry_path = (gchar*) iter->data; - GDesktopAppInfo* entry = - g_desktop_app_info_new_from_filename(entry_path); + const gchar* entry_path = (gchar*) iter->data; WINTC_LOG_DEBUG("start menu - found: %s", (gchar*) iter->data); - // Pull the directory components out of the path to check the submenu - // that should own this item - // - const gchar* dir_end = g_utf8_strrchr( - entry_path, - -1, - G_DIR_SEPARATOR - ); - const gchar* dir_start = entry_path + len_root; - - gchar* rel_dir = g_malloc0((dir_end - dir_start) + 1); - - if (dir_start != dir_end) - { - memcpy(rel_dir, dir_start, dir_end - dir_start); - } - - // Fetch the menu - // - GMenu* menu_owner = - g_hash_table_lookup(*map_dir_to_menu, rel_dir); - - if (!menu_owner) - { - menu_owner = g_menu_new(); - - g_hash_table_insert( - *map_dir_to_menu, - rel_dir, - menu_owner - ); - } - else - { - g_free(rel_dir); // No longer needed - } - - // Set up the cmdline - // - gchar* cmd = wintc_desktop_app_info_get_command(entry); - - // Create the menu item - // - GMenuItem* new_item = g_menu_item_new(NULL, NULL); - GVariant* variant = g_variant_new_string(cmd); - - g_menu_item_set_action_and_target_value( - new_item, - "progmenu.launch", - variant + wintc_toolbar_start_progmenu_menu_new_entry( + progmenu, + entry_path ); - g_menu_item_set_icon( - new_item, - g_app_info_get_icon(G_APP_INFO(entry)) - ); - g_menu_item_set_label( - new_item, - g_app_info_get_name(G_APP_INFO(entry)) - ); - - wintc_toolbar_start_progmenu_menu_insert_sorted( - menu_owner, - new_item - ); - - g_free(cmd); - g_object_unref(new_item); - g_object_unref(entry); } - // Link up submenus - // - GList* submenu_keys = g_hash_table_get_keys(*map_dir_to_menu); - - for (GList* iter = submenu_keys; iter; iter = iter->next) - { - gchar* this_dir = (gchar*) iter->data; - - if (g_strcmp0(this_dir, "") == 0) - { - continue; - } - - // Hold onto this menu - // - GMenu* this_menu = - g_hash_table_lookup( - *map_dir_to_menu, - this_dir - ); - - // Nav up dir components to find parent menus, it could be that a menu - // does not exist for a parent yet! - // - // strdup simplifies things so we can just free() - // - GMenu* cur_menu = this_menu; - gchar* iter = g_strdup(this_dir); - - while (iter) - { - // Retrieve the parent path - // - const gchar* dir_end = g_utf8_strrchr(iter, -1, G_DIR_SEPARATOR); - - gchar* parent = g_malloc0(dir_end - iter + 1); - - memcpy(parent, iter, dir_end - iter); - - // Create the menu item for the submenu - // - // FIXME: The icon will not show up until a custom menu tracker - // implementation is made - GTK3 itself does not bind the - // icon attribute for menu items with a submenu -_- - // - static GIcon* s_icon_programs = NULL; - - if (!s_icon_programs) - { - s_icon_programs = g_themed_icon_new("applications-other"); - } - - GMenuItem* submenu_item = g_menu_item_new(NULL, NULL); - - g_menu_item_set_icon( - submenu_item, - s_icon_programs - ); - g_menu_item_set_label( - submenu_item, - dir_end + 1 - ); - g_menu_item_set_submenu( - submenu_item, - G_MENU_MODEL(cur_menu) - ); - - // Check if the parent exists - // - GMenu* parent_menu = - g_hash_table_lookup( - *map_dir_to_menu, - parent - ); - - if (parent_menu) - { - wintc_toolbar_start_progmenu_menu_insert_sorted( - parent_menu, - submenu_item - ); - - // No more work required - g_free(parent); - g_object_unref(submenu_item); - break; - } - - // Parent doesn't exist, create it... - // - parent_menu = g_menu_new(); - - g_hash_table_insert( - *map_dir_to_menu, - parent, - parent_menu - ); - - // ...add us to it and continue iterating, because this new menu - // will need to be added to its parent too - // - g_menu_prepend_item( - parent_menu, - submenu_item - ); - - g_object_unref(submenu_item); - - cur_menu = parent_menu; - - g_free(iter); - iter = parent; - } - - g_free(iter); - } - - g_list_free(submenu_keys); - return menu; } @@ -704,10 +1482,15 @@ static void wintc_toolbar_start_progmenu_menu_insert_sorted( // if (g_menu_item_get_link(menu_item, G_MENU_LINK_SUBMENU)) { - if (n_submenu_items) + // If this is the first submenu item, it's always first in the menu + // + if (!n_submenu_items) { - idx_end = n_submenu_items - 1; + g_menu_prepend_item(menu, menu_item); + return; } + + idx_end = n_submenu_items - 1; } else { @@ -733,6 +1516,8 @@ static void wintc_toolbar_start_progmenu_menu_insert_sorted( &our_name ); + WINTC_LOG_DEBUG("start menu - inserting %s", our_name); + WINTC_UTF8_TRANSFORM(our_name, g_utf8_casefold); while (idx_end - idx_start > 1) @@ -768,21 +1553,6 @@ static void wintc_toolbar_start_progmenu_menu_insert_sorted( g_free(cmp_name); } - // If we found an exact location, insert there - // - if (idx_start == idx_end) - { - g_menu_insert_item( - menu, - idx_start, - menu_item - ); - - g_free(our_name); - - return; - } - // Otherwise determine between start and end // g_menu_model_get_item_attribute( @@ -839,6 +1609,188 @@ static void wintc_toolbar_start_progmenu_menu_insert_sorted( g_free(our_name); } +static void wintc_toolbar_start_progmenu_menu_new_entry( + WinTCToolbarStartProgmenu* progmenu, + const gchar* entry_path +) +{ + GDesktopAppInfo* entry = + g_desktop_app_info_new_from_filename(entry_path); + + if (!entry) + { + g_warning( + "start menu - new menu item - not a desktop entry: %s", + entry_path + ); + + return; + } + + // Retrieve the launch command - some entries don't have one, which is + // weird... we just ignore those + // + gchar* cmd = wintc_desktop_app_info_get_command(entry); + + if (!cmd) + { + WINTC_LOG_DEBUG( + "start menu - no command for %s, skipping", + entry_path + ); + + g_object_unref(entry); + return; + } + + // Create the menu item for the entry + // + GMenuItem* new_item = g_menu_item_new(NULL, NULL); + GVariant* variant = g_variant_new_string(cmd); + + g_menu_item_set_action_and_target_value( + new_item, + "progmenu.launch", + variant + ); + g_menu_item_set_icon( + new_item, + g_app_info_get_icon(G_APP_INFO(entry)) + ); + g_menu_item_set_label( + new_item, + g_app_info_get_name(G_APP_INFO(entry)) + ); + + g_free(cmd); + + // Store the entry for later bookkeeping + // + g_hash_table_insert( + progmenu->map_path_to_entry, + g_strdup(entry_path), + entry + ); + + // Pull the directory components out of the path to check the submenu + // that should own this item + // + const gchar* dir_end = g_utf8_strrchr( + entry_path, + -1, + G_DIR_SEPARATOR + ); + const gchar* dir_start = entry_path + g_utf8_strlen(S_DIR_START_MENU, -1); + + gchar* rel_dir = wintc_substr(dir_start, dir_end); + + // Fetch the menu + // + GMenu* menu_owner = + g_hash_table_lookup( + progmenu->map_dir_to_menu, + rel_dir + ); + + if (!menu_owner) + { + WINTC_LOG_DEBUG("start menu - new menu at %s", rel_dir); + + menu_owner = g_menu_new(); + + g_hash_table_insert( + progmenu->map_dir_to_menu, + g_strdup(rel_dir), + menu_owner + ); + + // Follow back up the chain if we need to create parent submenus + // + gboolean found_parent = FALSE; + gchar* iter_dir; + GMenu* menu_iter = menu_owner; + + while (!found_parent) + { + iter_dir = + wintc_substr( + rel_dir, + g_utf8_strrchr(rel_dir, -1, G_DIR_SEPARATOR) + ); + + // Create the submenu item + // + static GIcon* s_icon_programs = NULL; + + if (!s_icon_programs) + { + s_icon_programs = g_themed_icon_new("applications-other"); + } + + GMenuItem* submenu_item = g_menu_item_new(NULL, NULL); + + g_menu_item_set_icon( + submenu_item, + s_icon_programs + ); + g_menu_item_set_label( + submenu_item, + g_utf8_strrchr(rel_dir, -1, G_DIR_SEPARATOR) + 1 + ); + g_menu_item_set_submenu( + submenu_item, + G_MENU_MODEL(menu_iter) + ); + + // Locate the parent menu + // + menu_iter = + g_hash_table_lookup( + progmenu->map_dir_to_menu, + iter_dir + ); + + if (!menu_iter) + { + menu_iter = g_menu_new(); + + g_hash_table_insert( + progmenu->map_dir_to_menu, + g_strdup(iter_dir), + menu_iter + ); + + wintc_toolbar_start_progmenu_menu_insert_sorted( + menu_iter, + submenu_item + ); + } + else + { + wintc_toolbar_start_progmenu_menu_insert_sorted( + menu_iter, + submenu_item + ); + + found_parent = TRUE; + } + + g_object_unref(submenu_item); + + g_free(rel_dir); + rel_dir = iter_dir; + } + } + + wintc_toolbar_start_progmenu_menu_insert_sorted( + menu_owner, + new_item + ); + + g_object_unref(new_item); + g_free(rel_dir); +} + static gboolean create_symlink( const gchar* rel_path, const gchar* entry_name, @@ -853,7 +1805,7 @@ static gboolean create_symlink( g_build_path(G_DIR_SEPARATOR_S, S_DIR_START_MENU, rel_path, NULL); if ( - mkdir( + g_mkdir_with_parents( full_dir_path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH ) < 0 @@ -1008,3 +1960,106 @@ static const gchar* filter_qt_dev_tools( return NULL; } + +static void on_file_monitor_dir_programs_changed( + GObject* monitor, + GFile* file, + WINTC_UNUSED(GFile* other_file), + GFileMonitorEvent event_type, + gpointer user_data +) +{ + WinTCToolbarStartProgmenu* progmenu = + WINTC_TOOLBAR_START_PROGMENU(user_data); + + WinTCProgMenuSource src_id = + GPOINTER_TO_INT( + g_object_get_qdata(monitor, S_QUARK_PROGMENU_SRC) + ); + + switch (event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + WINTC_LOG_DEBUG( + "start menu - monitor progs - new entry: %s", + g_file_peek_path(file) + ); + + wintc_toolbar_start_progmenu_new_entry( + progmenu, + g_file_peek_path(file), + src_id + ); + + break; + + case G_FILE_MONITOR_EVENT_DELETED: + WINTC_LOG_DEBUG( + "start menu - monitor progs - deleted entry: %s", + g_file_peek_path(file) + ); + + wintc_toolbar_start_progmenu_delete_entry( + progmenu, + g_file_peek_path(file), + src_id + ); + + break; + + default: + WINTC_LOG_DEBUG( + "start menu - monitor progs - not handled event %d", + event_type + ); + break; + } +} + +static void on_file_monitor_dir_start_menu_changed( + WINTC_UNUSED(WinTCShDirMonitorRecursive* monitor), + GFile* file, + WINTC_UNUSED(GFile* other_file), + GFileMonitorEvent event_type, + gpointer user_data +) +{ + WinTCToolbarStartProgmenu* progmenu = + WINTC_TOOLBAR_START_PROGMENU(user_data); + + switch (event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + WINTC_LOG_DEBUG( + "start menu - monitor menu - new start menu entry: %s", + g_file_peek_path(file) + ); + + wintc_toolbar_start_progmenu_menu_new_entry( + progmenu, + g_file_peek_path(file) + ); + + break; + + case G_FILE_MONITOR_EVENT_DELETED: + WINTC_LOG_DEBUG( + "start menu - monitor menu - deleted start menu entry: %s", + g_file_peek_path(file) + ); + + wintc_toolbar_start_progmenu_menu_delete_entry( + progmenu, + g_file_peek_path(file) + ); + + break; + + default: + WINTC_LOG_DEBUG( + "start menu - monitor menu - not handled event %d", + event_type + ); + break; + } +} diff --git a/shell/taskband/src/start/progmenu.h b/shell/taskband/src/start/progmenu.h index 0dbdd60..e1e71e9 100644 --- a/shell/taskband/src/start/progmenu.h +++ b/shell/taskband/src/start/progmenu.h @@ -5,15 +5,27 @@ #include #include +// +// GTK OOP BOILERPLATE +// +#define WINTC_TYPE_TOOLBAR_START_PROGMENU (wintc_toolbar_start_progmenu_get_type()) + +G_DECLARE_FINAL_TYPE( + WinTCToolbarStartProgmenu, + wintc_toolbar_start_progmenu, + WINTC, + TOOLBAR_START_PROGMENU, + GObject +) + // // PUBLIC FUNCTIONS // -gboolean wintc_toolbar_start_progmenu_init( - GError** error -); +WinTCToolbarStartProgmenu* wintc_toolbar_start_progmenu_new(void); GtkWidget* wintc_toolbar_start_progmenu_new_gtk_menu( - WinTCCtlMenuBinding** menu_binding + WinTCToolbarStartProgmenu* progmenu, + WinTCCtlMenuBinding** menu_binding ); #endif diff --git a/shell/taskband/src/start/shared.h b/shell/taskband/src/start/shared.h index 01524b0..925a50d 100644 --- a/shell/taskband/src/start/shared.h +++ b/shell/taskband/src/start/shared.h @@ -5,6 +5,8 @@ #include #include +#include "progmenu.h" + // // INTERNAL STRUCTS // @@ -51,6 +53,8 @@ typedef struct _WinTCToolbarStart // PersonalStartMenuData personal; + WinTCToolbarStartProgmenu* progmenu; + // UI state // gboolean sync_button; diff --git a/shell/taskband/src/start/toolbar.c b/shell/taskband/src/start/toolbar.c index 525ad68..0e54ede 100644 --- a/shell/taskband/src/start/toolbar.c +++ b/shell/taskband/src/start/toolbar.c @@ -6,6 +6,7 @@ #include "../toolbar.h" #include "personal.h" +#include "progmenu.h" #include "shared.h" #include "toolbar.h" @@ -66,6 +67,10 @@ static void wintc_toolbar_start_init( GTK_STYLE_PROVIDER_PRIORITY_FALLBACK ); + // Initialize progmenu + // + self->progmenu = wintc_toolbar_start_progmenu_new(); + // Create root widget (Start button) // builder = @@ -120,6 +125,10 @@ static void wintc_toolbar_start_dispose( // destroy_personal_menu(toolbar_start); + // Destroy progmenu - ensures the data will be saved + // + g_object_unref(toolbar_start->progmenu); + (G_OBJECT_CLASS(wintc_toolbar_start_parent_class))->dispose(object); } diff --git a/shell/taskband/src/systray/power.c b/shell/taskband/src/systray/power.c index 2d858c5..90af536 100644 --- a/shell/taskband/src/systray/power.c +++ b/shell/taskband/src/systray/power.c @@ -143,6 +143,8 @@ static void wintc_notification_power_constructed( update_client_on_battery(power, power->up_client); + g_ptr_array_unref(all_devices); + (G_OBJECT_CLASS( wintc_notification_power_parent_class ))->constructed(object);