From 989ffe1c1d105f73c60ef6f773c4a097a46ceb12 Mon Sep 17 00:00:00 2001 From: jfacemyer <1776340+jfacemyer@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:57:01 -0400 Subject: [PATCH] WineFix: fill the empty Leading dropdown in the text context toolbar Affinity 3.3 merged ReflectedLayoutItemTemplates.xaml into ContextBarItemTemplates.xaml. Its implicit CheckedComboBoxItem style has an empty ControlTemplate, and the merged dictionary is searched before the application's Core.xaml, so LeadingComboItemStyle and CheckedComboBoxItemStyle (BasedOn the implicit style, no template of their own) inherit nothing. The Leading dropdown opens with zero height. Replacing the dictionary entries is not enough, because the context bar's DataTemplates keep the style objects their StaticResource references were bound to. Instead, when a CheckedComboBox applies its template, swap an ItemContainerStyle whose inherited template is empty for a copy BasedOn Core.xaml's style. Styles with a real template are untouched, and the patch does nothing once the XAML is fixed. Affinity 3.2.3 does not have the merge. --- WineFix/Patches/ContextBarItemStylePatch.cs | 148 ++++++++++++++++++++ WineFix/WineFixPlugin.cs | 15 ++ 2 files changed, 163 insertions(+) create mode 100644 WineFix/Patches/ContextBarItemStylePatch.cs diff --git a/WineFix/Patches/ContextBarItemStylePatch.cs b/WineFix/Patches/ContextBarItemStylePatch.cs new file mode 100644 index 0000000..b84c8ca --- /dev/null +++ b/WineFix/Patches/ContextBarItemStylePatch.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using HarmonyLib; +using AffinityPluginLoader.Core; + +namespace WineFix.Patches +{ + /// + /// Fixes the empty Leading dropdown in the text context toolbar (Affinity 3.3). + /// + /// LeadingComboItemStyle and CheckedComboBoxItemStyle in ContextBarItemTemplates.xaml set + /// no template of their own; they are BasedOn the implicit CheckedComboBoxItem style and + /// expect Core.xaml's. Affinity 3.3 merged ReflectedLayoutItemTemplates.xaml into the same + /// dictionary, and its implicit CheckedComboBoxItem style has an empty ControlTemplate (a + /// base its own derived styles replace). The merged dictionary is searched first, so the + /// dropdown items get no visual tree and the list opens with zero height. + /// + /// Replacing the dictionary entries does not help: the context bar's DataTemplates keep the + /// style objects their StaticResource references were bound to. Instead, when a + /// CheckedComboBox applies its template, an ItemContainerStyle whose inherited template is + /// empty is swapped for a copy BasedOn Core.xaml's style. Styles with a real template, + /// including Reflected's own, are left alone, as is everything once Serif fixes the XAML. + /// + public static class ContextBarItemStylePatch + { + private static readonly Dictionary Rebased = new Dictionary(); + + private static Type _itemType; + private static Style _coreStyle; + private static bool _coreLookedUp; + + public static void ApplyPatches(Harmony harmony) + { + Logger.Info("Applying ContextBarItemStyle patch..."); + + var serifAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity"); + + if (serifAssembly == null) + { + Logger.Error("Serif.Affinity assembly not found"); + return; + } + + _itemType = serifAssembly.GetType("Serif.Affinity.UI.Controls.CheckedComboBoxItem"); + var comboType = serifAssembly.GetType("Serif.Affinity.UI.Controls.CheckedComboBox"); + var onApplyTemplate = comboType == null ? null : AccessTools.Method(comboType, "OnApplyTemplate"); + + if (_itemType == null || onApplyTemplate == null) + { + Logger.Error("CheckedComboBoxItem or CheckedComboBox.OnApplyTemplate not found"); + return; + } + + harmony.Patch(onApplyTemplate, postfix: new HarmonyMethod(typeof(ContextBarItemStylePatch), nameof(OnApplyTemplate_Postfix))); + Logger.Info("Patched CheckedComboBox.OnApplyTemplate"); + } + + public static void OnApplyTemplate_Postfix(ItemsControl __instance) + { + try + { + var style = __instance.ItemContainerStyle; + if (style == null || style.TargetType != _itemType) + return; + + if (!Rebased.TryGetValue(style, out var replacement)) + { + var template = FindTemplate(style); + var core = template == null || HasContent(template) ? null : CoreStyle(); + replacement = core == null ? null : Rebase(style, core); + Rebased[style] = replacement; + if (replacement != null) + Logger.Info("ContextBarItemStyle: dropdown item style with an empty template now based on Core.xaml's"); + } + + if (replacement != null) + __instance.ItemContainerStyle = replacement; + } + catch (Exception ex) + { + Logger.Error("ContextBarItemStyle: " + ex); + } + } + + /// Core.xaml's implicit CheckedComboBoxItem style from the application resources, if it has a template. + private static Style CoreStyle() + { + if (_coreLookedUp) + return _coreStyle; + _coreLookedUp = true; + + var core = System.Windows.Application.Current?.Resources.MergedDictionaries.FirstOrDefault(d => d.Source != null && + d.Source.OriginalString.EndsWith("/Core.xaml", StringComparison.OrdinalIgnoreCase)); + if (core != null && core.Contains(_itemType) && core[_itemType] is Style style && HasContent(FindTemplate(style))) + _coreStyle = style; + else + Logger.Info("ContextBarItemStyle: Core.xaml's CheckedComboBoxItem style not found, nothing changed"); + return _coreStyle; + } + + private static Style Rebase(Style style, Style basedOn) + { + if (style.Triggers.Count != 0 || style.Resources.Count != 0) + return null; + + var rebased = new Style(style.TargetType, basedOn); + foreach (var setterBase in style.Setters) + { + if (setterBase is Setter setter) + rebased.Setters.Add(new Setter(setter.Property, setter.Value, setter.TargetName)); + else if (setterBase is EventSetter eventSetter) + rebased.Setters.Add(new EventSetter(eventSetter.Event, eventSetter.Handler) { HandledEventsToo = eventSetter.HandledEventsToo }); + else + return null; + } + return rebased; + } + + /// The template the style applies: its own Template setter, else the nearest base's. + private static ControlTemplate FindTemplate(Style style) + { + for (var s = style; s != null; s = s.BasedOn) + { + foreach (var setterBase in s.Setters) + { + if (setterBase is Setter setter && setter.Property == Control.TemplateProperty) + return setter.Value as ControlTemplate; + } + } + return null; + } + + private static bool HasContent(FrameworkTemplate template) + { + if (template == null) + return false; + if (template.VisualTree != null) + return true; + var hasContent = typeof(FrameworkTemplate).GetProperty("HasContent", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + return hasContent != null && (bool)hasContent.GetValue(template); + } + } +} diff --git a/WineFix/WineFixPlugin.cs b/WineFix/WineFixPlugin.cs index d05ea2c..b3e3176 100644 --- a/WineFix/WineFixPlugin.cs +++ b/WineFix/WineFixPlugin.cs @@ -22,6 +22,7 @@ public class WineFixPlugin : AffinityPlugin public const string SettingWidenStubFix = "widen_stub_fix"; public const string SettingBezierSplitGuard = "bezier_split_guard"; public const string SettingCommandLineFileOpen = "command_line_file_open"; + public const string SettingContextBarItemStyle = "context_bar_item_style_fix"; public override PluginSettingsDefinition DefineSettings() { @@ -69,6 +70,10 @@ public override PluginSettingsDefinition DefineSettings() defaultValue: true, restartRequired: true, description: "Fix opening files from the command line or desktop file manager.") + .AddBool(SettingContextBarItemStyle, "Context toolbar: Fix empty Leading dropdown", + defaultValue: true, + restartRequired: true, + description: "Affinity 3.3 gives the text context toolbar's Leading dropdown items an empty template, so the list opens with nothing in it. Base those item styles on the standard dropdown item style instead. Does nothing once Affinity's own style has content.") .AddSection("Crash Fixes") .AddBool(SettingForceSyncFontEnum, "Force synchronous font enumeration", defaultValue: true, @@ -111,6 +116,16 @@ public override void OnPatch(Harmony harmony, IPluginContext context) context.Patch("MainWindowLoaded fix", h => Patches.MainWindowLoadedPatch.ApplyPatches(h)); + if (context.Settings.GetEffectiveValue(SettingContextBarItemStyle)) + { + context.Patch("ContextBarItemStyle fix", + h => Patches.ContextBarItemStylePatch.ApplyPatches(h)); + } + else + { + Logger.Info("Skipping ContextBarItemStyle fix (setting: disabled)"); + } + if (context.Settings.GetEffectiveValue(SettingForceSyncFontEnum)) { context.Patch("FontEnumeration fix",