From 39baff5bc14633003548201157be2c3d5d65994a Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Wed, 23 Sep 2026 11:43:13 -0700 Subject: [PATCH] Improve theming of options and preview on VS --- src/Eto.Designer/PlatformProperties.cs | 5 + src/Eto.Designer/PreviewEditorView.cs | 21 ++ src/Eto.Designer/PreviewEditorViewSplitter.cs | 135 +++++++++- src/Eto.Designer/PreviewSplitBar.cs | 212 +++++++++++++++ src/Eto.Designer/RemoteDesignPanel.cs | 2 + src/Eto.DevExtension.PreviewHost/HostTheme.cs | 5 + .../Editor/CodeEditorHost.cs | 13 + .../Editor/EtoPreviewPane.cs | 102 +++++++- .../Editor/PreviewHostClient.cs | 53 ++-- .../Editor/PreviewLayoutSettings.cs | 58 +++++ .../Editor/PreviewPlatforms.cs | 242 ++++++++++++++++++ ...o.DevExtension.VisualStudio.Windows.csproj | 13 +- .../PlatformThemeHandler.cs | 10 + .../Wizards/EtoInitializer.cs | 9 +- .../Wizards/WindowTitleBar.cs | 62 +++++ .../theme/WindowStyles.xaml | 55 +++- 16 files changed, 956 insertions(+), 41 deletions(-) create mode 100644 src/Eto.Designer/PreviewSplitBar.cs create mode 100644 src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewLayoutSettings.cs create mode 100644 src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs create mode 100644 src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs diff --git a/src/Eto.Designer/PlatformProperties.cs b/src/Eto.Designer/PlatformProperties.cs index 86328ad..7de0385 100644 --- a/src/Eto.Designer/PlatformProperties.cs +++ b/src/Eto.Designer/PlatformProperties.cs @@ -21,6 +21,11 @@ public interface IPlatformTheme Color SummaryForeground { get; } Color DesignerBackground { get; } Color DesignerPanel { get; } + Color SplitBarBackground { get; } + Color SplitBarForeground { get; } + Color SplitBarHoverBackground { get; } + Color SplitBarSelectedBackground { get; } + Color SplitBarSelectedForeground { get; } IEnumerable AllColors { get; } } diff --git a/src/Eto.Designer/PreviewEditorView.cs b/src/Eto.Designer/PreviewEditorView.cs index 84e5b32..c510893 100755 --- a/src/Eto.Designer/PreviewEditorView.cs +++ b/src/Eto.Designer/PreviewEditorView.cs @@ -18,9 +18,27 @@ public class PreviewEditorView : Panel Func getCode; Control errorContent; Panel designPanelHolder; + Panel toolBarHolder; + Control toolBar; public double RefreshTime { get; set; } = 0.5; + /// Controls shown at the top right of the preview, such as a platform picker. + public Control ToolBar + { + get => toolBar; + set + { + toolBar = value; + toolBarHolder.Content = value == null ? null : new StackLayout + { + Orientation = Orientation.Horizontal, + Items = { new StackLayoutItem(null, expand: true), value } + }; + toolBarHolder.Visible = value != null; + } + } + public double ErrorDisplayTime { get; set; } = 6.0; public PreviewEditorView(IDesignHost designHost, Func getCode) @@ -36,11 +54,14 @@ public PreviewEditorView(IDesignHost designHost, Func getCode) errorPanel = new Panel { Padding = new Padding(5), Visible = false, BackgroundColor = new Color(Colors.Red, .4f) }; + toolBarHolder = new Panel { Padding = new Padding(6, 6, 10, 0), Visible = false, BackgroundColor = Global.Theme.DesignerBackground }; + Content = new StackLayout { HorizontalContentAlignment = HorizontalAlignment.Stretch, Items = { + toolBarHolder, new StackLayoutItem(designPanelHolder, expand: true), errorPanel } diff --git a/src/Eto.Designer/PreviewEditorViewSplitter.cs b/src/Eto.Designer/PreviewEditorViewSplitter.cs index ea4f7aa..b64a805 100755 --- a/src/Eto.Designer/PreviewEditorViewSplitter.cs +++ b/src/Eto.Designer/PreviewEditorViewSplitter.cs @@ -5,32 +5,148 @@ namespace Eto.Designer { - public class PreviewEditorViewSplitter : Splitter + /// Shows the preview and code editor split, with a bar to swap, rotate or collapse them. + public class PreviewEditorViewSplitter : Panel { static double lastPosition = 0.4; + readonly Splitter splitter; + readonly Panel firstSlot = new Panel(); + readonly Panel secondSlot = new Panel(); + readonly Panel innerBarSlot = new Panel(); + readonly Panel outerBarSlot = new Panel(); + readonly Panel mainSlot = new Panel(); + readonly PreviewSplitBar bar; + Orientation orientation = Orientation.Vertical; + bool swapped; + PreviewPane? singlePane; + // share of the space given to the design pane, whichever side it's on + double designPosition = lastPosition; + bool rebuilding; + public Control Editor { get; } public PreviewEditorView Preview { get; } + /// Raised when the user changes the orientation, swaps or collapses the panes. + public event EventHandler LayoutChanged; + + /// Vertical stacks the panes above each other, Horizontal puts them side by side. + public Orientation Orientation + { + get => orientation; + set => SetLayout(value, swapped, singlePane); + } + + /// Puts the code before the design. + public bool Swapped + { + get => swapped; + set => SetLayout(orientation, value, singlePane); + } + + /// The only pane shown, or null to show both. + public PreviewPane? SinglePane + { + get => singlePane; + set => SetLayout(orientation, swapped, value); + } + + /// Share of the space given to the design pane when split. + public double RelativePosition + { + get => designPosition; + set + { + designPosition = value; + ApplyPosition(); + } + } + public PreviewEditorViewSplitter(Control editor, IDesignHost designHost, Func getCode) { Preview = new PreviewEditorView(designHost, getCode); Editor = editor; - Orientation = Orientation.Vertical; - FixedPanel = SplitterFixedPanel.None; + splitter = new Splitter + { + FixedPanel = SplitterFixedPanel.None, + Panel1 = firstSlot, + Panel2 = new TableLayout(innerBarSlot, new TableRow(secondSlot) { ScaleHeight = true }) + }; + splitter.PositionChanged += Splitter_PositionChanged; - Panel1 = Preview; - Panel2 = editor; - RelativePosition = lastPosition; + bar = new PreviewSplitBar(this); + Content = new TableLayout(new TableRow(mainSlot) { ScaleHeight = true }, outerBarSlot); + Rebuild(); } + /// Applies a whole layout at once, such as one restored from settings. + public void SetLayout(Orientation orientation, bool swapped, PreviewPane? singlePane) + { + if (this.orientation == orientation && this.swapped == swapped && this.singlePane == singlePane) + return; + this.orientation = orientation; + this.swapped = swapped; + this.singlePane = singlePane; + Rebuild(); + LayoutChanged?.Invoke(this, EventArgs.Empty); + } + + internal void TabClicked(PreviewPane pane) + { + SinglePane = singlePane == pane ? (PreviewPane?)null : pane; + } + + // keeps the first pane, so swapping picks which one collapses + internal void ToggleCollapsed() => SinglePane = singlePane != null ? (PreviewPane?)null : swapped ? PreviewPane.Code : PreviewPane.Design; + + void Rebuild() + { + rebuilding = true; + // detach everything first so each control has one parent when re-added + mainSlot.Content = null; + firstSlot.Content = null; + secondSlot.Content = null; + innerBarSlot.Content = null; + outerBarSlot.Content = null; + + if (singlePane != null) + { + mainSlot.Content = singlePane == PreviewPane.Design ? Preview : Editor; + outerBarSlot.Content = bar; + } + else + { + splitter.Orientation = orientation; + firstSlot.Content = swapped ? Editor : Preview; + secondSlot.Content = swapped ? Preview : Editor; + // stacked panes keep the bar on the split line, like VS's XAML designer + if (orientation == Orientation.Vertical) + innerBarSlot.Content = bar; + else + outerBarSlot.Content = bar; + mainSlot.Content = splitter; + ApplyPosition(); + } + bar.UpdateState(); + rebuilding = false; + } - protected override void OnPositionChanged(EventArgs e) + void ApplyPosition() { - base.OnPositionChanged(e); - lastPosition = RelativePosition; + var old = rebuilding; + rebuilding = true; + splitter.RelativePosition = swapped ? 1 - designPosition : designPosition; + rebuilding = old; + } + + void Splitter_PositionChanged(object sender, EventArgs e) + { + if (rebuilding || singlePane != null || !splitter.Loaded) + return; + designPosition = swapped ? 1 - splitter.RelativePosition : splitter.RelativePosition; + lastPosition = designPosition; } protected override void OnGotFocus(EventArgs e) @@ -40,4 +156,3 @@ protected override void OnGotFocus(EventArgs e) } } } - diff --git a/src/Eto.Designer/PreviewSplitBar.cs b/src/Eto.Designer/PreviewSplitBar.cs new file mode 100644 index 0000000..4ac7b5e --- /dev/null +++ b/src/Eto.Designer/PreviewSplitBar.cs @@ -0,0 +1,212 @@ +using System; +using Eto.Forms; +using Eto.Drawing; + +namespace Eto.Designer +{ + /// A pane shown by . + public enum PreviewPane + { + Design, + Code + } + + /// + /// Bar between the preview and code, with tabs for each pane and buttons to rearrange them. + /// + class PreviewSplitBar : Panel + { + readonly PreviewEditorViewSplitter splitter; + readonly SplitBarButton designTab; + readonly SplitBarButton codeTab; + readonly SplitBarButton swapButton; + readonly SplitBarButton orientationButton; + readonly SplitBarButton collapseButton; + readonly StackLayout tabs = new StackLayout { Orientation = Orientation.Horizontal, VerticalContentAlignment = VerticalAlignment.Stretch }; + + public PreviewSplitBar(PreviewEditorViewSplitter splitter) + { + this.splitter = splitter; + BackgroundColor = Global.Theme.SplitBarBackground; + Padding = new Padding(2, 1); + + designTab = new SplitBarButton { Text = "Design" }; + designTab.Click += (sender, e) => splitter.TabClicked(PreviewPane.Design); + codeTab = new SplitBarButton { Text = "Code" }; + codeTab.Click += (sender, e) => splitter.TabClicked(PreviewPane.Code); + + swapButton = new SplitBarButton { ToolTip = "Swap panes", PaintIcon = PaintSwap }; + swapButton.Click += (sender, e) => splitter.Swapped = !splitter.Swapped; + + orientationButton = new SplitBarButton { PaintIcon = PaintOrientation }; + orientationButton.Click += (sender, e) => splitter.Orientation = splitter.Orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical; + + collapseButton = new SplitBarButton { PaintIcon = PaintCollapse }; + collapseButton.Click += (sender, e) => splitter.ToggleCollapsed(); + + Content = new StackLayout + { + Orientation = Orientation.Horizontal, + VerticalContentAlignment = VerticalAlignment.Stretch, + Items = { tabs, new StackLayoutItem(null, expand: true), orientationButton, collapseButton } + }; + UpdateState(); + } + + /// Reflects the splitter's current layout. + public void UpdateState() + { + var first = splitter.Swapped ? codeTab : designTab; + var second = splitter.Swapped ? designTab : codeTab; + tabs.Items.Clear(); + tabs.Items.Add(first); + tabs.Items.Add(swapButton); + tabs.Items.Add(second); + + var single = splitter.SinglePane; + designTab.Selected = single == PreviewPane.Design; + codeTab.Selected = single == PreviewPane.Code; + designTab.ToolTip = single == PreviewPane.Design ? "Show both panes" : "Show only the design"; + codeTab.ToolTip = single == PreviewPane.Code ? "Show both panes" : "Show only the code"; + + swapButton.Visible = single == null; + orientationButton.Visible = single == null; + orientationButton.ToolTip = splitter.Orientation == Orientation.Vertical ? "Show panes side by side" : "Show panes above each other"; + collapseButton.ToolTip = single == null ? "Collapse pane" : "Expand pane"; + foreach (var button in new[] { designTab, codeTab, swapButton, orientationButton, collapseButton }) + button.Invalidate(); + } + + void PaintSwap(Graphics g, RectangleF rect, Color color) + { + var c = rect.Center; + if (splitter.Orientation == Orientation.Vertical) + { + DrawArrow(g, color, new PointF(c.X - 2, c.Y + 4), new PointF(c.X - 2, c.Y - 4)); + DrawArrow(g, color, new PointF(c.X + 2, c.Y - 4), new PointF(c.X + 2, c.Y + 4)); + } + else + { + DrawArrow(g, color, new PointF(c.X + 4, c.Y - 2), new PointF(c.X - 4, c.Y - 2)); + DrawArrow(g, color, new PointF(c.X - 4, c.Y + 2), new PointF(c.X + 4, c.Y + 2)); + } + } + + void PaintOrientation(Graphics g, RectangleF rect, Color color) + { + // shows the layout the button switches to + var box = new RectangleF(rect.Center.X - 5, rect.Center.Y - 5, 10, 10); + g.DrawRectangle(color, box); + if (splitter.Orientation == Orientation.Vertical) + g.DrawLine(color, box.MiddleTop, box.MiddleBottom); + else + g.DrawLine(color, box.MiddleLeft, box.MiddleRight); + } + + void PaintCollapse(Graphics g, RectangleF rect, Color color) + { + // chevrons point the way the second pane collapses, or back when expanding + var c = rect.Center; + var sign = splitter.SinglePane == null ? 1 : -1; + for (int i = -1; i <= 1; i += 2) + { + var offset = i * 2 * sign; + if (splitter.Orientation == Orientation.Vertical || splitter.SinglePane != null) + { + var y = c.Y + offset; + g.DrawLines(color, new PointF(c.X - 4, y - 2 * sign), new PointF(c.X, y + 2 * sign), new PointF(c.X + 4, y - 2 * sign)); + } + else + { + var x = c.X + offset; + g.DrawLines(color, new PointF(x - 2 * sign, c.Y - 4), new PointF(x + 2 * sign, c.Y), new PointF(x - 2 * sign, c.Y + 4)); + } + } + } + + static void DrawArrow(Graphics g, Color color, PointF from, PointF to) + { + g.DrawLine(color, from, to); + var dir = to - from; + var length = (float)Math.Sqrt(dir.X * dir.X + dir.Y * dir.Y); + var unit = new PointF(dir.X / length, dir.Y / length); + var normal = new PointF(-unit.Y, unit.X); + g.DrawLine(color, to, to - unit * 3 + normal * 3); + g.DrawLine(color, to, to - unit * 3 - normal * 3); + } + } + + /// Flat tab or icon button drawn in the split bar's theme colors. + class SplitBarButton : Drawable + { + static readonly Font font = SystemFonts.Default(); + bool hover; + string text; + + public event EventHandler Click; + + public bool Selected { get; set; } + + public Action PaintIcon { get; set; } + + public string Text + { + get => text; + set + { + text = value; + var size = font.MeasureString(value); + Size = new Size((int)Math.Ceiling(size.Width) + 16, -1); + } + } + + public SplitBarButton() + { + Size = new Size(22, 20); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + var theme = Global.Theme; + var g = e.Graphics; + var rect = new RectangleF(PointF.Empty, Size); + if (Selected) + g.FillRectangle(theme.SplitBarSelectedBackground, rect); + else if (hover) + g.FillRectangle(theme.SplitBarHoverBackground, rect); + + var color = Selected ? theme.SplitBarSelectedForeground : theme.SplitBarForeground; + if (text != null) + { + var size = g.MeasureString(font, text); + g.DrawText(font, color, rect.Center - size / 2, text); + } + PaintIcon?.Invoke(g, rect, color); + } + + protected override void OnMouseEnter(MouseEventArgs e) + { + base.OnMouseEnter(e); + hover = true; + Invalidate(); + } + + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + hover = false; + Invalidate(); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + if (e.Buttons == MouseButtons.Primary) + { + e.Handled = true; + Click?.Invoke(this, EventArgs.Empty); + } + } + } +} diff --git a/src/Eto.Designer/RemoteDesignPanel.cs b/src/Eto.Designer/RemoteDesignPanel.cs index ad0fc70..8e651c3 100644 --- a/src/Eto.Designer/RemoteDesignPanel.cs +++ b/src/Eto.Designer/RemoteDesignPanel.cs @@ -21,6 +21,8 @@ public class PreviewRenderResult /// Size of the image in logical pixels. public Size Size { get; set; } public DesignError Error { get; set; } + /// Name of the platform it was drawn with, if known. + public string Platform { get; set; } } /// diff --git a/src/Eto.DevExtension.PreviewHost/HostTheme.cs b/src/Eto.DevExtension.PreviewHost/HostTheme.cs index b183ac2..9a4af56 100644 --- a/src/Eto.DevExtension.PreviewHost/HostTheme.cs +++ b/src/Eto.DevExtension.PreviewHost/HostTheme.cs @@ -24,6 +24,11 @@ Color Get(string name, Color fallback) => public Color DesignerBackground => Get(nameof(DesignerBackground), SystemColors.Control); // the platform's window colour, so panels look like forms in dark mode, but Gtk can report it as transparent public Color DesignerPanel => Get(nameof(DesignerPanel), SystemColors.WindowBackground is { A: > 0 } window ? window : Color.FromRgb(0xF0F0F0)); + public Color SplitBarBackground => Get(nameof(SplitBarBackground), SystemColors.Control); + public Color SplitBarForeground => Get(nameof(SplitBarForeground), SystemColors.ControlText); + public Color SplitBarHoverBackground => Get(nameof(SplitBarHoverBackground), SystemColors.Highlight); + public Color SplitBarSelectedBackground => Get(nameof(SplitBarSelectedBackground), SystemColors.Highlight); + public Color SplitBarSelectedForeground => Get(nameof(SplitBarSelectedForeground), SystemColors.HighlightText); public IEnumerable AllColors => Enumerable.Empty(); } } diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/CodeEditorHost.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/CodeEditorHost.cs index 350fa10..d6470f0 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/CodeEditorHost.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/CodeEditorHost.cs @@ -61,5 +61,18 @@ public CodeEditorHost(IVsTextLines textBuffer) } } } + + /// Closes the text view, before the document closes. Safe to call more than once. + public int Close() + { + ThreadHelper.ThrowIfNotOnUIThread(); + var window = codeWindow; + if (window == null) + return VSConstants.S_OK; + codeWindow = null; + viewAdapter = null; + textViewHost = null; + return window.Close(); + } } } diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs index 4c2bd39..432ac4f 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs @@ -50,9 +50,15 @@ public sealed class EtoPreviewPane : Microsoft.VisualStudio.Shell.WindowPane, PreviewEditorView preview; PreviewEditorViewSplitter previewSplitter; PreviewHostClient previewHost; + DropDown platformDropDown; + // label of the platform the last preview was drawn with, shown beside Auto + string drawnPlatform; + bool loadingPlatforms; Panel editorControl; uint dataEventsCookie; uint linesEventsCookie; + uint docCookie; + bool disposed; IWpfTextView WpfTextView => editor.textViewHost?.TextView; @@ -116,7 +122,16 @@ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines tex var host = previewHost = PreviewHostClient.Acquire(projectKey); host.ProjectChanged += PreviewHost_ProjectChanged; - var designHost = new RemoteDesignPanel(async request => await host.RenderAsync(request, await ProjectAssemblyPaths.GetAsync(fileName))); + var designHost = new RemoteDesignPanel(async request => + { + var result = await host.RenderAsync(request, await ProjectAssemblyPaths.GetAsync(fileName)); + if (result?.Platform != null && result.Platform != drawnPlatform) + { + drawnPlatform = result.Platform; + LoadPlatforms(); + } + return result; + }); previewSplitter = new PreviewEditorViewSplitter(editorControl, designHost, () => textBuffer?.GetText()); previewSplitter.GotFocus += (sender, e) => @@ -124,8 +139,17 @@ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines tex WpfTextView?.VisualElement?.Focus(); }; preview = previewSplitter.Preview; + PreviewLayoutSettings.Load(previewSplitter); + previewSplitter.LayoutChanged += (sender, e) => PreviewLayoutSettings.Save(previewSplitter); + + platformDropDown = new DropDown { ToolTip = "Platform to draw the preview with" }; + platformDropDown.SelectedKeyChanged += PlatformDropDown_SelectedKeyChanged; + preview.ToolBar = platformDropDown; + LoadPlatforms(); + PreviewPlatforms.ChoiceChanged += PreviewPlatforms_ChoiceChanged; var content = previewSplitter.ToNative(true); + Wizards.EtoInitializer.ApplyTheme(content); if (!preview.SetBuilder(fileName)) throw new InvalidOperationException(string.Format("Could not find builder for file {0}", fileName)); @@ -223,9 +247,44 @@ protected override void Initialize() InheritKeyBindings(); + // the document may not be registered yet while the frame is being created + if (!LockDocument()) + ThreadHelper.JoinableTaskFactory.StartOnIdle(() => LockDocument()); + preview.Update(); } + /// + /// Holds the document open until our text view is closed, as VS releases it before closing the pane, + /// and a view still open when it closes throws from the editor's margins. + /// + bool LockDocument() + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (disposed || docCookie != 0) + return true; + var rdt = (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable)); + if (rdt == null) + return false; + var hr = rdt.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_ReadLock, FileName, out _, out _, out var docData, out var cookie); + if (docData != IntPtr.Zero) + Marshal.Release(docData); + if (hr != VSConstants.S_OK || cookie == 0) + return false; + docCookie = cookie; + return true; + } + + void UnlockDocument() + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (docCookie == 0) + return; + var rdt = (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable)); + rdt?.UnlockDocument((uint)(_VSRDTFLAGS.RDT_ReadLock | _VSRDTFLAGS.RDT_Unlock_NoSave), docCookie); + docCookie = 0; + } + void InheritKeyBindings() { ThreadHelper.ThrowIfNotOnUIThread(); @@ -266,6 +325,14 @@ protected override void Dispose(bool disposing) RegisterIndependentView(false); + PreviewPlatforms.ChoiceChanged -= PreviewPlatforms_ChoiceChanged; + + disposed = true; + + // close the view before letting the document close + editor.Close(); + UnlockDocument(); + if (previewHost != null) { previewHost.ProjectChanged -= PreviewHost_ProjectChanged; @@ -322,6 +389,37 @@ void ViewCode() void PreviewHost_ProjectChanged(object sender, EventArgs e) => preview?.Update(); + void LoadPlatforms() + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (platformDropDown == null) + return; + var choice = PreviewPlatforms.Choice; + var auto = choice == PreviewPlatforms.Auto && drawnPlatform != null ? $"Auto ({drawnPlatform})" : "Auto"; + var items = new List { new ListItem { Key = PreviewPlatforms.Auto, Text = auto } }; + items.AddRange(PreviewPlatforms.GetAvailable().Select(r => new ListItem { Key = r.Id, Text = r.Label })); + + loadingPlatforms = true; + platformDropDown.DataStore = items; + platformDropDown.SelectedKey = items.Any(r => r.Key == choice) ? choice : PreviewPlatforms.Auto; + loadingPlatforms = false; + } + + void PlatformDropDown_SelectedKeyChanged(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (!loadingPlatforms && platformDropDown.SelectedKey != null) + PreviewPlatforms.Choice = platformDropDown.SelectedKey; + } + + void PreviewPlatforms_ChoiceChanged(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + drawnPlatform = null; + LoadPlatforms(); + preview?.Update(); + } + void IVsTextBufferDataEvents.OnFileChanged(uint grfChange, uint dwFileAttrs) { preview.Update(); @@ -370,7 +468,7 @@ public override int LoadUIState(Stream stateStream) public int GetViewClassID(out Guid pclsidView) => editor.codeWindow.GetViewClassID(out pclsidView); public int SetBaseEditorCaption(string[] pszBaseEditorCaption) => editor.codeWindow.SetBaseEditorCaption(pszBaseEditorCaption); public int GetEditorCaption(READONLYSTATUS dwReadOnly, out string pbstrEditorCaption) => editor.codeWindow.GetEditorCaption(dwReadOnly, out pbstrEditorCaption); - public int Close() => editor.codeWindow.Close(); + public int Close() => editor.Close(); public int GetLastActiveView(out IVsTextView ppView) => editor.codeWindow.GetLastActiveView(out ppView); int IVsCodeWindowEx.Initialize(uint grfCodeWindowBehaviorFlags, VSUSERCONTEXTATTRIBUTEUSAGE usageAuxUserContext, string szNameAuxUserContext, string szValueAuxUserContext, uint InitViewFlags, INITVIEW[] pInitView) => ((IVsCodeWindowEx)editor.codeWindow).Initialize(grfCodeWindowBehaviorFlags, usageAuxUserContext, szNameAuxUserContext, szValueAuxUserContext, InitViewFlags, pInitView); diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs index 1c0b93f..73d48d7 100644 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs @@ -19,18 +19,19 @@ namespace Eto.DevExtension.VisualStudio.Windows.Editor /// sealed class PreviewHostClient { - const string HostDll = "Eto.DevExtension.PreviewHost.dll"; // long enough for a first render that compiles code, short enough to recover from a hung control static readonly TimeSpan RenderTimeout = TimeSpan.FromSeconds(30); static readonly Dictionary hosts = new Dictionary(StringComparer.OrdinalIgnoreCase); - static bool unavailable; + // platforms whose host couldn't start, usually for want of a runtime, so don't keep trying + static readonly HashSet unavailable = new HashSet(StringComparer.OrdinalIgnoreCase); readonly string key; readonly SemaphoreSlim requestLock = new SemaphoreSlim(1, 1); int references; Process process; JsonRpc rpc; + PreviewPlatform platform; /// Raised on the UI thread after the project is rebuilt, so previews can be redrawn. public event EventHandler ProjectChanged; @@ -70,11 +71,12 @@ public async Task RenderAsync(PreviewRenderRequest request, try { // a second try covers a host that exited after a rebuild, or that serves an older build + var wanted = await PreviewPlatforms.ResolveAsync(); for (var attempt = 0; attempt < 2; attempt++) { - var server = await GetServerAsync(); + var server = await GetServerAsync(wanted); if (server == null) - return Error("The preview needs the .NET 8 Desktop Runtime (or newer) to be installed.", null); + return Error(wanted.Requirement, null, wanted); try { @@ -98,22 +100,22 @@ public async Task RenderAsync(PreviewRenderRequest request, Stop(); continue; } - return Read(result); + return Read(result, wanted); } } catch (OperationCanceledException) { Stop(); - return Error("The preview took too long to draw, so it was stopped.", null); + return Error("The preview took too long to draw, so it was stopped.", null, wanted); } catch (Exception ex) when (ex is ConnectionLostException || ex is ObjectDisposedException || ex is IOException) { Stop(); if (attempt > 0) - return Error("The preview host stopped unexpectedly.", ex.ToString()); + return Error("The preview host stopped unexpectedly.", ex.ToString(), wanted); } } - return Error("The preview host could not load the project.", null); + return Error("The preview host could not load the project.", null, wanted); } finally { @@ -121,44 +123,44 @@ public async Task RenderAsync(PreviewRenderRequest request, } } - static PreviewRenderResult Read(JToken result) + static PreviewRenderResult Read(JToken result, PreviewPlatform platform) { var error = result?["error"]; if (error != null && error.Type == JTokenType.Object) - return Error((string)error["message"], (string)error["details"]); + return Error((string)error["message"], (string)error["details"], platform); var image = (string)result?["image"]; return new PreviewRenderResult { Image = string.IsNullOrEmpty(image) ? null : Convert.FromBase64String(image), - Size = new Size((int?)result?["width"] ?? 0, (int?)result?["height"] ?? 0) + Size = new Size((int?)result?["width"] ?? 0, (int?)result?["height"] ?? 0), + Platform = platform.Label }; } - static PreviewRenderResult Error(string message, string details) => - new PreviewRenderResult { Error = new DesignError { Message = message, Details = details ?? message } }; + static PreviewRenderResult Error(string message, string details, PreviewPlatform platform) => + new PreviewRenderResult { Error = new DesignError { Message = message, Details = details ?? message }, Platform = platform.Label }; - async Task GetServerAsync() + async Task GetServerAsync(PreviewPlatform wanted) { - if (IsRunning) + if (IsRunning && platform?.Id == wanted.Id) return rpc; - if (unavailable) + if (unavailable.Contains(wanted.Id)) return null; - // clean up after a host that exited, such as after a rebuild + // clean up after a host that exited, such as after a rebuild, or one drawing with another platform Stop(); - var hostPath = Path.Combine(Path.GetDirectoryName(typeof(PreviewHostClient).Assembly.Location), "preview", HostDll); - if (!File.Exists(hostPath)) + if (!File.Exists(wanted.HostPath)) { - unavailable = true; - Debug.WriteLine($"Eto preview host not found at {hostPath}"); + unavailable.Add(wanted.Id); + Debug.WriteLine($"Eto preview host not found at {wanted.HostPath}"); return null; } try { - var start = new ProcessStartInfo("dotnet", "\"" + hostPath + "\"") + var start = new ProcessStartInfo("dotnet", $"\"{wanted.HostPath}\" --platform {wanted.Id}") { UseShellExecute = false, CreateNoWindow = true, @@ -166,7 +168,10 @@ async Task GetServerAsync() RedirectStandardOutput = true, RedirectStandardError = true }; + if (wanted.LibraryPath != null) + start.EnvironmentVariables["PATH"] = wanted.LibraryPath + Path.PathSeparator + start.EnvironmentVariables["PATH"]; process = Process.Start(start); + platform = wanted; process.ErrorDataReceived += (sender, e) => Debug.WriteLine(e.Data); process.BeginErrorReadLine(); @@ -180,8 +185,7 @@ async Task GetServerAsync() } catch (Exception ex) { - // usually no desktop runtime to run it with, so don't keep trying - unavailable = true; + unavailable.Add(wanted.Id); Debug.WriteLine($"Could not start the Eto preview host: {ex}"); Stop(); return null; @@ -212,6 +216,7 @@ void Stop() var oldProcess = process; rpc = null; process = null; + platform = null; try { oldRpc?.Dispose(); diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewLayoutSettings.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewLayoutSettings.cs new file mode 100644 index 0000000..167ec13 --- /dev/null +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewLayoutSettings.cs @@ -0,0 +1,58 @@ +using Eto.Designer; +using Eto.Forms; +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Settings; +using System; +using System.Diagnostics; + +namespace Eto.DevExtension.VisualStudio.Windows.Editor +{ + /// + /// Remembers how the last changed preview laid out its panes, for newly opened files. + /// + static class PreviewLayoutSettings + { + const string CollectionPath = @"Eto.DevExtension\PreviewLayout"; + + /// Applies the saved layout to a newly created splitter. Use on the UI thread. + public static void Load(PreviewEditorViewSplitter splitter) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var store = GetStore(); + if (store == null || !store.CollectionExists(CollectionPath)) + return; + var orientation = Enum.TryParse(store.GetString(CollectionPath, "Orientation", null), out var o) ? o : splitter.Orientation; + var swapped = store.GetBoolean(CollectionPath, "Swapped", splitter.Swapped); + var singlePane = Enum.TryParse(store.GetString(CollectionPath, "SinglePane", null), out var p) ? p : (PreviewPane?)null; + splitter.SetLayout(orientation, swapped, singlePane); + } + + /// Saves the splitter's layout. Use on the UI thread. + public static void Save(PreviewEditorViewSplitter splitter) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var store = GetStore(); + if (store == null) + return; + store.CreateCollection(CollectionPath); + store.SetString(CollectionPath, "Orientation", splitter.Orientation.ToString()); + store.SetBoolean(CollectionPath, "Swapped", splitter.Swapped); + store.SetString(CollectionPath, "SinglePane", splitter.SinglePane?.ToString() ?? string.Empty); + } + + static WritableSettingsStore GetStore() + { + ThreadHelper.ThrowIfNotOnUIThread(); + try + { + return new ShellSettingsManager(ServiceProvider.GlobalProvider).GetWritableSettingsStore(SettingsScope.UserSettings); + } + catch (Exception ex) + { + Debug.WriteLine($"Eto preview: could not open settings: {ex.Message}"); + return null; + } + } + } +} diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs new file mode 100644 index 0000000..6b7527b --- /dev/null +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs @@ -0,0 +1,242 @@ +using EnvDTE; +using EnvDTE80; +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell.Settings; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Eto.DevExtension.VisualStudio.Windows.Editor +{ + /// A platform the preview can draw with. + sealed class PreviewPlatform + { + /// Name passed to the host with --platform. + public string Id { get; set; } + public string Label { get; set; } + /// Matches a project that uses this platform, from a package or project reference. + public Regex Reference { get; set; } + public string HostPath { get; set; } + /// Folder the host needs first on PATH, or null. + public string LibraryPath { get; set; } + /// Shown when the host can't be started. + public string Requirement { get; set; } + } + + /// + /// The platforms the preview can draw with, and the one picked for the open solution. + /// + static class PreviewPlatforms + { + public const string Auto = "auto"; + const string HostDll = "Eto.DevExtension.PreviewHost.dll"; + const string CollectionPath = @"Eto.DevExtension\PreviewPlatform"; + const string Requirement = "The preview needs the .NET 8 Desktop Runtime (or newer) to be installed."; + // scanning projects on every keystroke is too slow, but an edited project should still be noticed + static readonly TimeSpan CacheTime = TimeSpan.FromSeconds(10); + + static (DateTime Time, string Solution, string Id) autoCache; + // used when there's no solution to remember it with + static string unsavedChoice = Auto; + + /// Raised on the UI thread when changes. + public static event EventHandler ChoiceChanged; + + /// Platforms available on this machine, best first. + public static List GetAvailable() + { + var previewDir = Path.Combine(Path.GetDirectoryName(typeof(PreviewPlatforms).Assembly.Location), "preview"); + var windowsHost = Path.Combine(previewDir, "win", HostDll); + var platforms = new List + { + new PreviewPlatform { Id = "Wpf", Label = "WPF", Reference = new Regex(@"Eto\.Platform\.Wpf\b|Eto\.Wpf\.csproj", RegexOptions.IgnoreCase), HostPath = windowsHost, Requirement = Requirement }, + new PreviewPlatform { Id = "WinForms", Label = "WinForms", Reference = new Regex(@"Eto\.Platform\.Windows\b|Eto\.WinForms\.csproj", RegexOptions.IgnoreCase), HostPath = windowsHost, Requirement = Requirement } + }; + + var gtk = FindGtk(); + var netHost = Path.Combine(previewDir, "net", HostDll); + if (gtk != null && File.Exists(netHost)) + { + platforms.Add(new PreviewPlatform + { + Id = "Gtk", + Label = "Gtk", + Reference = new Regex(@"Eto\.Platform\.Gtk\b|Eto\.Gtk\.csproj", RegexOptions.IgnoreCase), + HostPath = netHost, + LibraryPath = gtk, + Requirement = "The preview needs the .NET 8 runtime (or newer) and GTK 3." + }); + } + return platforms; + } + + /// A platform id or , remembered for each solution. Use on the UI thread. + public static string Choice + { + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + var solution = GetSolutionPath(); + if (string.IsNullOrEmpty(solution)) + return unsavedChoice; + var store = GetStore(); + return store?.GetString(CollectionPath, solution, Auto) ?? Auto; + } + set + { + ThreadHelper.ThrowIfNotOnUIThread(); + value = value ?? Auto; + if (value == Choice) + return; + + var solution = GetSolutionPath(); + var store = string.IsNullOrEmpty(solution) ? null : GetStore(); + if (store == null) + unsavedChoice = value; + else if (value == Auto) + { + if (store.CollectionExists(CollectionPath)) + store.DeleteProperty(CollectionPath, solution); + } + else + { + store.CreateCollection(CollectionPath); + store.SetString(CollectionPath, solution, value); + } + ChoiceChanged?.Invoke(null, EventArgs.Empty); + } + } + + /// The platform to draw with: the one picked, or for Auto the first the solution references, otherwise WPF. + public static async Task ResolveAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var platforms = GetAvailable(); + var choice = Choice; + return platforms.FirstOrDefault(r => r.Id == choice) ?? Detect(platforms); + } + + static PreviewPlatform Detect(List platforms) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var solution = GetSolutionPath() ?? string.Empty; + var cached = autoCache; + if (cached.Solution == solution && DateTime.UtcNow - cached.Time < CacheTime) + { + var platform = platforms.FirstOrDefault(r => r.Id == cached.Id); + if (platform != null) + return platform; + } + + var texts = GetProjectFiles().Select(ReadText).ToList(); + var referenced = platforms.FirstOrDefault(platform => texts.Any(text => platform.Reference.IsMatch(text))); + var result = referenced ?? platforms[0]; + if (cached.Id != result.Id || cached.Solution != solution) + { + Debug.WriteLine(referenced != null + ? $"Eto preview: drawing with {result.Label}, as the solution uses it." + : $"Eto preview: drawing with {result.Label}, as the solution doesn't reference an Eto platform available here."); + } + autoCache = (DateTime.UtcNow, solution, result.Id); + return result; + } + + static IEnumerable GetProjectFiles() + { + ThreadHelper.ThrowIfNotOnUIThread(); + var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); + var files = new List(); + if (dte?.Solution?.Projects != null) + { + foreach (Project project in dte.Solution.Projects) + AddProjectFiles(project, files); + } + return files; + } + + static void AddProjectFiles(Project project, List files) + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (project == null) + return; + if (project.Kind == ProjectKinds.vsProjectKindSolutionFolder) + { + foreach (ProjectItem item in project.ProjectItems) + AddProjectFiles(item.SubProject, files); + return; + } + try + { + if (!string.IsNullOrEmpty(project.FullName)) + files.Add(project.FullName); + } + catch (Exception ex) + { + // unloaded projects throw + Debug.WriteLine($"Eto preview: could not read project {project.Name}: {ex.Message}"); + } + } + + static string ReadText(string fileName) + { + try + { + return File.ReadAllText(fileName); + } + catch + { + return string.Empty; + } + } + + /// Folder holding GTK 3, or null when it isn't installed. + static string FindGtk() + { + // where GtkSharp's build installs it, or anywhere on PATH such as MSYS2 + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var installed = string.IsNullOrEmpty(localAppData) ? null : Path.Combine(localAppData, "Gtk", "3.24.24"); + var path = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator); + return new[] { installed }.Concat(path).FirstOrDefault(r => !string.IsNullOrWhiteSpace(r) && HasGtk(r)); + } + + static bool HasGtk(string dir) + { + try + { + return File.Exists(Path.Combine(dir, "libgtk-3-0.dll")); + } + catch (ArgumentException) + { + // PATH can hold invalid entries + return false; + } + } + + static string GetSolutionPath() + { + ThreadHelper.ThrowIfNotOnUIThread(); + var dte = (DTE2)Package.GetGlobalService(typeof(SDTE)); + return dte?.Solution?.FullName; + } + + static WritableSettingsStore GetStore() + { + ThreadHelper.ThrowIfNotOnUIThread(); + try + { + return new ShellSettingsManager(ServiceProvider.GlobalProvider).GetWritableSettingsStore(SettingsScope.UserSettings); + } + catch (Exception ex) + { + Debug.WriteLine($"Eto preview: could not open settings: {ex.Message}"); + return null; + } + } + } +} diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj b/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj index d83579a..1f009c1 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj +++ b/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj @@ -86,6 +86,8 @@ + + @@ -110,6 +112,7 @@ + @@ -285,11 +288,15 @@ $(MSBuildProjectDirectory)\..\Eto.DevExtension.PreviewHost\Eto.DevExtension.PreviewHost.csproj $(ArtifactsDir)Eto.DevExtension.PreviewHost\publish\$(Configuration)\ + - + + - - + + + + diff --git a/src/Eto.DevExtension.VisualStudio.Windows/PlatformThemeHandler.cs b/src/Eto.DevExtension.VisualStudio.Windows/PlatformThemeHandler.cs index 1138936..1b31757 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/PlatformThemeHandler.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/PlatformThemeHandler.cs @@ -33,6 +33,16 @@ class PlatformThemeHandler : IPlatformTheme public Color DesignerPanel => Color.FromRgb(0xF0F0F0); // hmm. + public Color SplitBarBackground => VSColorTheme.GetThemedColor(EnvironmentColors.CommandBarGradientBeginColorKey).ToEto(); + + public Color SplitBarForeground => VSColorTheme.GetThemedColor(EnvironmentColors.CommandBarTextActiveColorKey).ToEto(); + + public Color SplitBarHoverBackground => VSColorTheme.GetThemedColor(EnvironmentColors.CommandBarMouseOverBackgroundBeginColorKey).ToEto(); + + public Color SplitBarSelectedBackground => VSColorTheme.GetThemedColor(EnvironmentColors.FileTabSelectedGradientTopColorKey).ToEto(); + + public Color SplitBarSelectedForeground => VSColorTheme.GetThemedColor(EnvironmentColors.FileTabSelectedTextColorKey).ToEto(); + public IEnumerable AllColors { get diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs index 01c7961..4656be9 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs @@ -38,7 +38,14 @@ public static void Initialize() private static void ThemeWindow(System.Windows.Window w) { - w.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/Eto.DevExtension.VisualStudio.Windows;component/theme/WindowStyles.xaml", UriKind.RelativeOrAbsolute) }); + ApplyTheme(w); + WindowTitleBar.Attach(w); + } + + /// Styles the Eto controls within the element to match the VS theme. + public static void ApplyTheme(System.Windows.FrameworkElement element) + { + element.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/Eto.DevExtension.VisualStudio.Windows;component/theme/WindowStyles.xaml", UriKind.RelativeOrAbsolute) }); } } } diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs new file mode 100644 index 0000000..a9f169e --- /dev/null +++ b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs @@ -0,0 +1,62 @@ +using Microsoft.VisualStudio.PlatformUI; +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; + +namespace Eto.DevExtension.VisualStudio.Windows.Wizards +{ + /// + /// Colours a window's OS-drawn title bar to match the VS theme, following theme changes while it's open. + /// + static class WindowTitleBar + { + const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + const int DWMWA_BORDER_COLOR = 34; + const int DWMWA_CAPTION_COLOR = 35; + const int DWMWA_TEXT_COLOR = 36; + + [DllImport("dwmapi.dll")] + static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size); + + /// Themes the title bar of the window now, or once it has a handle. + public static void Attach(Window window) + { + void Apply() => ApplyTheme(window); + void ThemeChanged(ThemeChangedEventArgs e) => window.Dispatcher.BeginInvoke((Action)Apply); + + if (new WindowInteropHelper(window).Handle != IntPtr.Zero) + Apply(); + else + window.SourceInitialized += (sender, e) => Apply(); + + VSColorTheme.ThemeChanged += ThemeChanged; + window.Closed += (sender, e) => VSColorTheme.ThemeChanged -= ThemeChanged; + } + + static void ApplyTheme(Window window) + { + var hwnd = new WindowInteropHelper(window).Handle; + if (hwnd == IntPtr.Zero) + return; + + var caption = VSColorTheme.GetThemedColor(EnvironmentColors.MainWindowActiveCaptionColorKey); + var text = VSColorTheme.GetThemedColor(EnvironmentColors.MainWindowActiveCaptionTextColorKey); + var border = VSColorTheme.GetThemedColor(EnvironmentColors.MainWindowActiveDefaultBorderColorKey); + + // dark mode alone covers Windows 10, where the caption colours below are ignored + var dark = caption.GetBrightness() < 0.5f ? 1 : 0; + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref dark, sizeof(int)); + Set(hwnd, DWMWA_CAPTION_COLOR, caption); + Set(hwnd, DWMWA_TEXT_COLOR, text); + Set(hwnd, DWMWA_BORDER_COLOR, border); + } + + static void Set(IntPtr hwnd, int attribute, System.Drawing.Color color) + { + // COLORREF is 0x00BBGGRR + var value = color.R | (color.G << 8) | (color.B << 16); + DwmSetWindowAttribute(hwnd, attribute, ref value, sizeof(int)); + } + } +} diff --git a/src/Eto.DevExtension.VisualStudio.Windows/theme/WindowStyles.xaml b/src/Eto.DevExtension.VisualStudio.Windows/theme/WindowStyles.xaml index 622203d..abd24a4 100755 --- a/src/Eto.DevExtension.VisualStudio.Windows/theme/WindowStyles.xaml +++ b/src/Eto.DevExtension.VisualStudio.Windows/theme/WindowStyles.xaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:eto="clr-namespace:Eto.Wpf.Forms;assembly=Eto.Wpf" xmlns:etoc="clr-namespace:Eto.Wpf.Forms.Controls;assembly=Eto.Wpf" - xmlns:vsp="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0"> + xmlns:vsp="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0" + xmlns:vs="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"> @@ -83,6 +84,7 @@ + @@ -212,4 +214,55 @@ + + + + + + + + + + + + \ No newline at end of file