Skip to content

Commit 181178d

Browse files
committed
Add support for dark color scheme
1 parent 786554f commit 181178d

File tree

8 files changed

+296
-20
lines changed

8 files changed

+296
-20
lines changed

SvgFileType/PaintGroupBoundaries.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
namespace SvgFileTypePlugin
1111
{
12-
// Used to determine boundaries of a group.
12+
/// <summary>
13+
/// Used to determine boundaries of a group.
14+
/// </summary>
1315
internal sealed class PaintGroupBoundaries : SvgVisualElement
1416
{
1517
public PaintGroupBoundaries(SvgGroup relatedGroup, bool isStart)

SvgFileType/PdnPluginForm.cs

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Copyright 2021 Osman Tunçelli. All rights reserved.
2+
// Use of this source code is governed by a LGPL license that can be
3+
// found in the COPYING file.
4+
5+
using PaintDotNet.VisualStyling;
6+
using System;
7+
using System.Drawing;
8+
using System.Runtime.InteropServices;
9+
using System.Security;
10+
using System.Windows.Forms;
11+
12+
namespace SvgFileTypePlugin
13+
{
14+
internal class PdnPluginForm : Form
15+
{
16+
public PdnPluginForm()
17+
{
18+
StartPosition = FormStartPosition.CenterParent;
19+
MaximizeBox = MinimizeBox = false;
20+
ShowIcon = false;
21+
}
22+
23+
public bool UseNativeDarkMode { get; set; }
24+
25+
protected override void OnHandleCreated(EventArgs e)
26+
{
27+
base.OnHandleCreated(e);
28+
InitTheme();
29+
}
30+
31+
public void InitTheme()
32+
{
33+
bool isDark = false;
34+
void Apply(Control.ControlCollection controls)
35+
{
36+
foreach (Control control in controls)
37+
{
38+
if (control is TransparentLabel)
39+
{
40+
continue;
41+
}
42+
43+
if (control is Label label)
44+
{
45+
label.FlatStyle = FlatStyle.System;
46+
if (control is LinkLabel link)
47+
{
48+
if (ForeColor != DefaultForeColor)
49+
{
50+
link.LinkColor = ForeColor;
51+
link.VisitedLinkColor = ForeColor;
52+
}
53+
else
54+
{
55+
link.LinkColor = Color.Empty;
56+
link.VisitedLinkColor = Color.Empty;
57+
}
58+
}
59+
}
60+
else if (control is ButtonBase buttonBase)
61+
{
62+
if (control is CheckBox || control is RadioButton)
63+
{
64+
buttonBase.FlatStyle = FlatStyle.Standard;
65+
}
66+
else
67+
{
68+
buttonBase.FlatStyle = FlatStyle.System;
69+
}
70+
}
71+
72+
if (isDark && UseNativeDarkMode)
73+
{
74+
Native.SetWindowTheme(control.Handle, "DarkMode_Explorer", null);
75+
}
76+
77+
control.ForeColor = ForeColor;
78+
control.BackColor = BackColor;
79+
if (control.HasChildren)
80+
{
81+
Apply(control.Controls);
82+
}
83+
}
84+
}
85+
86+
if (ThemeConfig.EffectiveTheme == PdnTheme.Aero)
87+
{
88+
if (AeroColors.IsDark)
89+
{
90+
BackColor = AeroColors.CanvasBackFillColor;
91+
if (UseNativeDarkMode)
92+
{
93+
Native.UseImmersiveDarkModeColors(Handle, true);
94+
}
95+
isDark = true;
96+
}
97+
else
98+
{
99+
BackColor = AeroColors.FormBackColor;
100+
}
101+
ForeColor = AeroColors.FormTextColor;
102+
}
103+
else if (SystemInformation.HighContrast)
104+
{
105+
BackColor = DefaultBackColor;
106+
ForeColor = DefaultForeColor;
107+
}
108+
else
109+
{
110+
BackColor = ClassicColors.FormBackColor;
111+
ForeColor = ClassicColors.FormForeColor;
112+
}
113+
Apply(Controls);
114+
}
115+
116+
public DialogResult ShowDialog(Form owner)
117+
{
118+
Func<DialogResult> action = () => base.ShowDialog(owner);
119+
return owner?.InvokeRequired == true ? (DialogResult)owner.Invoke(action) : action();
120+
}
121+
122+
public new DialogResult ShowDialog()
123+
{
124+
return ShowDialog(Utils.GetMainForm());
125+
}
126+
127+
protected void ModifyControl(Control control, Action action)
128+
{
129+
if (control.InvokeRequired)
130+
{
131+
control.Invoke((MethodInvoker)(() => ModifyControl(control, action)));
132+
return;
133+
}
134+
action();
135+
}
136+
137+
#region Dark Mode
138+
[SuppressUnmanagedCodeSecurity]
139+
internal static class Native
140+
{
141+
const string uxtheme = "uxtheme";
142+
const string user32 = "user32";
143+
144+
[DllImport(uxtheme, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
145+
public static extern int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList);
146+
147+
[DllImport(uxtheme, SetLastError = true, EntryPoint = "#133")]
148+
public static extern bool AllowDarkModeForWindow(IntPtr hWnd, bool allow);
149+
150+
[DllImport(uxtheme, SetLastError = true, EntryPoint = "#135")]
151+
static extern int SetPreferredAppMode(int i);
152+
153+
[DllImport(user32, SetLastError = true)]
154+
static extern bool SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
155+
156+
[DllImport(user32, SetLastError = true, CharSet = CharSet.Auto)]
157+
public static extern bool DestroyIcon(IntPtr handle);
158+
159+
[DllImport("dwmapi", SetLastError = true)]
160+
static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
161+
162+
[DllImport("gdi32", SetLastError = true)]
163+
static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
164+
165+
const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
166+
const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
167+
const int PREFFERED_APP_MODE__ALLOW_DARK = 1;
168+
const int PREFFERED_APP_MODE__DEFAULT = 0;
169+
170+
public enum DeviceCap
171+
{
172+
VERTRES = 10,
173+
DESKTOPVERTRES = 117
174+
}
175+
176+
[StructLayout(LayoutKind.Sequential)]
177+
struct WindowCompositionAttributeData
178+
{
179+
public WindowCompositionAttribute Attribute;
180+
public IntPtr Data;
181+
public int SizeOfData;
182+
}
183+
184+
public static void ControlSetDarkMode(IntPtr handle, bool v)
185+
{
186+
SetWindowTheme(handle, v ? "DarkMode_Explorer" : "Explorer", null);
187+
}
188+
189+
enum WindowCompositionAttribute
190+
{
191+
WCA_ACCENT_POLICY = 19,
192+
WCA_USEDARKMODECOLORS = 26
193+
}
194+
195+
internal static bool UseImmersiveDarkModeColors(IntPtr handle, bool dark)
196+
{
197+
int size = Marshal.SizeOf(typeof(int));
198+
IntPtr pBool = Marshal.AllocHGlobal(size);
199+
try
200+
{
201+
Marshal.WriteInt32(pBool, 0, dark ? 1 : 0);
202+
var data = new WindowCompositionAttributeData()
203+
{
204+
Attribute = WindowCompositionAttribute.WCA_USEDARKMODECOLORS,
205+
Data = pBool,
206+
SizeOfData = size
207+
};
208+
return SetWindowCompositionAttribute(handle, ref data);
209+
}
210+
finally
211+
{
212+
if (pBool != IntPtr.Zero)
213+
{
214+
Marshal.FreeHGlobal(pBool);
215+
}
216+
}
217+
}
218+
219+
public static void SetPrefferDarkMode(bool dark)
220+
{
221+
SetPreferredAppMode(dark ? PREFFERED_APP_MODE__ALLOW_DARK : PREFFERED_APP_MODE__DEFAULT);
222+
}
223+
224+
public static bool UseImmersiveDarkMode(IntPtr handle, bool enabled)
225+
{
226+
if (IsWindows10OrGreater(17763))
227+
{
228+
229+
var attribute = DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1;
230+
if (IsWindows10OrGreater(18985))
231+
{
232+
attribute = DWMWA_USE_IMMERSIVE_DARK_MODE;
233+
}
234+
235+
int useImmersiveDarkMode = enabled ? 1 : 0;
236+
return DwmSetWindowAttribute(handle, attribute, ref useImmersiveDarkMode, sizeof(int)) == 0;
237+
}
238+
239+
return false;
240+
}
241+
242+
public static bool IsWindows10OrGreater(int build = -1)
243+
{
244+
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= build;
245+
}
246+
247+
public static float GetScalingFactor()
248+
{
249+
float ScreenScalingFactor;
250+
251+
using (var g = Graphics.FromHwnd(IntPtr.Zero))
252+
{
253+
var desktop = g.GetHdc();
254+
var LogicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.VERTRES);
255+
var PhysicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.DESKTOPVERTRES);
256+
257+
ScreenScalingFactor = PhysicalScreenHeight / (float)LogicalScreenHeight;
258+
}
259+
260+
return ScreenScalingFactor; // 1.25 = 125%
261+
}
262+
}
263+
#endregion
264+
}
265+
}

SvgFileType/SvgElementExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ namespace SvgFileTypePlugin
99
{
1010
internal static class SvgElementExtensions
1111
{
12-
private static readonly MethodInfo ElementNameGetter = typeof(SvgElement).GetProperty("ElementName", BindingFlags.NonPublic | BindingFlags.Instance)?.GetGetMethod(true);
13-
private static readonly MethodInfo AttributesGetter = typeof(SvgElement).GetProperty("Attributes", BindingFlags.NonPublic | BindingFlags.Instance)?.GetGetMethod(true);
12+
private static readonly MethodInfo ElementNameGetter = GetGetMethod("ElementName");
13+
private static readonly MethodInfo AttributesGetter = GetGetMethod("Attributes");
1414

1515
public static string GetName(this SvgElement element)
1616
{
@@ -23,5 +23,10 @@ public static SvgAttributeCollection GetAttributes(this SvgElement element)
2323
{
2424
return (SvgAttributeCollection)AttributesGetter.Invoke(element, null);
2525
}
26+
27+
private static MethodInfo GetGetMethod(string propertyName)
28+
{
29+
return typeof(SvgElement).GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Instance)?.GetGetMethod(true);
30+
}
2631
}
2732
}

SvgFileType/SvgFileType.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,23 @@ public SvgFileType() : base("Scalable Vector Graphics", options)
2222
{
2323
}
2424

25-
protected override Document OnLoad(Stream input)
26-
=> new SvgImporter().Import(input);
25+
protected override Document OnLoad(Stream input)
26+
{
27+
return new SvgImporter().Import(input);
28+
}
2729

28-
protected override void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback)
29-
=> throw new NotSupportedException();
30+
protected override void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback)
31+
{
32+
throw new NotSupportedException();
33+
}
3034

3135
#region IFileTypeFactory
32-
public FileType[] GetFileTypeInstances() => new[] { new SvgFileType() };
36+
37+
public FileType[] GetFileTypeInstances()
38+
{
39+
return new FileType[] { new SvgFileType() };
40+
}
41+
3342
#endregion
3443
}
3544
}

SvgFileType/SvgImportDialog.Designer.cs

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

SvgFileType/SvgImportDialog.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace SvgFileTypePlugin
1212
{
13-
internal partial class SvgImportDialog : Form
13+
internal partial class SvgImportDialog : PdnPluginForm
1414
{
1515
#region Properties
1616

@@ -160,6 +160,7 @@ private void FixProgressLabel()
160160
ProgressLabel.Location = ProgressBar.Location;
161161
ProgressLabel.Width = ProgressBar.Width;
162162
ProgressLabel.Height = ProgressBar.Height;
163+
ProgressLabel.ForeColor = SystemColors.ControlText;
163164
}
164165

165166
private void UpdateCanvasH()

SvgFileType/SvgImporter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ private static int GetLayerCountToRender(IReadOnlyCollection<SvgVisualElement> e
232232
{
233233
return importHiddenLayers ? elements.Count : elements.Count(IsVisibleOriginally);
234234
}
235+
235236
private Document RenderElements(IReadOnlyCollection<SvgVisualElement> elements, bool setOpacityForLayer, bool importHiddenLayers, Action<int> progress, CancellationToken cancellationToken)
236237
{
237238
// I had problems to render each element directly while parent transformation can affect child.
@@ -260,7 +261,7 @@ private Document RenderElements(IReadOnlyCollection<SvgVisualElement> elements,
260261
var pdnLayer = new BitmapLayer(rasterWidth, rasterHeight)
261262
{
262263
Name = boundaryNode.ID,
263-
Opacity = (byte)(boundaryNode.RelatedGroup.Opacity * 255),
264+
Opacity = (byte)(boundaryNode.RelatedGroup.Opacity * 255f),
264265
Visible = boundaryNode.RelatedGroup.Visible
265266
};
266267
pdnDocument.Layers.Add(pdnLayer);

0 commit comments

Comments
 (0)