From 23129dc99634ad8037cd80cd3f01a7e79b63bcfc Mon Sep 17 00:00:00 2001 From: Michael Maltsev <4129781+m417z@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:00:58 +0200 Subject: [PATCH] Windows 11 Taskbar Styler v1.2 (#420) * Index, properties and a visual state can now be specified for a target. Refer to the mod description for more information and examples. * Styles can now be specified per visual state. * Styles can now be specified in XAML form. * Customized styles are now restored on mod unload. * Fixed a possible crash on mod unload. --- mods/windows-11-taskbar-styler.wh.cpp | 940 +++++++++++++++++++------- 1 file changed, 687 insertions(+), 253 deletions(-) diff --git a/mods/windows-11-taskbar-styler.wh.cpp b/mods/windows-11-taskbar-styler.wh.cpp index 75c797a7f..a766b075d 100644 --- a/mods/windows-11-taskbar-styler.wh.cpp +++ b/mods/windows-11-taskbar-styler.wh.cpp @@ -2,7 +2,7 @@ // @id windows-11-taskbar-styler // @name Windows 11 Taskbar Styler // @description An advanced mod to override style attributes of the taskbar control elements -// @version 1.1.3 +// @version 1.2 // @author m417z // @github https://github.com/m417z // @twitter https://twitter.com/m417z @@ -43,15 +43,32 @@ taskbar's control elements in real time, and experiment with various styles. ## Control styles -Each entry has a target control and list of styles. +Each entry has a target control and a list of styles. The target control is written as `Control` or `Control#Name`, i.e. the target control tag name, such as `taskbar:TaskListButton` or `Rectangle`, optionally followed by `#` and the target control's `x:Name` attribute. The target control -can also include parent elements, separated by `>`, for example: -`ParentControl#ParentName > Control#Name`. - -Each style is written as `Style=Value`, for example: `Height=5`. +can also include: +* Child control index, for example: `Control#Name[2]` will only match the + relevant control that's also the second child among all of its parent's child + controls. +* Control properties, for example: + `Control#Name[Property1=Value1][Property2=Value2]`. +* Parent controls, separated by `>`, for example: `ParentControl#ParentName > + Control#Name`. +* Visual state group name, for example: `Control#Name@VisualStateGroupName`. It + can be specified for the target control or for a parent control, but can be + specified only once per target. The visual state group can be used in styles + as specified below. + +**Note**: The target is evaluated only once. If, for example, the index or the +properties of a control change, the target conditions aren't evaluated again. + +Each style is written as `Style=Value`, for example: `Height=5`. The `:=` syntax +can be used to use XAML syntax, for example: `Fill:=`. In addition, a visual state can be specified as following: +`Style@VisualState=Value`, in which case the style will only apply when the +visual state group specified in the target matches the specified visual state. A couple of practical examples: @@ -62,37 +79,44 @@ A couple of practical examples: * Target: `taskbar:TaskListButton` * Style: `CornerRadius=0` -### Task list button background color - -![Screenshot](https://i.imgur.com/eP13uBu.png) - -* Target: `taskbar:TaskListButtonPanel > Border#BackgroundElement` -* Style: `Background=gray` - ### Running indicator size and color -![Screenshot](https://i.imgur.com/enttdYJ.png) +![Screenshot](https://i.imgur.com/mR5c3F5.png) + +* Target: `taskbar:TaskListLabeledButtonPanel@RunningIndicatorStates > + Rectangle#RunningIndicator` +* Styles: + * `Fill=#FFED7014` + * `Height=2` + * `Width=12` + * `Fill@ActiveRunningIndicator=Red` + * `Width@ActiveRunningIndicator=20` -* Target: `Rectangle#RunningIndicator` -* Styles: `Fill=#FFED7014`, `Height=2`, `MinWidth=12` +### Task list button background gradient -**Note:** Currently, it's not possible to set styles only for a specific visual -state. In this example, the color and width apply for all states, including -active and inactive buttons. +![Screenshot](https://i.imgur.com/LNPcw0G.png) -### Time font and color +* Targets: + * `taskbar:TaskListButtonPanel > Border#BackgroundElement` + * `taskbar:TaskListLabeledButtonPanel > Border#BackgroundElement` +* Style: `Background:=` -![Screenshot](https://i.imgur.com/ZRM0drT.png) +### Hide the start button -* Target: `TextBlock#TimeInnerTextBlock` -* Styles: `FontFamily=Comic Sans MS`, `FontSize=14`, `Foreground=Red` +* Target: + `taskbar:ExperienceToggleButton#LaunchListButton[AutomationProperties.AutomationId=StartButton]` +* Style: `Visibility=Collapsed` -### Hide date +### Hide the network notification icon -![Screenshot](https://i.imgur.com/9fGyL9W.png) +* Target: `systemtray:OmniButton#ControlCenterButton > Grid > ContentPresenter > + ItemsPresenter > StackPanel > ContentPresenter[1]` +* Style: `Visibility=Collapsed` -* Target: `TextBlock#DateInnerTextBlock` -* Styles: `Visibility=Collapsed` +**Note**: To hide the volume notification icon instead, use `[2]` instead of +`[1]`. ## Resource variables @@ -132,34 +156,9 @@ relevant `#pragma region` regions in the code editor. - value: "0" $name: Value $name: Resource variables -- promptForExplorerRestart: true - $name: Prompt for Explorer restart - $description: >- - Show a message prompting to restart Explorer on each style change */ // ==/WindhawkModSettings== -#include - -#undef GetCurrentTime - -#include - -std::atomic g_targetThreadId = 0; - -void ApplyCustomizations(winrt::Windows::UI::Xaml::FrameworkElement element); - -HMODULE GetCurrentModuleHandle() { - HMODULE module; - if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - L"", &module)) { - return nullptr; - } - - return module; -} - //////////////////////////////////////////////////////////////////////////////// // clang-format off @@ -299,11 +298,11 @@ extern "C"{ #pragma region Application Family #if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) -#pragma warning(push) -#pragma warning(disable:4668) -#pragma warning(disable:4001) +// #pragma warning(push) +// #pragma warning(disable:4668) +// #pragma warning(disable:4001) // #pragma once -#pragma warning(pop) +// #pragma warning(pop) // Win32 API definitions #define E_NOTFOUND HRESULT_FROM_WIN32(ERROR_NOT_FOUND) #define E_UNKNOWNTYPE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_XAML, 40L) @@ -1044,6 +1043,35 @@ extern RPC_IF_HANDLE __MIDL_itf_windows2Eui2Examl2Ehosting2Edesktopwindowxamlsou #pragma endregion // windows_ui_xaml_hosting_desktopwindowxamlsource_h +// clang-format on +//////////////////////////////////////////////////////////////////////////////// + +#include + +#undef GetCurrentTime + +#include + +std::atomic g_targetThreadId = 0; + +void ApplyCustomizations(InstanceHandle handle, + winrt::Windows::UI::Xaml::FrameworkElement element); +void CleanupCustomizations(InstanceHandle handle); + +HMODULE GetCurrentModuleHandle() { + HMODULE module; + if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + L"", &module)) { + return nullptr; + } + + return module; +} + +//////////////////////////////////////////////////////////////////////////////// +// clang-format off + #pragma region visualtreewatcher_hpp #include @@ -1129,23 +1157,26 @@ HRESULT VisualTreeWatcher::OnVisualTreeChange(ParentChildRelation, VisualElement Wh_Log(L"Element type: %s", element.Type); - const auto inspectable = FromHandle(element.Handle); - - auto frameworkElement = inspectable.try_as(); - if (!frameworkElement) + if (mutationType == Add) { - const auto desktopXamlSource = FromHandle(element.Handle); - frameworkElement = desktopXamlSource.Content().try_as(); - } + const auto inspectable = FromHandle(element.Handle); - if (frameworkElement) - { - Wh_Log(L"FrameworkElement name: %s", frameworkElement.Name().c_str()); - } + auto frameworkElement = inspectable.try_as(); + if (!frameworkElement) + { + const auto desktopXamlSource = FromHandle(element.Handle); + frameworkElement = desktopXamlSource.Content().try_as(); + } - if (frameworkElement && mutationType == Add) + if (frameworkElement) + { + Wh_Log(L"FrameworkElement name: %s", frameworkElement.Name().c_str()); + ApplyCustomizations(element.Handle, frameworkElement); + } + } + else if (mutationType == Remove) { - ApplyCustomizations(frameworkElement); + CleanupCustomizations(element.Handle); } return S_OK; @@ -1345,9 +1376,11 @@ HRESULT InjectWindhawkTAP() noexcept // clang-format on //////////////////////////////////////////////////////////////////////////////// +#include #include #include #include +#include #include #include @@ -1373,29 +1406,75 @@ struct deleter_from_fn { using string_setting_unique_ptr = std::unique_ptr>; -HANDLE g_restartExplorerPromptThread; -std::atomic g_restartExplorerPromptWindow; - using PropertyKeyValue = std::pair; struct ElementMatcher { std::wstring type; std::wstring name; + std::wstring visualStateGroup; + int oneBasedIndex = 0; + std::vector> propertyValuesStr; + std::vector propertyValues; +}; + +struct StyleRule { + std::wstring name; + std::wstring visualState; + std::wstring value; + bool isXamlValue = false; }; -struct ElementPropertyOverrides { +// Property -> visual state -> value. +using PropertyOverrides = std::unordered_map< + DependencyProperty, + std::unordered_map>; + +struct ElementCustomizationRules { ElementMatcher elementMatcher; std::vector parentElementMatchers; - std::vector propertyValues; + PropertyOverrides propertyOverrides; }; -bool g_propertyOverridesLoaded; -std::vector g_propertyOverrides; -DWORD g_propertyOverridesUpdateCount = 0; +bool g_elementsCustomizationRulesLoaded; +std::vector g_elementsCustomizationRules; + +struct ElementPropertyCustomizationState { + std::optional originalValue; + std::optional customValue; + int64_t propertyChangedToken = 0; +}; + +struct ElementCustomizationState { + winrt::weak_ref element; + std::unordered_map + propertyCustomizationStates; + winrt::weak_ref visualStateGroup; + winrt::event_token visualStateGroupCurrentStateChangedToken; +}; + +std::unordered_map + g_elementsCustomizationState; + +bool g_elementPropertyModifying; + +// https://stackoverflow.com/a/12835139 +VisualStateGroup GetVisualStateGroup(FrameworkElement element, + std::wstring_view stateGroupName) { + auto list = VisualStateManager::GetVisualStateGroups(element); + + for (const auto& v : list) { + if (v.Name() == stateGroupName) { + return v; + } + } + + return nullptr; +} bool TestElementMatcher(FrameworkElement element, - const ElementMatcher& matcher) { + const ElementMatcher& matcher, + VisualStateGroup* visualStateGroup) { if (!matcher.type.empty() && matcher.type != winrt::get_class_name(element)) { return false; @@ -1405,13 +1484,78 @@ bool TestElementMatcher(FrameworkElement element, return false; } + if (matcher.oneBasedIndex) { + auto parent = Media::VisualTreeHelper::GetParent(element); + if (!parent) { + return false; + } + + int index = matcher.oneBasedIndex - 1; + if (index < 0 || + index >= Media::VisualTreeHelper::GetChildrenCount(parent) || + Media::VisualTreeHelper::GetChild(parent, index) != element) { + return false; + } + } + + auto elementDo = element.as(); + + for (const auto& propertyValue : matcher.propertyValues) { + const auto value = elementDo.ReadLocalValue(propertyValue.first); + const auto className = winrt::get_class_name(value); + const auto expectedClassName = + winrt::get_class_name(propertyValue.second); + if (className != expectedClassName) { + Wh_Log(L"Different property class: %s vs. %s", className.c_str(), + expectedClassName.c_str()); + return false; + } + + if (className == L"Windows.Foundation.IReference`1") { + if (winrt::unbox_value(propertyValue.second) == + winrt::unbox_value(value)) { + continue; + } + + return false; + } + + if (className == L"Windows.Foundation.IReference`1") { + if (winrt::unbox_value(propertyValue.second) == + winrt::unbox_value(value)) { + continue; + } + + return false; + } + + if (className == L"Windows.Foundation.IReference`1") { + if (winrt::unbox_value(propertyValue.second) == + winrt::unbox_value(value)) { + continue; + } + + return false; + } + + Wh_Log(L"Unsupported property class: %s", className.c_str()); + return false; + } + + if (!matcher.visualStateGroup.empty() && visualStateGroup) { + *visualStateGroup = + GetVisualStateGroup(element, matcher.visualStateGroup); + } + return true; } -const std::vector* FindElementPropertyOverrides( - FrameworkElement element) { - for (const auto& override : g_propertyOverrides) { - if (!TestElementMatcher(element, override.elementMatcher)) { +const ElementCustomizationRules* FindElementCustomizationRules( + FrameworkElement element, + VisualStateGroup* visualStateGroup) { + for (const auto& override : g_elementsCustomizationRules) { + if (!TestElementMatcher(element, override.elementMatcher, + visualStateGroup)) { continue; } @@ -1428,14 +1572,15 @@ const std::vector* FindElementPropertyOverrides( break; } - if (!TestElementMatcher(parentElementIter, matcher)) { + if (!TestElementMatcher(parentElementIter, matcher, + visualStateGroup)) { parentElementMatchFailed = true; break; } } if (!parentElementMatchFailed) { - return &override.propertyValues; + return &override; } } @@ -1445,42 +1590,213 @@ const std::vector* FindElementPropertyOverrides( void ProcessAllStylesFromSettings(); void ProcessResourceVariablesFromSettings(); -void ApplyCustomizations(FrameworkElement element) { - if (!g_propertyOverridesLoaded) { +void ApplyCustomizations(InstanceHandle handle, FrameworkElement element) { + if (!g_elementsCustomizationRulesLoaded) { ProcessAllStylesFromSettings(); ProcessResourceVariablesFromSettings(); - g_propertyOverridesLoaded = true; + g_elementsCustomizationRulesLoaded = true; } - auto propertyValues = FindElementPropertyOverrides(element); - if (!propertyValues) { + VisualStateGroup visualStateGroup; + auto rules = FindElementCustomizationRules(element, &visualStateGroup); + if (!rules) { return; } - Wh_Log(L"Applying %zu styles", propertyValues->size()); + Wh_Log(L"Applying styles"); - auto elementDo = element.as(); + auto& elementCustomizationState = g_elementsCustomizationState[handle]; - for (const auto& [property, value] : *propertyValues) { - element.SetValue(property, value); - elementDo.RegisterPropertyChangedCallback( - property, - [value = value, - propertyOverridesUpdateCount = g_propertyOverridesUpdateCount]( - DependencyObject sender, DependencyProperty property) { - if (propertyOverridesUpdateCount != - g_propertyOverridesUpdateCount) { - return; + { + auto oldElement = elementCustomizationState.element.get(); + if (oldElement) { + auto oldElementDo = oldElement.as(); + for (const auto& [property, state] : + elementCustomizationState.propertyCustomizationStates) { + oldElementDo.UnregisterPropertyChangedCallback( + property, state.propertyChangedToken); + + if (state.originalValue) { + oldElement.SetValue(property, *state.originalValue); } + } + } + + auto oldVisualStateGroup = + elementCustomizationState.visualStateGroup.get(); + if (oldVisualStateGroup) { + oldVisualStateGroup.CurrentStateChanged( + elementCustomizationState + .visualStateGroupCurrentStateChangedToken); + } + } + + elementCustomizationState = { + .element = element, + }; + + auto elementDo = element.as(); + + VisualState currentVisualState( + visualStateGroup ? visualStateGroup.CurrentState() : nullptr); + + std::wstring currentVisualStateName( + currentVisualState ? currentVisualState.Name() : L""); + + for (auto& [property, valuesPerVisualState] : rules->propertyOverrides) { + const auto [propertyCustomizationStatesIt, inserted] = + elementCustomizationState.propertyCustomizationStates.insert( + {property, {}}); + if (!inserted) { + continue; + } + + auto& propertyCustomizationState = + propertyCustomizationStatesIt->second; + + auto it = valuesPerVisualState.find(currentVisualStateName); + if (it == valuesPerVisualState.end() && + !currentVisualStateName.empty()) { + it = valuesPerVisualState.find(L""); + } + + if (it != valuesPerVisualState.end()) { + propertyCustomizationState.originalValue = + element.ReadLocalValue(property); + propertyCustomizationState.customValue = it->second; + element.SetValue(property, it->second); + } + + propertyCustomizationState.propertyChangedToken = + elementDo.RegisterPropertyChangedCallback( + property, + [&propertyCustomizationState](DependencyObject sender, + DependencyProperty property) { + if (g_elementPropertyModifying) { + return; + } + + auto element = sender.try_as(); + if (!element) { + return; + } + + if (!propertyCustomizationState.customValue) { + return; + } - auto element = sender.try_as(); - if (element) { Wh_Log(L"Re-applying style for %s", winrt::get_class_name(element).c_str()); - element.SetValue(property, value); - } - }); + + auto localValue = element.ReadLocalValue(property); + + if (*propertyCustomizationState.customValue != localValue) { + propertyCustomizationState.originalValue = localValue; + } + + g_elementPropertyModifying = true; + element.SetValue(property, + *propertyCustomizationState.customValue); + g_elementPropertyModifying = false; + }); + } + + if (visualStateGroup) { + elementCustomizationState.visualStateGroup = visualStateGroup; + + elementCustomizationState.visualStateGroupCurrentStateChangedToken = + visualStateGroup.CurrentStateChanged( + [rules, &elementCustomizationState]( + winrt::Windows::Foundation::IInspectable const& sender, + VisualStateChangedEventArgs const& e) { + auto element = elementCustomizationState.element.get(); + + Wh_Log(L"Re-applying all styles for %s", + winrt::get_class_name(element).c_str()); + + g_elementPropertyModifying = true; + + auto& propertyCustomizationStates = + elementCustomizationState.propertyCustomizationStates; + + for (auto& [property, valuesPerVisualState] : + rules->propertyOverrides) { + auto& propertyCustomizationState = + propertyCustomizationStates.at(property); + + auto newState = e.NewState(); + auto newStateName = + std::wstring{newState ? newState.Name() : L""}; + auto it = valuesPerVisualState.find(newStateName); + if (it == valuesPerVisualState.end()) { + it = valuesPerVisualState.find(L""); + if (it != valuesPerVisualState.end()) { + auto oldState = e.OldState(); + auto oldStateName = std::wstring{ + oldState ? oldState.Name() : L""}; + if (!valuesPerVisualState.contains( + oldStateName)) { + continue; + } + } + } + + if (it != valuesPerVisualState.end()) { + if (!propertyCustomizationState.originalValue) { + propertyCustomizationState.originalValue = + element.ReadLocalValue(property); + } + + propertyCustomizationState.customValue = it->second; + element.SetValue(property, it->second); + } else { + if (propertyCustomizationState.originalValue) { + if (*propertyCustomizationState.originalValue == + DependencyProperty::UnsetValue()) { + element.ClearValue(property); + } else { + element.SetValue(property, + *propertyCustomizationState + .originalValue); + } + + propertyCustomizationState.originalValue + .reset(); + } + + propertyCustomizationState.customValue.reset(); + } + } + + g_elementPropertyModifying = false; + }); + } +} + +void CleanupCustomizations(InstanceHandle handle) { + auto it = g_elementsCustomizationState.find(handle); + if (it == g_elementsCustomizationState.end()) { + return; + } + + auto& [k, v] = *it; + + auto oldElement = v.element.get(); + if (oldElement) { + auto oldElementDo = oldElement.as(); + for (const auto& [property, state] : v.propertyCustomizationStates) { + oldElementDo.UnregisterPropertyChangedCallback( + property, state.propertyChangedToken); + } } + + auto oldVisualStateGroup = v.visualStateGroup.get(); + if (oldVisualStateGroup) { + oldVisualStateGroup.CurrentStateChanged( + v.visualStateGroupCurrentStateChangedToken); + } + + g_elementsCustomizationState.erase(it); } // https://stackoverflow.com/a/5665377 @@ -1539,14 +1855,28 @@ std::vector SplitStringView(std::wstring_view s, return res; } -struct ResolveTypeAndStylesResult { - std::wstring type; - std::vector propertyValues; -}; +std::wstring AdjustTypeName(std::wstring_view type) { + static const std::vector> + adjustments = { + {L"Taskbar.", L"taskbar:"}, + {L"SystemTray.", L"systemtray:"}, + {L"Microsoft.UI.Xaml.Control.", L"muxc:"}, + }; + + for (const auto& adjustment : adjustments) { + if (type.starts_with(adjustment.first)) { + auto result = std::wstring{adjustment.second}; + result += type.substr(adjustment.first.size()); + return result; + } + } -ResolveTypeAndStylesResult ResolveTypeAndStyles( - std::wstring_view targetType, - std::vector> styles) { + return std::wstring{type}; +} + +void ResolveTypeAndStyles(ElementMatcher* elementMatcher, + std::vector styleRules = {}, + PropertyOverrides* propertyOverrides = nullptr) { std::wstring xaml = LR"(type)); xaml += L"\">\n"; - for (const auto& [property, value] : styles) { + for (const auto& [property, value] : elementMatcher->propertyValuesStr) { xaml += L" \n"; } + for (const auto& rule : styleRules) { + xaml += L" \n"; + } else { + xaml += + L">\n" + L" \n"; + xaml += rule.value; + xaml += + L"\n" + L" \n" + L" \n"; + } + } + xaml += LR"( )"; @@ -1588,75 +1938,205 @@ ResolveTypeAndStylesResult ResolveTypeAndStyles( auto [styleKey, styleInspectable] = resourceDictionary.First().Current(); auto style = styleInspectable.as