From 646ebcdd007b2a1cf47776489ef97952136997be Mon Sep 17 00:00:00 2001 From: WerWolv Date: Mon, 22 Dec 2025 16:20:23 +0100 Subject: [PATCH] feat: Add create and open options to macOS dock icon context menu --- .../api/content_registry/user_interface.hpp | 18 +++- .../include/hex/helpers/menu_items.hpp | 3 + .../include/hex/helpers/utils_macos.hpp | 1 + lib/libimhex/source/api/content_registry.cpp | 13 ++- lib/libimhex/source/helpers/macos_menu.m | 97 +++++++++++++++++++ lib/libimhex/source/helpers/utils_macos.m | 1 - main/gui/source/window/platform/macos.cpp | 1 + .../source/content/main_menu_items.cpp | 19 +++- plugins/builtin/source/content/ui_items.cpp | 14 +-- .../source/content/window_decoration.cpp | 5 + plugins/ui/source/ui/menu_items.cpp | 17 ++++ 11 files changed, 175 insertions(+), 14 deletions(-) diff --git a/lib/libimhex/include/hex/api/content_registry/user_interface.hpp b/lib/libimhex/include/hex/api/content_registry/user_interface.hpp index b3e68cbfe..251b0c0cc 100644 --- a/lib/libimhex/include/hex/api/content_registry/user_interface.hpp +++ b/lib/libimhex/include/hex/api/content_registry/user_interface.hpp @@ -68,6 +68,7 @@ EXPORT_MODULE namespace hex { constexpr static auto SeparatorValue = "$SEPARATOR$"; constexpr static auto SubMenuValue = "$SUBMENU$"; + constexpr static auto TaskBarMenuValue = "$TASKBAR$"; const std::multimap& getMainMenuItems(); @@ -199,6 +200,19 @@ EXPORT_MODULE namespace hex { */ void addMenuItemSeparator(std::vector unlocalizedMainMenuNames, u32 priority, View *view = nullptr); + /** + * @brief Adds a new main menu entry + * @param unlocalizedMainMenuNames The unlocalized names of the main menu entries + * @param priority The priority of the entry. Lower values are displayed first + * @param function The function to call when the entry is clicked + * @param enabledCallback The function to call to determine if the entry is enabled + */ + void addTaskBarMenuItem( + std::vector unlocalizedMainMenuNames, + u32 priority, + const impl::MenuCallback &function, + const impl::EnabledCallback& enabledCallback + ); /** * @brief Adds a new welcome screen entry @@ -220,10 +234,10 @@ EXPORT_MODULE namespace hex { /** * @brief Adds a menu item to the toolbar - * @param unlocalizedName Unlocalized name of the menu item + * @param unlocalizedNames Unlocalized name of the menu item * @param color Color of the toolbar icon */ - void addMenuItemToToolbar(const UnlocalizedString &unlocalizedName, ImGuiCustomCol color); + void addMenuItemToToolbar(const std::vector &unlocalizedNames, ImGuiCustomCol color); /** * @brief Reconstructs the toolbar items list after they have been modified diff --git a/lib/libimhex/include/hex/helpers/menu_items.hpp b/lib/libimhex/include/hex/helpers/menu_items.hpp index 30a51cad3..1802f2347 100644 --- a/lib/libimhex/include/hex/helpers/menu_items.hpp +++ b/lib/libimhex/include/hex/helpers/menu_items.hpp @@ -12,6 +12,9 @@ namespace hex::menu { bool beginMenu(const char *label, bool enabled = true); void endMenu(); + bool beginTaskBarMenu(); + void endTaskBarMenu(); + bool beginMenuEx(const char* label, const char* icon, bool enabled = true); bool menuItem(const char *label, const Shortcut &shortcut = Shortcut::None, bool selected = false, bool enabled = true); diff --git a/lib/libimhex/include/hex/helpers/utils_macos.hpp b/lib/libimhex/include/hex/helpers/utils_macos.hpp index bc76b94db..c059c208a 100644 --- a/lib/libimhex/include/hex/helpers/utils_macos.hpp +++ b/lib/libimhex/include/hex/helpers/utils_macos.hpp @@ -32,6 +32,7 @@ void macosInstallEventListener(); void toastMessageMacos(const char *title, const char *message); + void macosSetupDockMenu(void); } #endif diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index 279aec95c..82e566ce8 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -1044,6 +1044,15 @@ namespace hex { }); } + void addTaskBarMenuItem(std::vector unlocalizedMainMenuNames, u32 priority, const impl::MenuCallback &function, const impl::EnabledCallback& enabledCallback) { + log::debug("Added new taskbar menu item to menu {} ", unlocalizedMainMenuNames[0].get()); + + unlocalizedMainMenuNames.insert(unlocalizedMainMenuNames.begin(), impl::TaskBarMenuValue); + impl::s_menuItems->insert({ + priority, impl::MenuItem { .unlocalizedNames=unlocalizedMainMenuNames, .icon="", .shortcut=Shortcut::None, .view=nullptr, .callback=function, .enabledCallback=enabledCallback, .selectedCallback=[]{ return false; }, .toolbarIndex=-1 } + }); + } + void addWelcomeScreenEntry(const impl::DrawCallback &function) { impl::s_welcomeScreenEntries->push_back(function); } @@ -1056,13 +1065,13 @@ namespace hex { impl::s_toolbarItems->push_back(function); } - void addMenuItemToToolbar(const UnlocalizedString& unlocalizedName, ImGuiCustomCol color) { + void addMenuItemToToolbar(const std::vector& unlocalizedNames, ImGuiCustomCol color) { const auto maxIndex = std::ranges::max_element(impl::getMenuItems(), [](const auto &a, const auto &b) { return a.second.toolbarIndex < b.second.toolbarIndex; })->second.toolbarIndex; for (auto &[priority, menuItem] : *impl::s_menuItems) { - if (menuItem.unlocalizedNames.back() == unlocalizedName) { + if (menuItem.unlocalizedNames == unlocalizedNames) { menuItem.toolbarIndex = maxIndex + 1; menuItem.icon.color = color; updateToolbarItems(); diff --git a/lib/libimhex/source/helpers/macos_menu.m b/lib/libimhex/source/helpers/macos_menu.m index c099bb5ac..75bed4e6d 100644 --- a/lib/libimhex/source/helpers/macos_menu.m +++ b/lib/libimhex/source/helpers/macos_menu.m @@ -1,5 +1,6 @@ #import #import +#import struct KeyEquivalent { bool valid; @@ -192,6 +193,11 @@ bool macosBeginMenu(const char* label, const char *icon, bool enabled) { menuItem.title = title; [menuItem setSubmenu:newMenu]; + // Hide menus starting with '$' (used for special menus) + if (label[0] == '$') { + [menuItem setHidden:YES]; + } + if (icon != NULL) { NSString *iconString = [NSString stringWithUTF8String:icon]; NSImage* iconImage = imageFromIconFont(iconString, 16.0, [NSColor blackColor]); @@ -305,3 +311,94 @@ void macosSeparator(void) { [s_menuStack[s_menuStackSize - 1] addItem:separator]; } } + +@interface NSObject (DockMenuAddition) +- (NSMenu *)imhexApplicationDockMenu:(NSApplication *)sender; +@end + +@implementation NSObject (DockMenuAddition) + +- (NSMenu *)cloneMenu:(NSMenu *)originalMenu { + NSMenu *clonedMenu = [[NSMenu alloc] initWithTitle:[originalMenu title]]; + + for (NSMenuItem *item in [originalMenu itemArray]) { + NSMenuItem *clonedItem = [self cloneMenuItem:item]; + [clonedMenu addItem:clonedItem]; + } + + return clonedMenu; +} + +- (NSMenuItem *)cloneMenuItem:(NSMenuItem *)original { + if ([original isSeparatorItem]) { + return [NSMenuItem separatorItem]; + } + + // Create new item with same properties + NSMenuItem *clone = [[NSMenuItem alloc] initWithTitle:[original title] + action:[original action] + keyEquivalent:[original keyEquivalent]]; + + // Copy other properties + [clone setTarget:[original target]]; + [clone setEnabled:[original isEnabled]]; + [clone setImage:[original image]]; + [clone setTag:[original tag]]; + [clone setRepresentedObject:[original representedObject]]; + [clone setToolTip:[original toolTip]]; + [clone setState:[original state]]; + + // Handle submenus recursively + if ([original hasSubmenu]) { + NSMenu *clonedSubmenu = [self cloneMenu:[original submenu]]; + [clone setSubmenu:clonedSubmenu]; + } + + return clone; +} + +- (NSMenu *)imhexApplicationDockMenu:(NSApplication *)sender { + NSMenu *dockMenu = [[NSMenu alloc] init]; + + NSInteger menuIndex = [s_menuStack[s_menuStackSize - 1] indexOfItemWithTitle:@"$TASKBAR$"]; + if (menuIndex == -1) { + return dockMenu; + } + NSMenuItem *fileMenuItem = [s_menuStack[s_menuStackSize - 1] itemAtIndex:menuIndex]; + + // Get the File submenu + NSMenu *fileSubmenu = [fileMenuItem submenu]; + + if (fileSubmenu) { + // Clone each item from the File submenu directly into the dock menu + for (NSMenuItem *item in [fileSubmenu itemArray]) { + NSMenuItem *clonedItem = [self cloneMenuItem:item]; + [dockMenu addItem:clonedItem]; + } + } + + return dockMenu; +} + +@end + +void macosSetupDockMenu(void) { + @autoreleasepool { + // Get GLFW's delegate class + Class delegateClass = objc_getClass("GLFWApplicationDelegate"); + + if (delegateClass != nil) { + // Get our custom implementation + Method customMethod = class_getInstanceMethod([NSObject class], + @selector(imhexApplicationDockMenu:)); + + // Add the method to GLFW's delegate class + class_addMethod(delegateClass, + @selector(applicationDockMenu:), + method_getImplementation(customMethod), + method_getTypeEncoding(customMethod)); + } else { + NSLog(@"ERROR: Could not find GLFWApplicationDelegate class"); + } + } +} diff --git a/lib/libimhex/source/helpers/utils_macos.m b/lib/libimhex/source/helpers/utils_macos.m index ef97bb391..56ee0dbde 100644 --- a/lib/libimhex/source/helpers/utils_macos.m +++ b/lib/libimhex/source/helpers/utils_macos.m @@ -407,5 +407,4 @@ } } - #endif diff --git a/main/gui/source/window/platform/macos.cpp b/main/gui/source/window/platform/macos.cpp index 0947b2374..1ee544004 100644 --- a/main/gui/source/window/platform/macos.cpp +++ b/main/gui/source/window/platform/macos.cpp @@ -47,6 +47,7 @@ namespace hex { } enumerateFontsMacos(); + macosSetupDockMenu(); } void Window::setupNativeWindow() { diff --git a/plugins/builtin/source/content/main_menu_items.cpp b/plugins/builtin/source/content/main_menu_items.cpp index 0c0e09cdb..982c8f9a0 100644 --- a/plugins/builtin/source/content/main_menu_items.cpp +++ b/plugins/builtin/source/content/main_menu_items.cpp @@ -375,19 +375,34 @@ namespace hex::plugin::builtin { ContentRegistry::UserInterface::registerMainMenuItem("hex.builtin.menu.file", 1000); /* Create File */ - ContentRegistry::UserInterface::addMenuItem({ "hex.builtin.menu.file", "hex.builtin.menu.file.create_file" }, ICON_VS_FILE, 1050, CTRLCMD + Keys::N + AllowWhileTyping + ShowOnWelcomeScreen, [] { + const auto createFile = [] { auto newProvider = hex::ImHexApi::Provider::createProvider("hex.builtin.provider.mem_file", true); if (newProvider != nullptr && newProvider->open().isFailure()) hex::ImHexApi::Provider::remove(newProvider.get()); else EventProviderOpened::post(newProvider.get()); - }, noRunningTasks, ContentRegistry::Views::getViewByName("hex.builtin.view.hex_editor.name")); + }; + + ContentRegistry::UserInterface::addMenuItem({ "hex.builtin.menu.file", "hex.builtin.menu.file.create_file" }, ICON_VS_FILE, 1050, CTRLCMD + Keys::N + AllowWhileTyping + ShowOnWelcomeScreen, + createFile, + noRunningTasks, + ContentRegistry::Views::getViewByName("hex.builtin.view.hex_editor.name") + ); + + ContentRegistry::UserInterface::addTaskBarMenuItem({ "hex.builtin.menu.file.create_file" }, 100, + createFile, + noRunningTasks + ); /* Open File */ ContentRegistry::UserInterface::addMenuItem({ "hex.builtin.menu.file", "hex.builtin.menu.file.open_file" }, ICON_VS_FOLDER_OPENED, 1100, CTRLCMD + Keys::O + AllowWhileTyping + ShowOnWelcomeScreen, [] { RequestOpenWindow::post("Open File"); }, noRunningTasks, ContentRegistry::Views::getViewByName("hex.builtin.view.hex_editor.name")); + ContentRegistry::UserInterface::addTaskBarMenuItem({ "hex.builtin.menu.file.open_file" }, 200, [] { + RequestOpenWindow::post("Open File"); + }, noRunningTasks); + /* Open Other */ ContentRegistry::UserInterface::addMenuItemSubMenu({ "hex.builtin.menu.file", "hex.builtin.menu.file.open_other"}, ICON_VS_TELESCOPE, 1150, [] { for (const auto &[unlocalizedProviderName, icon] : ContentRegistry::Provider::impl::getEntries()) { diff --git a/plugins/builtin/source/content/ui_items.cpp b/plugins/builtin/source/content/ui_items.cpp index 0a06db57c..9342bceec 100644 --- a/plugins/builtin/source/content/ui_items.cpp +++ b/plugins/builtin/source/content/ui_items.cpp @@ -577,13 +577,13 @@ namespace hex::plugin::builtin { }); EventImHexStartupFinished::subscribe([] { - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.edit.undo", ImGuiCustomCol_ToolbarBlue); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.edit.redo", ImGuiCustomCol_ToolbarBlue); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.menu.file.create_file", ImGuiCustomCol_ToolbarGray); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.menu.file.open_file", ImGuiCustomCol_ToolbarBrown); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.file.save", ImGuiCustomCol_ToolbarBlue); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.file.save_as", ImGuiCustomCol_ToolbarBlue); - ContentRegistry::UserInterface::addMenuItemToToolbar("hex.builtin.menu.edit.bookmark.create", ImGuiCustomCol_ToolbarGreen); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.edit", "hex.builtin.view.hex_editor.menu.edit.undo" }, ImGuiCustomCol_ToolbarBlue); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.edit", "hex.builtin.view.hex_editor.menu.edit.redo" }, ImGuiCustomCol_ToolbarBlue); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.file", "hex.builtin.menu.file.create_file" }, ImGuiCustomCol_ToolbarGray); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.file", "hex.builtin.menu.file.open_file" }, ImGuiCustomCol_ToolbarBrown); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.file", "hex.builtin.view.hex_editor.menu.file.save" }, ImGuiCustomCol_ToolbarBlue); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.file", "hex.builtin.view.hex_editor.menu.file.save_as" }, ImGuiCustomCol_ToolbarBlue); + ContentRegistry::UserInterface::addMenuItemToToolbar({ "hex.builtin.menu.edit", "hex.builtin.menu.edit.bookmark.create" }, ImGuiCustomCol_ToolbarGreen); }); } diff --git a/plugins/builtin/source/content/window_decoration.cpp b/plugins/builtin/source/content/window_decoration.cpp index a99d2c0e7..ebebfe6dd 100644 --- a/plugins/builtin/source/content/window_decoration.cpp +++ b/plugins/builtin/source/content/window_decoration.cpp @@ -470,6 +470,11 @@ namespace hex::plugin::builtin { } } } + + if (menu::beginTaskBarMenu()) { + populateMenu(ContentRegistry::UserInterface::impl::TaskBarMenuValue); + menu::endTaskBarMenu(); + } } void drawMainMenu([[maybe_unused]] float menuBarHeight) { diff --git a/plugins/ui/source/ui/menu_items.cpp b/plugins/ui/source/ui/menu_items.cpp index 0148c46c7..282f8fc3b 100644 --- a/plugins/ui/source/ui/menu_items.cpp +++ b/plugins/ui/source/ui/menu_items.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #if defined(OS_MACOS) extern "C" { @@ -98,6 +99,22 @@ namespace hex::menu { ImGui::EndMenu(); } + bool beginTaskBarMenu() { + #if defined(OS_MACOS) + return beginMenu(ContentRegistry::UserInterface::impl::TaskBarMenuValue, true); + #else + return false; + #endif + } + + void endTaskBarMenu() { + #if defined(OS_MACOS) + endMenu(); + #else + + #endif + } + bool menuItem(const char *label, const Shortcut &shortcut, bool selected, bool enabled) { #if defined(OS_MACOS)