From 715c1346b47ca94233c0d36268b53a367f5bf471 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 13 Jan 2026 20:05:23 -0700 Subject: [PATCH] EditorWindow: use an application menu in the main menubar Fixes #153 --- locales/en.catkeys | 6 +- src/App.cpp | 6 + src/App.h | 3 +- src/editor/EditorWindow.cpp | 55 ++++++- src/editor/EditorWindow.h | 5 + src/support/IconMenuItem.cpp | 298 +++++++++++++++++++++++++++++++++++ src/support/IconMenuItem.h | 51 ++++++ 7 files changed, 418 insertions(+), 6 deletions(-) create mode 100644 src/support/IconMenuItem.cpp create mode 100644 src/support/IconMenuItem.h diff --git a/locales/en.catkeys b/locales/en.catkeys index 1c949d3..057449f 100644 --- a/locales/en.catkeys +++ b/locales/en.catkeys @@ -1,4 +1,4 @@ -1 English x-vnd.KapiX-Koder 3292494141 +1 English x-vnd.KapiX-Koder 2982779499 Something wrong has happened while opening the configuration file. Your personal settings will not be %s%. Preferences Something wrong has happened while opening the configuration file. Your personal settings will not be %s%. Access denied EditorWindow Access denied Line endings EditorWindow Line endings @@ -24,6 +24,7 @@ Special symbols EditorWindow Special symbols Show line endings EditorWindow Show line endings Undo EditorWindow Undo Comment line EditorWindow Comment line +Open project website EditorWindow Open project website Show full path in title AppPreferencesWindow Show full path in title Close EditorWindow Close Spaces per tab: AppPreferencesWindow Spaces per tab: @@ -76,6 +77,7 @@ save Preferences to _ the configuration save Up to the next non-empty line AppPreferencesWindow Up to the next non-empty line Replace all FindWindow Replace all Koder System name Koder +View or submit bug reports EditorWindow View or submit bug reports Extra large AppPreferencesWindow Toolbar icon size Extra large Stack new windows AppPreferencesWindow Stack new windows Find selection EditorWindow Find selection @@ -116,6 +118,7 @@ Quit EditorWindow Quit Redo EditorWindow Redo Go to line: GoToLineWindow Go to line: Editor settings Preferences Editor settings +Windows EditorWindow Windows You don't have sufficient permissions to edit this file. EditorWindow You don't have sufficient permissions to edit this file. Convert tabs to spaces AppPreferencesWindow Convert tabs to spaces Search EditorWindow Search @@ -134,6 +137,7 @@ Untitled EditorWindow Untitled Unknown error. Preferences Unknown error. Find FindWindow Find Delete BookmarksListView Delete +Preferences… EditorWindow Preferences… Reload EditorWindow Reload Go to line GoToLineWindow Go to line There are unsaved changes.\nSelect the files to save. QuitAlert There are unsaved changes.\nSelect the files to save. diff --git a/src/App.cpp b/src/App.cpp index ffcb4b3..88768d6 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -266,6 +266,12 @@ App::MessageReceived(BMessage* message) case SUPPRESS_INITIAL_WINDOW: { fSuppressInitialWindow = true; } break; + case ACTIVATE_WINDOW: { + BWindow* window = nullptr; + if(message->FindPointer("window", (void**) &window) == B_OK && window != nullptr) { + window->Activate(); + } + } break; case ACTIVE_WINDOW_CHANGED: { if(message->FindPointer("window", (void**) &fLastActiveWindow) != B_OK) { fLastActiveWindow = nullptr; diff --git a/src/App.h b/src/App.h index 695e9e7..f15ccfe 100644 --- a/src/App.h +++ b/src/App.h @@ -25,7 +25,8 @@ class Styler; enum { SUPPRESS_INITIAL_WINDOW = 'Siwn', - WINDOW_NEW_WITH_QUIT_REPLY = 'NWwn' + WINDOW_NEW_WITH_QUIT_REPLY = 'NWwn', + ACTIVATE_WINDOW = 'actw' }; diff --git a/src/editor/EditorWindow.cpp b/src/editor/EditorWindow.cpp index a5e1c05..31def38 100644 --- a/src/editor/EditorWindow.cpp +++ b/src/editor/EditorWindow.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "App.h" @@ -37,6 +38,7 @@ #include "FindReplaceHandler.h" #include "FindWindow.h" #include "GoToLineWindow.h" +#include "IconMenuItem.h" #include "Languages.h" #include "Preferences.h" #include "ScintillaUtils.h" @@ -85,8 +87,20 @@ EditorWindow::EditorWindow(bool stagger) fOpenPanel->SetMessage(&openMessage); fSavePanel = new BFilePanel(B_SAVE_PANEL, windowMessenger, nullptr, 0, false); + BMenu* appMenu = new BMenu(""); + fWindowsMenu = new BMenu(B_TRANSLATE("Windows")); + BLayoutBuilder::Menu<>(appMenu) + .AddItem(B_TRANSLATE("About" B_UTF8_ELLIPSIS), B_ABOUT_REQUESTED) + .AddSeparator() + .AddMenu(fWindowsMenu) + .End() + .AddItem(B_TRANSLATE("Preferences" B_UTF8_ELLIPSIS), MAINMENU_EDIT_APP_PREFERENCES) + .AddSeparator() + .AddItem(B_TRANSLATE("Quit"), MAINMENU_FILE_QUIT, 'Q'); + fMainMenu = new BMenuBar("MainMenu"); BLayoutBuilder::Menu<>(fMainMenu) + .AddItem(new IconMenuItem(appMenu, nullptr, gAppMime, B_MINI_ICON)) .AddMenu(B_TRANSLATE("File")) .AddItem(B_TRANSLATE("New"), MAINMENU_FILE_NEW, 'N') .AddSeparator() @@ -101,7 +115,6 @@ EditorWindow::EditorWindow(bool stagger) .AddItem(B_TRANSLATE("Open partner file"), MAINMENU_FILE_OPEN_CORRESPONDING, 'O', B_OPTION_KEY) .AddSeparator() .AddItem(B_TRANSLATE("Close"), B_QUIT_REQUESTED, 'W') - .AddItem(B_TRANSLATE("Quit"), MAINMENU_FILE_QUIT, 'Q') .End() .AddMenu(B_TRANSLATE("Edit")) .AddItem(B_TRANSLATE("Undo"), B_UNDO, 'Z') @@ -123,9 +136,8 @@ EditorWindow::EditorWindow(bool stagger) .AddItem(B_TRANSLATE("Windows format"), MAINMENU_EDIT_CONVERTEOLS_WINDOWS) .AddItem(B_TRANSLATE("Old Mac format"), MAINMENU_EDIT_CONVERTEOLS_MAC) .End() - .AddSeparator() + //.AddSeparator() //.AddItem(B_TRANSLATE("File preferences" B_UTF8_ELLIPSIS), MAINMENU_EDIT_FILE_PREFERENCES) - .AddItem(B_TRANSLATE("Koder preferences" B_UTF8_ELLIPSIS), MAINMENU_EDIT_APP_PREFERENCES) .End() .AddMenu(B_TRANSLATE("View")) .AddMenu(B_TRANSLATE("Special symbols")) @@ -153,7 +165,8 @@ EditorWindow::EditorWindow(bool stagger) .AddItem("Dummy", MAINMENU_LANGUAGE) .End() .AddMenu(B_TRANSLATE("Help")) - .AddItem(B_TRANSLATE("About" B_UTF8_ELLIPSIS), B_ABOUT_REQUESTED) + .AddItem(B_TRANSLATE("Open project website"), MAINMENU_HELP_PROJECT) + .AddItem(B_TRANSLATE("View or submit bug reports"), MAINMENU_HELP_ISSUES) .End(); // When changing this shortcut remember to update one in StatusView as well @@ -673,6 +686,12 @@ EditorWindow::MessageReceived(BMessage* message) fEditor->SendMessage(SCI_SETWRAPMODE, fPreferences->fWrapLines ? SC_WRAP_WORD : SC_WRAP_NONE, 0); } break; + case MAINMENU_HELP_PROJECT: { + BUrl("https://github.com/KapiX/Koder", true).OpenWithPreferredApplication(); + } break; + case MAINMENU_HELP_ISSUES: { + BUrl("https://github.com/KapiX/Koder/issues", true).OpenWithPreferredApplication(); + } break; case MAINMENU_LANGUAGE: { _SetLanguage(message->GetString("lang", "text")); } break; @@ -932,6 +951,34 @@ EditorWindow::Show() } +void +EditorWindow::MenusBeginning() +{ + if(fWindowsMenu == nullptr) { + return; + } + for(int32 x = fWindowsMenu->CountItems() - 1; x >= 0; x--) { + delete fWindowsMenu->ItemAt(x); + } + for(int32 x = 0; x < be_app->CountWindows(); x++) { + // use a dynamic_cast to filter out other window types (find, save, bookmarks, ...) + EditorWindow* window = dynamic_cast(be_app->WindowAt(x)); + if(window == nullptr) + continue; + + BMessage* message = new BMessage(ACTIVATE_WINDOW); + message->AddPointer("window", window); + BMenuItem* menuItem = new BMenuItem(window->Title(), message); + if(window == this) { + menuItem->SetEnabled(false); + menuItem->SetMarked(true); + } + fWindowsMenu->AddItem(menuItem); + } + fWindowsMenu->SetTargetForItems(be_app); +} + + const char* EditorWindow::OpenedFilePath() { diff --git a/src/editor/EditorWindow.h b/src/editor/EditorWindow.h index 52cd385..4051571 100644 --- a/src/editor/EditorWindow.h +++ b/src/editor/EditorWindow.h @@ -80,6 +80,9 @@ enum { MAINMENU_SEARCH_PREVBOOKMARK = 'mpbk', MAINMENU_SEARCH_GOTOLINE = 'msgl', + MAINMENU_HELP_PROJECT = 'hlpp', + MAINMENU_HELP_ISSUES = 'hlpi', + MAINMENU_LANGUAGE = 'ml00', MAINMENU_OPEN_RECENT = 'mr00', @@ -114,6 +117,7 @@ class EditorWindow : public BWindow { void WindowActivated(bool active); void FrameMoved(BPoint origin); void Show(); + void MenusBeginning(); bool IsModified() { return fModified; } const char* OpenedFilePath(); @@ -148,6 +152,7 @@ class EditorWindow : public BWindow { BFilePanel* fSavePanel; BMenu* fOpenRecentMenu; BMenu* fLanguageMenu; + BMenu* fWindowsMenu; std::string fCurrentLanguage; ToolBar* fToolbar; StatusView* fStatusView; diff --git a/src/support/IconMenuItem.cpp b/src/support/IconMenuItem.cpp new file mode 100644 index 0000000..56e8b90 --- /dev/null +++ b/src/support/IconMenuItem.cpp @@ -0,0 +1,298 @@ +/* + * Copyright 2023 Nexus6 + * All rights reserved. Distributed under the terms of the MIT license. + * Parts are taken from the IconMenuItem class from Haiku (Tracker) under the + * Open Tracker Licence + * Copyright (c) 1991-2000, Be Incorporated. All rights reserved. + * + * dospuntos (Johan Wagenheim) + */ + +#include "IconMenuItem.h" + +#include +#include +#include +#include +#include + + +// #include "IconCache.h" + +IconMenuItem::IconMenuItem(const char* label, BMessage* message, BBitmap* icon, icon_size which) + : BMenuItem(label, message), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(which) +{ + _SetIcon(icon); + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +IconMenuItem::IconMenuItem( + const char* label, BMessage* message, const BNodeInfo* nodeInfo, icon_size which) + : BMenuItem(label, message), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(which) +{ + if (nodeInfo != NULL) { + fDeviceIcon = new BBitmap( + BRect(BPoint(0, 0), be_control_look->ComposeIconSize(which)), kDefaultIconDepth); + if (nodeInfo->GetTrackerIcon(fDeviceIcon, (icon_size)-1) != B_OK) { + delete fDeviceIcon; + fDeviceIcon = NULL; + } + } + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +IconMenuItem::IconMenuItem( + const char* label, BMessage* message, const char* iconType, icon_size which) + : BMenuItem(label, message), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(which) +{ + BMimeType mime(iconType); + fDeviceIcon = new BBitmap( + BRect(BPoint(0, 0), be_control_look->ComposeIconSize(which)), kDefaultIconDepth); + + if (mime.GetIcon(fDeviceIcon, which) != B_OK) { + BMimeType super; + mime.GetSupertype(&super); + if (super.GetIcon(fDeviceIcon, which) != B_OK) { + delete fDeviceIcon; + fDeviceIcon = NULL; + } + } + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +IconMenuItem::IconMenuItem(BMenu* submenu, BMessage* message, const char* iconType, icon_size which) + : BMenuItem(submenu, message), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(which) +{ + BMimeType mime(iconType); + fDeviceIcon = new BBitmap( + BRect(BPoint(0, 0), be_control_look->ComposeIconSize(which)), kDefaultIconDepth); + + if (mime.GetIcon(fDeviceIcon, which) != B_OK) { + BMimeType super; + mime.GetSupertype(&super); + if (super.GetIcon(fDeviceIcon, which) != B_OK) { + delete fDeviceIcon; + fDeviceIcon = NULL; + } + } + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +IconMenuItem::IconMenuItem(BMenu* menu, BMessage* message, BBitmap* icon, icon_size which) + : BMenuItem(menu, message), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(which) +{ + _SetIcon(icon); + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +IconMenuItem::IconMenuItem(BMessage* data) + : BMenuItem(data), + fDeviceIcon(NULL), + fHeightDelta(0), + fWhich(B_MINI_ICON) +{ + if (data != NULL) { + fWhich = (icon_size)data->GetInt32("_which", B_MINI_ICON); + + fDeviceIcon = new BBitmap( + BRect(BPoint(0, 0), be_control_look->ComposeIconSize(fWhich)), kDefaultIconDepth); + + if (data->HasData("_deviceIconBits", B_RAW_TYPE)) { + ssize_t numBytes; + const void* bits; + if (data->FindData("_deviceIconBits", B_RAW_TYPE, &bits, &numBytes) == B_OK) + fDeviceIcon->SetBits(bits, numBytes, (int32)0, kDefaultIconDepth); + } + } + + // IconMenuItem is used in synchronously invoked menus, make sure + // we invoke with a timeout + SetTimeout(kSynchMenuInvokeTimeout); +} + + +BArchivable* +IconMenuItem::Instantiate(BMessage* data) +{ + // if (validate_instantiation(data, "IconMenuItem")) + return new IconMenuItem(data); + + return NULL; +} + + +status_t +IconMenuItem::Archive(BMessage* data, bool deep) const +{ + status_t result = _inherited::Archive(data, deep); + + if (result == B_OK) + result = data->AddInt32("_which", (int32)fWhich); + + if (result == B_OK && fDeviceIcon != NULL) { + result = data->AddData( + "_deviceIconBits", B_RAW_TYPE, fDeviceIcon->Bits(), fDeviceIcon->BitsLength()); + } + + return result; +} + + +IconMenuItem::~IconMenuItem() +{ + delete fDeviceIcon; +} + + +void +IconMenuItem::GetContentSize(float* width, float* height) +{ + _inherited::GetContentSize(width, height); + + int32 iconHeight = fWhich; + if (fDeviceIcon != NULL) + iconHeight = fDeviceIcon->Bounds().IntegerHeight() + 1; + + fHeightDelta = iconHeight - *height; + if (*height < iconHeight) + *height = iconHeight; + + *width += 20; +} + + +void +IconMenuItem::DrawContent() +{ + BPoint drawPoint(ContentLocation()); + if (fDeviceIcon != NULL) + drawPoint.x += (fDeviceIcon->Bounds().Width() + 1) + 4.0f; + + if (fHeightDelta > 0) + drawPoint.y += ceilf(fHeightDelta / 2); + + Menu()->MovePenTo(drawPoint); + _inherited::DrawContent(); + + Menu()->PushState(); + + BPoint where(ContentLocation()); + float deltaHeight = fHeightDelta < 0 ? -fHeightDelta : 0; + where.y += ceilf(deltaHeight / 2); + + if (fDeviceIcon != NULL) { + if (IsEnabled()) { + Menu()->SetDrawingMode(B_OP_ALPHA); + } else { + Menu()->SetDrawingMode(B_OP_ALPHA); + Menu()->SetHighColor(0, 0, 0, 64); + Menu()->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); + } + Menu()->DrawBitmapAsync(fDeviceIcon, where); + } + + Menu()->PopState(); +} + + +void +IconMenuItem::SetMarked(bool mark) +{ + _inherited::SetMarked(mark); + + if (!mark) + return; + + // we are marking the item + + BMenu* menu = Menu(); + if (menu == NULL) + return; + + // we have a parent menu + + BMenu* _menu = menu; + while ((_menu = _menu->Supermenu()) != NULL) + menu = _menu; + + // went up the hierarchy to found the topmost menu + + if (menu == NULL || menu->Parent() == NULL) + return; + + // our topmost menu has a parent + + if (dynamic_cast(menu->Parent()) == NULL) + return; + + // our topmost menu's parent is a BMenuField + + BMenuItem* topLevelItem = menu->ItemAt((int32)0); + + if (topLevelItem == NULL) + return; + + // our topmost menu has a menu item + + IconMenuItem* topLevelIconMenuItem = dynamic_cast(topLevelItem); + if (topLevelIconMenuItem == NULL) + return; + + // our topmost menu's item is an IconMenuItem + + // update the icon + topLevelIconMenuItem->_SetIcon(fDeviceIcon); + menu->Invalidate(); +} + + +void +IconMenuItem::_SetIcon(BBitmap* icon) +{ + if (icon != NULL) { + if (fDeviceIcon != NULL) + delete fDeviceIcon; + + fDeviceIcon = new BBitmap( + BRect(BPoint(0, 0), be_control_look->ComposeIconSize(fWhich)), icon->ColorSpace()); + fDeviceIcon->ImportBits(icon); + } else { + delete fDeviceIcon; + fDeviceIcon = NULL; + } +} diff --git a/src/support/IconMenuItem.h b/src/support/IconMenuItem.h new file mode 100644 index 0000000..94a701a --- /dev/null +++ b/src/support/IconMenuItem.h @@ -0,0 +1,51 @@ +/* + * Copyright 2023 Nexus6 + * All rights reserved. Distributed under the terms of the MIT license. + * Parts are taken from the IconMenuItem class from Haiku (Tracker) under the + * Open Tracker Licence + * Copyright (c) 1991-2000, Be Incorporated. All rights reserved. + */ +#ifndef ICON_MENU_ITEM_H +#define ICON_MENU_ITEM_H + + +#include +#include + +class BBitmap; +class BNodeInfo; + +const bigtime_t kSynchMenuInvokeTimeout = 5000000; +const color_space kDefaultIconDepth = B_RGBA32; + +class IconMenuItem : public BMenuItem { +public: + IconMenuItem( + const char* label, BMessage* message, BBitmap* icon, icon_size which = B_MINI_ICON); + IconMenuItem( + const char* label, BMessage* message, const char* iconType, icon_size which = B_MINI_ICON); + IconMenuItem(const char* label, BMessage* message, const BNodeInfo* nodeInfo, icon_size which); + IconMenuItem(BMenu*, BMessage*, const char* iconType, icon_size which = B_MINI_ICON); + IconMenuItem(BMenu*, BMessage*, BBitmap* icon, icon_size which = B_MINI_ICON); + IconMenuItem(BMessage* data); + virtual ~IconMenuItem(); + + static BArchivable* Instantiate(BMessage* data); + virtual status_t Archive(BMessage* data, bool deep = true) const; + + virtual void GetContentSize(float* width, float* height); + virtual void DrawContent(); + virtual void SetMarked(bool mark); + +private: + virtual void _SetIcon(BBitmap* icon); + +private: + BBitmap* fDeviceIcon; + float fHeightDelta; + icon_size fWhich; + + typedef BMenuItem _inherited; +}; + +#endif // ICON_MENU_ITEM_H