Customize system menu items through dedicated API (#677)

Document new interface changes.
This commit is contained in:
vldmr11080 2019-11-12 11:48:14 +01:00 committed by Enrico Giordani
parent 9f78af29bf
commit be86cd4028
17 changed files with 350 additions and 1 deletions

View file

@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "interface", "interface", "{
ProjectSection(SolutionItems) = preProject
src\modules\interface\lowlevel_keyboard_event_data.h = src\modules\interface\lowlevel_keyboard_event_data.h
src\modules\interface\powertoy_module_interface.h = src\modules\interface\powertoy_module_interface.h
src\modules\interface\powertoy_system_menu.h = src\modules\interface\powertoy_system_menu.h
src\modules\interface\win_hook_event_data.h = src\modules\interface\win_hook_event_data.h
EndProjectSection
EndProject

View file

@ -214,6 +214,9 @@ public:
}
return 0;
}
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) override { }
virtual void signal_system_menu_action(const wchar_t* name) override { }
};
// Load the settings file.

View file

@ -182,6 +182,9 @@ public:
return 0;
}
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) override { }
virtual void signal_system_menu_action(const wchar_t* name) override { }
// Destroy the powertoy and free memory
virtual void destroy() override
{

View file

@ -19,6 +19,8 @@ public:
virtual void disable() = 0;
virtual bool is_enabled() = 0;
virtual intptr_t signal_event(const wchar_t* name, intptr_t data) = 0;
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) = 0;
virtual void signal_system_menu_action(const wchar_t* name) = 0;
virtual void destroy() = 0;
};
@ -43,6 +45,8 @@ and destroy():
- [`set_config()`](#set_config) to set settings after they have been edited in the Settings editor,
- [`call_custom_action()`](#call_custom_action) when the user selects a custom action in the Settings editor,
- [`signal_event()`](#signal_event) to send an event the PowerToy registered to.
- [`register_system_menu_helper()`](#register_system_menu_helper) to pass object, responsible for handling customized system menus, to module.
- [`signal_system_menu_action()`](#signal_system_menu_action) to send an event when action is taken on system menu item.
When terminating, the runner will:
- call [`disable()`](#disable),
@ -313,6 +317,25 @@ Sample code from [`the example PowerToy`](/src/modules/example_powertoy/dllmain.
}
```
### register_system_menu_helper
```cpp
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) = 0;
```
Register helper class to handle all system menu items related actions. Creation, deletion
and all other actions taken on system menu item will be handled by provided class.
Module will be informed when action is taken on any item created on request of the module.
### signal_system_menu_action
```cpp
virtual void signal_system_menu_action(const wchar_t* name) = 0;
```
Runner invokes this API when action is taken on item created on request from the module.
Item name is passed as an argument, so that module can distinguish between different menu items.
#### destroy
```cpp
@ -328,11 +351,59 @@ Sample code from [`the example PowerToy`](/src/modules/example_powertoy/dllmain.
}
```
### Powertoys system menu helper interface
Interface for helper class responsible for handling all system menu related actions.
```cpp
class PowertoySystemMenuIface {
public:
struct ItemInfo {
std::wstring name{};
bool enable{ false };
bool checkBox{ false };
};
virtual void SetConfiguration(PowertoyModuleIface* module, const std::vector<ItemInfo>& config) = 0;
virtual void ProcessSelectedItem(PowertoyModuleIface* module, HWND window, const wchar_t* itemName) = 0;
};
```
### ItemInfo
```cpp
struct ItemInfo {
std::wstring name{};
bool enable{ false };
bool checkBox{ false };
};
```
Structure containing all relevant information for system menu item: name (and hotkey if available), item
status at creation (enabled/disabled) and whether check box will appear next to item name when action is taken.
### SetConfiguration
```cpp
virtual void SetConfiguration(PowertoyModuleIface* module, const std::vector<ItemInfo>& config) = 0;
```
Module should use this interface to inform system menu helper class which custom system menu items to create.
### ProcessSelectedItem
```cpp
virtual void ProcessSelectedItem(PowertoyModuleIface* module, HWND window, const wchar_t* itemName) = 0;
```
Process action taken on specific system menu item.
## Code organization
#### [`powertoy_module_interface.h`](./powertoy_module_interface.h)
Contains the PowerToys interface definition.
### [`powertoy_system_menu.h`](./powertoy_system_module.h)
Contains the PowerToys system menu helper interface definition.
#### [`lowlevel_keyboard_event_data.h`](./lowlevel_keyboard_event_data.h)
Contains the `LowlevelKeyboardEvent` structure that's passed to `signal_event` for `ll_keyboard` events.

View file

@ -28,6 +28,8 @@
- unload the DLL.
*/
class PowertoySystemMenuIface;
class PowertoyModuleIface {
public:
/* Returns the name of the PowerToy, this will be cached by the runner. */
@ -63,6 +65,12 @@ public:
* win_hook_event: see win_hook_event_data.h
*/
virtual intptr_t signal_event(const wchar_t* name, intptr_t data) = 0;
/* Register helper class to handle system menu items related actions. */
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) = 0;
/* Handle action on system menu item. */
virtual void signal_system_menu_action(const wchar_t* name) = 0;
/* Destroy the PowerToy and free all memory. */
virtual void destroy() = 0;
};

View file

@ -0,0 +1,22 @@
#pragma once
#include <string>
class PowertoyModuleIface;
class PowertoySystemMenuIface {
public:
struct ItemInfo {
std::wstring name{};
bool enable{ false };
bool checkBox{ false };
};
/*
* Set configuration of system menu items for specific powertoy module. Configuration
* parameters include item name (and hotkey), item status at creation (enabled/disabled)
* and whether check box will appear next to item name when action is taken.
*/
virtual void SetConfiguration(PowertoyModuleIface* module, const std::vector<ItemInfo>& config) = 0;
/* Process action on specific system menu item. */
virtual void ProcessSelectedItem(PowertoyModuleIface* module, HWND window, const wchar_t* itemName) = 0;
};

View file

@ -227,6 +227,9 @@ public:
return 0;
}
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) override { }
virtual void signal_system_menu_action(const wchar_t* name) override { }
// Destroy the powertoy and free memory
virtual void destroy() override
{

View file

@ -21,6 +21,9 @@ public:
virtual bool is_enabled() override;
virtual intptr_t signal_event(const wchar_t* name, intptr_t data) override;
virtual void register_system_menu_helper(PowertoySystemMenuIface* helper) override { }
virtual void signal_system_menu_action(const wchar_t* name) override { }
void on_held();
void on_held_press(DWORD vkCode);
void quick_hide();

View file

@ -20,5 +20,6 @@ PowertoyModule load_powertoy(const std::wstring& filename) {
FreeLibrary(handle);
winrt::throw_last_error();
}
module->register_system_menu_helper(&SystemMenuHelperInstace());
return PowertoyModule(module, handle);
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "powertoys_events.h"
#include "system_menu_helper.h"
#include <interface/powertoy_module_interface.h>
#include <string>
#include <memory>
@ -12,6 +13,7 @@ class PowertoyModule;
struct PowertoyModuleDeleter {
void operator()(PowertoyModuleIface* module) const {
if (module) {
powertoys_events().unregister_system_menu_action(module);
powertoys_events().unregister_receiver(module);
module->destroy();
}
@ -38,6 +40,9 @@ public:
powertoys_events().register_receiver(*want_signals, module);
}
}
if (SystemMenuHelperInstace().HasCustomConfig(module)) {
powertoys_events().register_system_menu_action(module);
}
}
const std::wstring& get_name() const {

View file

@ -2,6 +2,7 @@
#include "powertoys_events.h"
#include "lowlevel_keyboard_event.h"
#include "win_hook_event.h"
#include "system_menu_helper.h"
void first_subscribed(const std::wstring& event) {
if (event == ll_keyboard)
@ -41,6 +42,37 @@ void PowertoysEvents::unregister_receiver(PowertoyModuleIface* module) {
}
}
void PowertoysEvents::register_system_menu_action(PowertoyModuleIface* module) {
std::unique_lock lock(mutex);
system_menu_receivers.insert(module);
}
void PowertoysEvents::unregister_system_menu_action(PowertoyModuleIface* module) {
std::unique_lock lock(mutex);
auto it = system_menu_receivers.find(module);
if (it != system_menu_receivers.end()) {
SystemMenuHelperInstace().Reset(module);
system_menu_receivers.erase(it);
}
}
void PowertoysEvents::handle_system_menu_action(const WinHookEvent& data) {
if (data.event == EVENT_SYSTEM_MENUSTART) {
for (auto& module : system_menu_receivers) {
SystemMenuHelperInstace().Customize(module, data.hwnd);
}
}
else if (data.event == EVENT_OBJECT_INVOKED) {
if (PowertoyModuleIface* module{ SystemMenuHelperInstace().ModuleFromItemId(data.idChild) }) {
std::wstring itemName = SystemMenuHelperInstace().ItemNameFromItemId(data.idChild);
// Process event on specified system menu item by responsible module.
module->signal_system_menu_action(itemName.c_str());
// Process event on specified system menu item by system menu helper (check/uncheck if needed).
SystemMenuHelperInstace().ProcessSelectedItem(module, GetForegroundWindow(), itemName.c_str());
}
}
}
intptr_t PowertoysEvents::signal_event(const std::wstring & event, intptr_t data) {
intptr_t rvalue = 0;
std::shared_lock lock(mutex);

View file

@ -1,15 +1,23 @@
#pragma once
#include <interface/powertoy_module_interface.h>
#include <interface/win_hook_event_data.h>
#include <string>
class PowertoysEvents {
public:
void register_receiver(const std::wstring& event, PowertoyModuleIface* module);
void unregister_receiver(PowertoyModuleIface* module);
void register_system_menu_action(PowertoyModuleIface* module);
void unregister_system_menu_action(PowertoyModuleIface* module);
void handle_system_menu_action(const WinHookEvent& data);
intptr_t signal_event(const std::wstring& event, intptr_t data);
private:
std::shared_mutex mutex;
std::unordered_map<std::wstring, std::vector<PowertoyModuleIface*>> receivers;
std::unordered_set<PowertoyModuleIface*> system_menu_receivers;
};
PowertoysEvents& powertoys_events();

View file

@ -107,6 +107,7 @@
<ClCompile Include="powertoy_module.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="settings_window.cpp" />
<ClCompile Include="system_menu_helper.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="tray_icon.cpp" />
<ClCompile Include="unhandled_exception_handler.cpp" />
@ -121,6 +122,7 @@
<ClInclude Include="powertoy_module.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="settings_window.h" />
<ClInclude Include="system_menu_helper.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="tray_icon.h" />
<ClInclude Include="unhandled_exception_handler.h" />

View file

@ -33,6 +33,9 @@
<ClCompile Include="win_hook_event.cpp">
<Filter>Events</Filter>
</ClCompile>
<ClCompile Include="system_menu_helper.cpp">
<Filter>Utils</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
@ -67,6 +70,9 @@
<ClInclude Include="win_hook_event.h">
<Filter>Events</Filter>
</ClInclude>
<ClInclude Include="system_menu_helper.h">
<Filter>Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Utils">

View file

@ -0,0 +1,129 @@
#include "pch.h"
#include "system_menu_helper.h"
#include <interface/powertoy_module_interface.h>
namespace {
constexpr int KSeparatorPos = 1;
constexpr int KNewItemPos = 2;
unsigned int GenerateItemId() {
static unsigned int generator = 0x70777479;
return ++generator;
}
}
SystemMenuHelper& SystemMenuHelperInstace() {
static SystemMenuHelper instance;
return instance;
}
void SystemMenuHelper::SetConfiguration(PowertoyModuleIface* module, const std::vector<ItemInfo>& config) {
Reset(module);
Configurations[module] = config;
for (auto& [window, modules] : ProcessedModules) {
// Unregister module. After system menu is opened again, new configuration will be applied.
modules.erase(std::remove(std::begin(modules), std::end(modules), module), std::end(modules));
}
}
void SystemMenuHelper::ProcessSelectedItem(PowertoyModuleIface* module, HWND window, const wchar_t* itemName) {
for (const auto& item : Configurations[module]) {
if (itemName == item.name && item.checkBox) {
// Handle check/uncheck action only if specified by module configuration.
for (const auto& [id, data] : IdMappings) {
if (data.second == itemName) {
HMENU systemMenu = GetSystemMenu(window, false);
int state = (GetMenuState(systemMenu, id, MF_BYCOMMAND) == MF_CHECKED) ? MF_UNCHECKED : MF_CHECKED;
CheckMenuItem(systemMenu, id, MF_BYCOMMAND | state);
break;
}
}
break;
}
}
}
bool SystemMenuHelper::Customize(PowertoyModuleIface* module, HWND window) {
auto& modules = ProcessedModules[window];
for (const auto& m : modules) {
if (module == m) {
return false;
}
}
AddSeparator(module, window);
for (const auto& info : Configurations[module]) {
AddItem(module, window, info.name, info.enable);
}
modules.push_back(module);
return true;
}
void SystemMenuHelper::Reset(PowertoyModuleIface* module) {
for (auto& [window, modules] : ProcessedModules) {
if (HMENU systemMenu{ GetSystemMenu(window, false) }) {
for (auto& [id, data] : IdMappings) {
if (data.first == module) {
DeleteMenu(systemMenu, id, MF_BYCOMMAND);
}
}
}
}
}
bool SystemMenuHelper::HasCustomConfig(PowertoyModuleIface* module) {
return Configurations.find(module) != Configurations.end();
}
bool SystemMenuHelper::AddItem(PowertoyModuleIface* module, HWND window, const std::wstring& name, const bool enable) {
if (HMENU systemMenu{ GetSystemMenu(window, false) }) {
MENUITEMINFO item;
item.cbSize = sizeof(item);
item.fMask = MIIM_ID | MIIM_STRING | MIIM_STATE;
item.fState = MF_UNCHECKED | MF_DISABLED; // Item is disabled by default.
item.wID = GenerateItemId();
item.dwTypeData = const_cast<WCHAR*>(name.c_str());
item.cch = name.size() + 1;
if (InsertMenuItem(systemMenu, GetMenuItemCount(systemMenu) - KNewItemPos, true, &item)) {
IdMappings[item.wID] = { module, name };
if (enable) {
EnableMenuItem(systemMenu, item.wID, MF_BYCOMMAND | MF_ENABLED);
}
return true;
}
}
return false;
}
bool SystemMenuHelper::AddSeparator(PowertoyModuleIface* module, HWND window) {
if (HMENU systemMenu{ GetSystemMenu(window, false) }) {
MENUITEMINFO separator;
separator.cbSize = sizeof(separator);
separator.fMask = MIIM_ID | MIIM_FTYPE;
separator.fType = MFT_SEPARATOR;
separator.wID = GenerateItemId();
if (InsertMenuItem(systemMenu, GetMenuItemCount(systemMenu) - KSeparatorPos, true, &separator)) {
IdMappings[separator.wID] = { module, L"sepparator_dummy_name" };
return true;
}
}
return false;
}
PowertoyModuleIface* SystemMenuHelper::ModuleFromItemId(const int& id) {
auto it = IdMappings.find(id);
if (it != IdMappings.end()) {
return it->second.first;
}
return nullptr;
}
const std::wstring SystemMenuHelper::ItemNameFromItemId(const int& id) {
auto itemIt = IdMappings.find(id);
if (itemIt != IdMappings.end()) {
return itemIt->second.second;
}
return std::wstring{};
}

View file

@ -0,0 +1,42 @@
#pragma once
#pragma once
#include <interface/powertoy_system_menu.h>
#include <windows.h>
#include <string>
#include <vector>
#include <unordered_map>
class PowertoyModuleIface;
class SystemMenuHelper : public PowertoySystemMenuIface {
public:
// PowertoySystemMenuIface
virtual void SetConfiguration(PowertoyModuleIface* module, const std::vector<ItemInfo>& config) override;
virtual void ProcessSelectedItem(PowertoyModuleIface* module, HWND window, const wchar_t* itemName) override;
bool Customize(PowertoyModuleIface* module, HWND window);
void Reset(PowertoyModuleIface* module);
bool HasCustomConfig(PowertoyModuleIface* module);
PowertoyModuleIface* ModuleFromItemId(const int& id);
const std::wstring ItemNameFromItemId(const int& id);
private:
bool AddItem(PowertoyModuleIface* module, HWND window, const std::wstring& name, const bool enable);
bool AddSeparator(PowertoyModuleIface* module, HWND window);
// Store processed modules per window to avoid handling it multiple times.
std::unordered_map<HWND, std::vector<PowertoyModuleIface*>> ProcessedModules{};
// Keep mappings form item id to the module who created it and item name for faster processing later.
std::unordered_map<int, std::pair<PowertoyModuleIface*, std::wstring>> IdMappings{};
// Store configurations provided by module.
// This will be used to create custom system menu items and to handle updates.
std::unordered_map<PowertoyModuleIface*, std::vector<ItemInfo>> Configurations{};
};
SystemMenuHelper& SystemMenuHelperInstace();

View file

@ -9,6 +9,8 @@ static std::mutex mutex;
static std::deque<WinHookEvent> hook_events;
static std::condition_variable dispatch_cv;
void intercept_system_menu_action(intptr_t);
static void CALLBACK win_hook_event_proc(HWINEVENTHOOK winEventHook,
DWORD event,
HWND window,
@ -39,7 +41,9 @@ static void dispatch_thread_proc() {
auto event = hook_events.front();
hook_events.pop_front();
lock.unlock();
powertoys_events().signal_event(win_hook_event, reinterpret_cast<intptr_t>(&event));
intptr_t data = reinterpret_cast<intptr_t>(&event);
intercept_system_menu_action(data);
powertoys_events().signal_event(win_hook_event, data);
lock.lock();
}
}
@ -70,3 +74,9 @@ void stop_win_hook_event() {
hook_events.shrink_to_fit();
}
void intercept_system_menu_action(intptr_t data) {
WinHookEvent* evt = reinterpret_cast<WinHookEvent*>(data);
if (evt->event == EVENT_SYSTEM_MENUSTART || evt->event == EVENT_OBJECT_INVOKED) {
powertoys_events().handle_system_menu_action(*evt);
}
}