From 915e92e7e0e27c14dec6f80677026d5ebf32c793 Mon Sep 17 00:00:00 2001 From: sinterdev <189787597+sinterdev@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:02:39 -0500 Subject: [PATCH] Adding menu support. Fixes #44. --- Photino.Native/Exports.cpp | 234 +++++++- Photino.Native/Photino.Errors.cpp | 25 + Photino.Native/Photino.Errors.h | 35 ++ Photino.Native/Photino.Menu.h | 9 + Photino.Native/Photino.Native.vcxproj | 5 + Photino.Native/Photino.Windows.Menu.cpp | 742 ++++++++++++++++++++++++ Photino.Native/Photino.Windows.Menu.h | 344 +++++++++++ Photino.Native/Photino.Windows.cpp | 9 +- Photino.Native/Photino.h | 4 +- 9 files changed, 1402 insertions(+), 5 deletions(-) create mode 100644 Photino.Native/Photino.Errors.cpp create mode 100644 Photino.Native/Photino.Errors.h create mode 100644 Photino.Native/Photino.Menu.h create mode 100644 Photino.Native/Photino.Windows.Menu.cpp create mode 100644 Photino.Native/Photino.Windows.Menu.h diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 98d4904..785a90c 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -1,5 +1,8 @@ #include "Photino.Dialog.h" +#include "Photino.Errors.h" +#include "Photino.Menu.h" #include "Photino.h" +#include #ifdef _WIN32 #define EXPORTED __declspec(dllexport) @@ -292,6 +295,235 @@ extern "C" return inst->GetDialog()->ShowMessage(title, text, buttons, icon); } + //Error + EXPORTED void Photino_ClearErrorMessage() + { + ClearErrorMessage(); + } + + EXPORTED void Photino_GetErrorMessage(int length, char* buffer) + { + GetErrorMessage(length, buffer); + } + + EXPORTED void Photino_GetErrorMessageLength(int* length) + { + *length = GetErrorMessageLength(); + } + +#ifdef _WIN32 + //Menu + EXPORTED PhotinoErrorKind Photino_Menu_Create(Menu** menu) + { + try + { + *menu = new Menu(); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_Destroy(Menu* menu) + { + try + { + menuRenderer.Destroy(menu); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_AddMenuItem(Menu* menu, MenuItem* menuItem) + { + try + { + menu->Add(menuItem); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_AddMenuSeparator(Menu* menu, MenuSeparator* menuSeparator) + { + try + { + menu->Add(menuSeparator); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_AddOnClicked(Menu* menu, OnClickedCallback onClicked) + { + try + { + menu->AddOnClicked(onClicked); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_Hide(Menu* menu) + { + try + { + menu->Hide(); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_RemoveOnClicked(Menu* menu, OnClickedCallback onClicked) + { + try + { + menu->RemoveOnClicked(onClicked); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_Menu_Show(Menu* menu, Photino* window, int x, int y) + { + try + { + menu->Show(window, x, y); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuItem_Create(MenuItemOptions options, MenuItem** menuItem) + { + try + { + *menuItem = new MenuItem(options); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuItem_AddMenuItem(MenuItem* menuItem, MenuItem* newMenuItem) + { + try + { + menuItem->Add(newMenuItem); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuItem_AddMenuSeparator(MenuItem* menuItem, MenuSeparator* menuSeparator) + { + try + { + menuItem->Add(menuSeparator); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuItem_Destroy(MenuItem* menuItem) + { + try + { + delete menuItem; + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuSeparator_Create(MenuSeparator** menuSeparator) + { + try + { + *menuSeparator = new MenuSeparator(); + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } + + EXPORTED PhotinoErrorKind Photino_MenuSeparator_Destroy(MenuSeparator* menuSeparator) + { + try + { + delete menuSeparator; + } + catch (std::exception exception) + { + SetErrorMessage(exception.what()); + return PhotinoErrorKind::GenericError; + } + + return PhotinoErrorKind::NoError; + } +#endif // _WIN32 + //Callbacks @@ -334,4 +566,4 @@ extern "C" { instance->Invoke(callback); } -} +} \ No newline at end of file diff --git a/Photino.Native/Photino.Errors.cpp b/Photino.Native/Photino.Errors.cpp new file mode 100644 index 0000000..65a4ea3 --- /dev/null +++ b/Photino.Native/Photino.Errors.cpp @@ -0,0 +1,25 @@ +#include "Photino.Errors.h" +#include +#include + +static thread_local std::string _lastError; + +void ClearErrorMessage() noexcept +{ + _lastError.clear(); +} + +int GetErrorMessageLength() noexcept +{ + return _lastError.length(); +} + +void GetErrorMessage(int length, char* buffer) noexcept +{ + _lastError.copy(buffer, length, 0); +} + +void SetErrorMessage(std::string message) noexcept +{ + _lastError = message; +} \ No newline at end of file diff --git a/Photino.Native/Photino.Errors.h b/Photino.Native/Photino.Errors.h new file mode 100644 index 0000000..5c071df --- /dev/null +++ b/Photino.Native/Photino.Errors.h @@ -0,0 +1,35 @@ +#pragma once +#include + +/// +/// The kind of error message that occurred. +/// +enum class PhotinoErrorKind +{ + NoError = 0, + GenericError +}; + +/// +/// Clears the last error message to occur. +/// +void ClearErrorMessage() noexcept; + +/// +/// Gets the length of the message of the last error to occur. +/// +/// The length of the last error to occur on this thread. +int GetErrorMessageLength() noexcept; + +/// +/// Gets the message of the last error to occur. +/// +/// The length of the buffer. +/// The buffer to which the message should be written. +void GetErrorMessage(int length, char* buffer) noexcept; + +/// +/// Sets the message of the last error to occur. +/// +/// The message. +void SetErrorMessage(std::string message) noexcept; \ No newline at end of file diff --git a/Photino.Native/Photino.Menu.h b/Photino.Native/Photino.Menu.h new file mode 100644 index 0000000..692c973 --- /dev/null +++ b/Photino.Native/Photino.Menu.h @@ -0,0 +1,9 @@ +#pragma once + +#ifdef __APPLE__ +// TODO: Implement this. +#elif __linux__ +// TODO: Implement this. +#else +#include "Photino.Windows.Menu.h" +#endif \ No newline at end of file diff --git a/Photino.Native/Photino.Native.vcxproj b/Photino.Native/Photino.Native.vcxproj index ac5657a..a29df93 100644 --- a/Photino.Native/Photino.Native.vcxproj +++ b/Photino.Native/Photino.Native.vcxproj @@ -243,15 +243,18 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\ARM64\Debug\net8.0\ + + + @@ -260,6 +263,8 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\ARM64\Debug\net8.0\ + + diff --git a/Photino.Native/Photino.Windows.Menu.cpp b/Photino.Native/Photino.Windows.Menu.cpp new file mode 100644 index 0000000..6dca87d --- /dev/null +++ b/Photino.Native/Photino.Windows.Menu.cpp @@ -0,0 +1,742 @@ +#include +#include +#include +#include +#include +#include "Photino.h" +#include "Photino.Windows.Menu.h" +#pragma comment(lib, "atls.lib") + +// ---------------------------------------------------------------------------- +// Variables +// ---------------------------------------------------------------------------- + +/// +/// The id of the last menu item that was created. +/// +static std::atomic lastMenuItemId; + +/// +/// The id of the last window class that was created. +/// +static std::atomic lastWindowClassId; + +/// +/// The menu renderer for this application. +/// +MenuRenderer menuRenderer{}; + +// ---------------------------------------------------------------------------- +// Utilities +// ---------------------------------------------------------------------------- + +/// +/// Generates a unique window class name. +/// +/// The window class name. +static std::wstring GenerateClassName() +{ + return L"Win32Window" + std::to_wstring(lastWindowClassId++); +} + +/// +/// Reads and decodes the image at the given path using WIC. +/// +/// The path of the image. +/// A handle to the win32 bitmap resource. +static +std::unique_ptr::type, Win32BitmapDeleter> +Win32_CreateImage(const std::wstring path) +{ + CComPtr factory; + + { + auto result = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&factory)); + + if (!SUCCEEDED(result)) + { + throw Win32Exception(); + } + } + + CComPtr decoder; + + { + auto result = factory->CreateDecoderFromFilename( + path.c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnDemand, + &decoder); + + if (!SUCCEEDED(result)) + { + throw Win32Exception(result); + } + } + + CComPtr frame; + + { + auto result = decoder->GetFrame(0, &frame); + + if (!SUCCEEDED(result)) + { + throw Win32Exception(); + } + } + + UINT height; + UINT width; + + if (!SUCCEEDED(frame->GetSize(&width, &height))) + { + throw Win32Exception(); + } + + std::vector buffer(width * height * 4); + + { + auto result = frame->CopyPixels( + nullptr, + width * 4, + buffer.size(), + buffer.data()); + + if (!SUCCEEDED(result)) + { + throw Win32Exception(); + } + } + + auto bitmap = CreateBitmap(width, height, 1, 32, buffer.data()); + + if (bitmap == nullptr) + { + throw Win32Exception(); + } + + return std::unique_ptr::type, Win32BitmapDeleter>( + bitmap, + Win32BitmapDeleter{}); +} + +/// +/// Creates a popup menu. +/// +/// A handle to the menu resource. +static +std::unique_ptr::type, Win32MenuDeleter> +Win32_CreateMenu() +{ + auto result = CreatePopupMenu(); + + if (!result) + { + throw Win32Exception{}; + } + + return std::unique_ptr::type, Win32MenuDeleter>( + result, + Win32MenuDeleter{}); +} + +/// +/// Creates a dummy window to host popup menus. +/// +/// +/// A handle to the window class for the window. +/// +/// A handle to the window resource. +static +std::unique_ptr::type, Win32WindowDeleter> +Win32_CreateWindow(const ATOM windowClass) +{ + auto result = CreateWindow( + reinterpret_cast(windowClass), + nullptr, + 0, + 0, + 0, + 0, + 0, + nullptr, + nullptr, + GetModuleHandle(nullptr), + nullptr); + + if (!result) + { + throw Win32Exception{}; + } + + return std::unique_ptr::type, Win32WindowDeleter>( + result, + Win32WindowDeleter{}); +} + +/// +/// Creates a dummy window class for a window hosting popup menus. +/// +/// The window's message handler. +/// A handle to the window class resource. +static +std::unique_ptr::type, Win32WindowClassDeleter> +Win32_CreateWindowClass(const WNDPROC wndProc) +{ + std::wstring className = GenerateClassName(); + + WNDCLASSEX wndClass; + + wndClass.cbSize = sizeof(wndClass); + wndClass.lpfnWndProc = wndProc; + wndClass.cbClsExtra = 0; + wndClass.cbWndExtra = 0; + wndClass.hInstance = GetModuleHandle(nullptr); + wndClass.hIcon = nullptr; + wndClass.hCursor = LoadIcon(nullptr, IDC_ARROW); + wndClass.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); + wndClass.lpszMenuName = nullptr; + wndClass.lpszClassName = className.c_str(); + wndClass.hIconSm = nullptr; + + auto result = RegisterClassEx(&wndClass); + + if (!result) + { + throw Win32Exception{}; + } + + return std::unique_ptr::type, Win32WindowClassDeleter>( + reinterpret_cast(result), + Win32WindowClassDeleter{ wndClass.hInstance }); +} + +/// +/// Converts the given UTF16 string to UTF8. +/// +/// The UTF16 string. +/// The UTF8 string. +static std::string ToUTF8(const std::wstring& value) +{ + std::wstring_convert> converter; + return converter.to_bytes(value); +} + +/// +/// Converts the given UTF8 string to UTF16. +/// +/// The UTF8 string. +/// The UTF16 string. +static std::wstring ToUTF16(const std::string& value) +{ + std::wstring_convert> converter; + return converter.from_bytes(value); +} + +/// +/// Retrieves an error string representing the given error code. +/// +/// The error code. +/// The error string. +static std::string ToLastErrorString(const DWORD lastError) +{ + LPTSTR errorText = nullptr; + + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + lastError, + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + reinterpret_cast(&errorText), + 0, + nullptr); + + if (errorText == nullptr) + { + return "Failed to format win32 error."; + } + + // Yes, converting to UTF-8 only to convert back to UTF-16 when we reach + // C# is wasteful, but STL exceptions are UTF-8 and this is supposed to be + // an exceptional circumstance (i.e., we are already paying to throw). + + std::wstring errorString(errorText); + LocalFree(errorText); + return ToUTF8(errorString); +} + +// --------------------------------------------------------------------------- +// MenuRenderer +// --------------------------------------------------------------------------- + +static LRESULT MenuRendererWndProc( + HWND hWnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) noexcept +{ + switch (msg) + { + case WM_USER: + { + auto menuRenderer = reinterpret_cast( + GetWindowLongPtr(hWnd, GWLP_USERDATA)); + + if (menuRenderer == nullptr) + { + break; // TODO: Log this error. + } + + try + { + auto message = reinterpret_cast(lParam); + message->Execute(*menuRenderer, hWnd); + delete message; + } + catch (std::exception) + { + // TODO: Log this error. + } + + break; + } + } + + return DefWindowProc(hWnd, msg, wParam, lParam); +} + +MenuRenderer::MenuRenderer() + : _activeMenu(NULL) + , _thread(MenuRenderer::PumpMessages, this) +{ +} + +MenuRenderer::~MenuRenderer() +{ + if (_hWnd != nullptr) + { + PostMessage(_hWnd.get(), WM_CLOSE, 0, 0); + } + + _thread.join(); +} + +void MenuRenderer::Destroy(Menu* menu) +{ + // First, post `DestroyMenuCommand` since `TrackPopupMenuEx` begins a + // modal window and cannot process this message until the menu is + // dismissed. + + PostMessage( + _hWnd.get(), + WM_USER, + 0, + reinterpret_cast(new DestroyMenuCommand(menu))); + + // Then, once the message is queued, attempt to hide the menu if + // it is currently being tracked. This frees up the event loop to + // process the queued `DestroyMenuCommand`. + + Hide(menu); +} + +void MenuRenderer::Hide(const Menu* menu) +{ + std::lock_guard lock(_activeMenuMutex); + + if (_activeMenu == menu) + { + PostMessage(_hWnd.get(), WM_CANCELMODE, 0, 0); + } +} + +void MenuRenderer::PumpMessages(MenuRenderer* renderer) noexcept +{ + try + { + auto windowClass = Win32_CreateWindowClass(MenuRendererWndProc); + auto window = Win32_CreateWindow(reinterpret_cast(windowClass.get())); + SetWindowLongPtr(window.get(), GWLP_USERDATA, reinterpret_cast(renderer)); + renderer->_hWnd = std::move(window); + + while (true) + { + MSG msg; + + switch (GetMessage(&msg, nullptr, 0, 0)) + { + case -1: + { + return; // TODO: Log this error. + } + case 0: + { + return; + } + default: + { + TranslateMessage(&msg); + DispatchMessage(&msg); + break; + } + } + } + } + catch (std::exception) + { + // TODO: Log this error. + } +} + +void MenuRenderer::Show(Menu* menu, const int x, const int y) const +{ + PostMessage( + _hWnd.get(), + WM_USER, + 0, + reinterpret_cast(new ShowMenuCommand(menu, x, y))); +} + +// ---------------------------------------------------------------------------- +// Menu +// ---------------------------------------------------------------------------- + +Menu::~Menu() +{ +} + +void Menu::AddOnClicked(const OnClickedCallback onClicked) +{ + const std::lock_guard lock(_onClickedMutex); + _onClicked.push_back(onClicked); +} + +void Menu::Hide() const +{ + menuRenderer.Hide(this); +} + +void Menu::NotifyClicked(MenuItem* menuItem) +{ + std::vector onClicked; + + { + const std::lock_guard lock(_onClickedMutex); + onClicked = std::vector(_onClicked); + } + + for (auto listener : onClicked) + { + listener(menuItem); + } +} + +void Menu::RemoveOnClicked(const OnClickedCallback onClicked) +{ + const std::lock_guard lock(_onClickedMutex); + + for (auto i = _onClicked.begin(); i != _onClicked.end(); ++i) + { + if (*i == onClicked) + { + _onClicked.erase(i); + break; + } + } +} + +void Menu::Show(const Photino* photino, const int x, const int y) +{ + POINT point = {}; + + point.x = x; + point.y = y; + + if (!ClientToScreen(photino->getHwnd(), &point)) + { + throw Win32Exception{}; + } + + menuRenderer.Show(this, point.x, point.y); +} + +// ---------------------------------------------------------------------------- +// MenuItem +// ---------------------------------------------------------------------------- + +MenuItem::MenuItem(MenuItemOptions options) + : _id(++lastMenuItemId) + , _hBitmap(options.ImagePath == nullptr + ? nullptr + : Win32_CreateImage(ToUTF16(options.ImagePath))) // TODO: Cache this? + , _label(options.Label == nullptr + ? std::wstring() + : ToUTF16(options.Label)) +{ +} + +MenuItem::~MenuItem() +{ +} + +void MenuItem::AddTo(HMENU hMenu) +{ + UINT uFlags = MF_STRING; + UINT_PTR uIDNewItem = _id; + LPCWSTR lpNewItem = const_cast(_label.c_str()); + + if (_hMenu != nullptr) + { + uFlags |= MF_POPUP; + uIDNewItem = reinterpret_cast(_hMenu.get()); + } + + if (!AppendMenu(hMenu, uFlags, uIDNewItem, lpNewItem)) + { + throw Win32Exception(); + } + + MENUITEMINFO info = {}; + + info.cbSize = sizeof(info); + info.dwItemData = reinterpret_cast(this); + info.fMask = MIIM_DATA; + + SetMenuItemInfo(hMenu, _id, false, &info); + + if (_hBitmap != nullptr) + { + if (!SetMenuItemBitmaps( + _hMenu.get(), + uIDNewItem, + MF_BYCOMMAND, + _hBitmap.get(), + _hBitmap.get())) + { + throw Win32Exception(); + } + } +} + +// ---------------------------------------------------------------------------- +// MenuParent +// ---------------------------------------------------------------------------- + +MenuParent::~MenuParent() +{ +} + +void MenuParent::Add(MenuChild* child) +{ + if (_hMenu == nullptr) + { + _hMenu = Win32_CreateMenu(); + } + + child->AddTo(_hMenu.get()); +} + +// ---------------------------------------------------------------------------- +// MenuSeparator +// ---------------------------------------------------------------------------- + +void MenuSeparator::AddTo(HMENU hMenu) +{ + if (!AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr)) + { + throw Win32Exception(); + } +} + +// ---------------------------------------------------------------------------- +// MenuRendererCommand +// ---------------------------------------------------------------------------- + +MenuRendererCommand::~MenuRendererCommand() +{ +} + +// ---------------------------------------------------------------------------- +// DestroyMenuCommand +// ---------------------------------------------------------------------------- + +DestroyMenuCommand::DestroyMenuCommand(Menu* menu) + : _menu(menu) +{ +} + +DestroyMenuCommand::~DestroyMenuCommand() +{ +} + +void DestroyMenuCommand::Execute(MenuRenderer& menuRenderer, HWND hWnd) +{ + delete _menu; +} + +// ---------------------------------------------------------------------------- +// ShowMenuCommand +// ---------------------------------------------------------------------------- + +ShowMenuCommand::ShowMenuCommand(Menu* menu, const int x, const int y) + : _menu(menu) + , _x(x) + , _y(y) +{ +} + +ShowMenuCommand::~ShowMenuCommand() +{ +} + +void ShowMenuCommand::Execute(MenuRenderer& menuRenderer, HWND hWnd) +{ + { + std::lock_guard lock(menuRenderer._activeMenuMutex); + menuRenderer._activeMenu = _menu; + } + + // Since our commands run on a different thread than the main window, we + // must bring our menu thread to the foreground before tracking the + // popup menu to ensure that dismissal works correctly. + + if (!SetForegroundWindow(hWnd)) + { + // TODO: Log this error. + } + + SetLastError(0); + + auto itemId = TrackPopupMenuEx( + _menu->_hMenu.get(), + TPM_LEFTALIGN | TPM_RETURNCMD | TPM_TOPALIGN, + _x, + _y, + hWnd, + nullptr); + + { + std::lock_guard lock(menuRenderer._activeMenuMutex); + menuRenderer._activeMenu = nullptr; + } + + if (!itemId) + { + auto lastError = GetLastError(); + + if (lastError) + { + throw Win32Exception(lastError); + } + + _menu->NotifyClicked(nullptr); + } + else + { + MENUITEMINFO info = {}; + + info.cbSize = sizeof(info); + info.fMask = MIIM_DATA; + + if (!GetMenuItemInfo(_menu->_hMenu.get(), itemId, false, &info)) + { + throw Win32Exception(); + } + + _menu->NotifyClicked(reinterpret_cast(info.dwItemData)); + } +} + +// ---------------------------------------------------------------------------- +// Win32BitmapDeleter +// ---------------------------------------------------------------------------- + +void Win32BitmapDeleter::operator()(HBITMAP hBitmap) const noexcept +{ + if (hBitmap == nullptr) + { + return; + } + + if (!DeleteObject(hBitmap)) + { + // TODO: Log this error. + } +} + +// ---------------------------------------------------------------------------- +// Win32Exception +// ---------------------------------------------------------------------------- + +Win32Exception::Win32Exception() + : std::runtime_error(ToLastErrorString(GetLastError())) +{ +} + +Win32Exception::Win32Exception(DWORD lastError) + : std::runtime_error(ToLastErrorString(lastError)) +{ +} + +// ---------------------------------------------------------------------------- +// Win32MenuDeleter +// ---------------------------------------------------------------------------- + +void Win32MenuDeleter::operator()(HMENU hMenu) const noexcept +{ + if (hMenu == nullptr) + { + return; + } + + if (!DestroyMenu(hMenu)) + { + // TODO: Log this error. + } +} + +// ---------------------------------------------------------------------------- +// Win32WindowDeleter +// ---------------------------------------------------------------------------- + +void Win32WindowDeleter::operator()(HWND hWnd) const noexcept +{ + if (hWnd == nullptr) + { + return; + } + + if (!DestroyWindow(hWnd)) + { + // TODO: Log this error. + } +} + +// ---------------------------------------------------------------------------- +// Win32WindowClassDeleter +// ---------------------------------------------------------------------------- + +Win32WindowClassDeleter::Win32WindowClassDeleter(HINSTANCE hInstance) noexcept + : _hInstance(hInstance) +{ +} + +void Win32WindowClassDeleter::operator()(LPCWSTR atom) const noexcept +{ + if (atom == 0) + { + return; + } + + if (!UnregisterClass(reinterpret_cast(atom), _hInstance)) + { + // TODO: Log this error. + } +} \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.Menu.h b/Photino.Native/Photino.Windows.Menu.h new file mode 100644 index 0000000..72c92a1 --- /dev/null +++ b/Photino.Native/Photino.Windows.Menu.h @@ -0,0 +1,344 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +class Menu; +class MenuItem; +class MenuItemOptions; +class MenuRenderer; +class MenuSeparator; +class Win32Exception; + +/// +/// A callback that is called when a menu item is selected from a menu or submenu. +/// +typedef void (*OnClickedCallback)(MenuItem* menuItem); + +/// +/// Deleter for bitmap handles. +/// +class Win32BitmapDeleter final +{ +public: + /// + /// Deletes a bitmap handle. + /// + void operator()(HBITMAP hBitmap) const noexcept; +}; + +/// +/// Deleter for menu handles. +/// +class Win32MenuDeleter final +{ +public: + /// + /// Deletes a menu handle. + /// + void operator()(HMENU hMenu) const noexcept; +}; + +/// +/// Deleter for window handles. +/// +class Win32WindowDeleter final +{ +public: + /// + /// Deletes a window handle. + /// + void operator()(HWND hWnd) const noexcept; +}; + +/// +/// Deleter for window class handles. +/// +class Win32WindowClassDeleter final +{ +private: + HINSTANCE _hInstance; +public: + /// + /// Constructor. + /// + /// The handle for the module that registered the window class. + Win32WindowClassDeleter(HINSTANCE hInstance) noexcept; + + /// + /// Deletes a window class handle. + /// + void operator()(LPCWSTR atom) const noexcept; +}; + +/// +/// The child of a menu or submenu. +/// +class MenuChild +{ +public: + /// + /// Add this child to the given menu. + /// + /// The menu handle. + virtual void AddTo(HMENU hMenu) = 0; +}; + +/// +/// The parent of menu items (i.e., a menu or submenu). +/// +class MenuParent +{ +protected: + std::unique_ptr::type, Win32MenuDeleter> _hMenu; +public: + /// + /// Destructor. + /// + virtual ~MenuParent(); + + /// + /// Add the given child to this parent. + /// + /// The child to add. + void Add(MenuChild* child); +}; + +/// +/// A popup menu. +/// +class Menu final : public MenuParent +{ + friend class ShowMenuCommand; +private: + std::vector _children; + std::vector _onClicked; + std::mutex _onClickedMutex; +public: + /// + /// Destructor. + /// + ~Menu() override; + + /// + /// Adds a listener that is called when a menu item is clicked. + /// + /// The listener to add. + void AddOnClicked(const OnClickedCallback onClicked); + + /// + /// Hides the menu. + /// + void Hide() const; + + /// + /// Notifies listeners that an item has been clicked. + /// + /// The item that has been clicked. + void NotifyClicked(MenuItem* menuItem); + + /// + /// Removes a listener that is called when a menu item is clicked. + /// + /// The listener to remove. + void RemoveOnClicked(const OnClickedCallback onClicked); + + /// + /// Shows the menu. + /// + /// The window to be used when calculating the relative coordinates. + /// The x-coordinate of the top-left of the menu, relative to the window. + /// The y-coordinate of the top-left of the menu, relative to the window. + void Show(const Photino* window, const int x, const int y); +}; + +/// +/// An item in a menu or submenu. +/// +class MenuItem final : public MenuChild, public MenuParent +{ +private: + int _id; + std::unique_ptr::type, Win32BitmapDeleter> _hBitmap; + std::wstring _label; +public: + /// + /// Constructor. + /// + /// The options for this menu item. + MenuItem(MenuItemOptions options); + + /// + /// Destructor. + /// + ~MenuItem() override; + + /// + /// Add this item to the given menu. + /// + /// The menu handle. + void AddTo(HMENU hMenu) override; +}; + +/// +/// Options for the behavior of a menu. +/// +class MenuItemOptions final +{ +public: + /// + /// The path of the image displayed to the left of the item. + /// + char* ImagePath; + + /// + /// The label (i.e., displayed text) of this item. + /// + char* Label; +}; + +class MenuRenderer final +{ + friend class DestroyMenuCommand; + friend class HideMenuCommand; + friend class ShowMenuCommand; +private: + Menu* _activeMenu; + std::mutex _activeMenuMutex; + std::unique_ptr::type, Win32WindowDeleter> _hWnd; + std::thread _thread; + + /// + /// Start the message queue for the menu thread. + /// + /// A reference to the manager. + static void PumpMessages(MenuRenderer* renderer) noexcept; +public: + /// + /// Constructor. + /// + MenuRenderer(); + + /// + /// Destructor. + /// + ~MenuRenderer(); + + /// + /// Dismisses and destroys the given menu. + /// + /// The menu. + void Destroy(Menu* menu); + + /// + /// Dismisses the menu. + /// + /// The menu. + void Hide(const Menu* menu); + + /// + /// Displays the menu to the user at the given point in screen coordinates. + /// + /// The menu to be displayed. + /// The x-coordinate in screen coordinates. + /// The y-coordinate in screen coordinates. + void Show(Menu* menu, const int x, const int y) const; +}; + +/// +/// A visual separator in a menu or submenu. +/// +class MenuSeparator final : public MenuChild +{ +public: + /// + /// Add this separator to the given menu. + /// + /// The menu handle. + void AddTo(HMENU hMenu) override; +}; + +/// +/// A command to be executed on the menu thread. +/// +class MenuRendererCommand +{ +public: + /// + /// Destructor. + /// + virtual ~MenuRendererCommand(); + + /// + /// Execute the command. + /// + /// The command executor. + /// The handle to the window on which this command is executing. + virtual void Execute(MenuRenderer& menuRenderer, HWND hWnd) = 0; +}; + +/// +/// Command for displaying a menu. +/// +class ShowMenuCommand final : public MenuRendererCommand +{ +private: + Menu* _menu; + const int _x; + const int _y; +public: + /// + /// Constructor. + /// + /// The menu to show. + /// The x-coordinate of the top-left corner, in screen coordinates. + /// The y-coordinate of the top-left corner, in screen coordinates. + ShowMenuCommand(Menu* menu, const int x, const int y); + + /// + /// Destructor. + /// + ~ShowMenuCommand() override; + + /// + /// Show the menu. + /// + /// The command executor. + /// The handle to the window on which this command is executing. + void Execute(MenuRenderer& menuRenderer, HWND hWnd) override; +}; + +class DestroyMenuCommand final : public MenuRendererCommand +{ +private: + Menu* _menu; +public: + DestroyMenuCommand(Menu* menu); + ~DestroyMenuCommand() override; + void Execute(MenuRenderer& menuRenderer, HWND hWnd) override; +}; + +/// +/// An exception occurring from improper use of win32 APIs. +/// +class Win32Exception final : public std::runtime_error +{ +public: + /// + /// Constructor. + /// + Win32Exception(); + + /// + /// Constructor. + /// + /// The error code returned by GetLastError. + Win32Exception(DWORD lastError); +}; + +extern MenuRenderer menuRenderer; \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 52bf739..c422e9c 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -51,6 +51,11 @@ const HBRUSH lightBrush = CreateSolidBrush(RGB(255, 255, 255)); void Photino::Register(HINSTANCE hInstance) { + if (!SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) + { + // TODO: Handle failure to initialize COM. + } + InitDarkModeSupport(); _hInstance = hInstance; @@ -318,7 +323,7 @@ Photino::~Photino() if (_notificationsEnabled && _toastHandler != NULL) delete _toastHandler; } -HWND Photino::getHwnd() +HWND Photino::getHwnd() const { return _hWnd; } @@ -1268,4 +1273,4 @@ void Photino::Show(bool isAlreadyShown) else exit(0); } -} +} \ No newline at end of file diff --git a/Photino.Native/Photino.h b/Photino.Native/Photino.h index 05c0caa..dea06c2 100644 --- a/Photino.Native/Photino.h +++ b/Photino.Native/Photino.h @@ -212,7 +212,7 @@ class Photino #ifdef _WIN32 static void Register(HINSTANCE hInstance); static void SetWebView2RuntimePath(AutoString pathToWebView2); - HWND getHwnd(); + HWND getHwnd() const; void RefitContent(); void FocusWebView2(); void NotifyWebView2WindowMove(); @@ -351,4 +351,4 @@ class Photino if (_minimizedCallback) return _minimizedCallback(); } -}; +}; \ No newline at end of file