diff --git a/CHANGELOG.md b/CHANGELOG.md index 6073726..3c31c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.15.0 + +GUI restructure from the audit — ProfileCard (GUI-1) and Edit-profile dialog (GUI-5). Layout-only changes; every field and action is preserved. + +Changed: + +- **GUI-1 ProfileCard.** Header rebuilt as `[status dot] [title] [drive chip] [pill] [⋯]`. The chip surfaces the assigned drive letter, the dot lights up green when mounted / gray when not. The five less-frequent actions (Test, Edit, Set primary, Auto-mount toggle, Remove) move from a second row of always-visible buttons into a `ContextMenuStrip` triggered by the `⋯` header button — trimming the action bar from 9 visible buttons to 4 (Mount / Full cache / Unmount / Open). Auto-mount state is now a checked menu item. +- **GUI-5 Edit-profile dialog.** Same fields, but now organised into four tabs (General · Bandwidth · Schedule · Watch) instead of one tall scrolling form. Each concern is self-contained on its own tab so you don't have to scroll past everything to reach the Watch group at the bottom. The Save path reads the same field locals; no logic change. + ## 0.14.2 ARCH-2 from the audit. SDK-style csproj alongside the existing `csc.exe` build, so `dotnet build` and `dotnet test` work out of the box without losing the PowerShell pipeline. diff --git a/src/Pixelpipe.MainWindow.cs b/src/Pixelpipe.MainWindow.cs index 2306010..b56b1ea 100644 --- a/src/Pixelpipe.MainWindow.cs +++ b/src/Pixelpipe.MainWindow.cs @@ -951,6 +951,11 @@ private sealed class ProfileCard public readonly Panel Root; private readonly Label titleLabel; private readonly Label statusPill; + private readonly StatusDot statusDot; + private readonly Label driveChip; + private readonly Button overflowBtn; + private readonly ContextMenuStrip overflowMenu; + private readonly ToolStripMenuItem overflowAutoMount; private readonly Label remoteLabel; private readonly Label driveLabel; private readonly Label statusLabel; @@ -966,11 +971,6 @@ private sealed class ProfileCard private readonly Button mountFull; private readonly Button unmount; private readonly Button openDrive; - private readonly Button testBtn; - private readonly Button editBtn; - private readonly Button setPrimaryBtn; - private readonly Button autoMountBtn; - private readonly Button removeBtn; public ProfileCard(TrayContext owner, RemoteProfile p) { @@ -1002,14 +1002,20 @@ public ProfileCard(TrayContext owner, RemoteProfile p) layout.Padding = new Padding(0); layout.GrowStyle = TableLayoutPanelGrowStyle.AddRows; - // Header is a TableLayoutPanel with two AutoSize columns. No percent - // column — those force GrowAndShrink to collapse and were the cause of - // the "unmounte" clipping bug. We just put the pill right next to the - // title with a small gap; visually that reads as a chip on the right. + // GUI-1 (v0.15.0): header is `[dot] title [drive-chip] [pill] [⋯]`. + // Status dot tells you at a glance whether the profile is + // mounted; drive chip surfaces the assigned letter without + // needing to scan to "Drive: P:\" below; overflow button + // hides 5 less-frequent actions (Test / Edit / Set primary / + // Auto-mount / Remove) behind a ⋯ menu so the action bar + // only shows the 3 high-frequency buttons. TableLayoutPanel header = new TableLayoutPanel(); header.AutoSize = true; header.AutoSizeMode = AutoSizeMode.GrowAndShrink; - header.ColumnCount = 2; + header.ColumnCount = 5; + header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); header.RowCount = 1; @@ -1018,11 +1024,23 @@ public ProfileCard(TrayContext owner, RemoteProfile p) header.Padding = new Padding(0); header.BackColor = CardBg; + statusDot = new StatusDot(); + statusDot.Margin = new Padding(0, 12, 8, 0); + titleLabel = new Label(); titleLabel.AutoSize = true; titleLabel.Font = new Font("Segoe UI", 11.5f, FontStyle.Bold); titleLabel.ForeColor = FgColor; - titleLabel.Margin = new Padding(0, 6, 16, 0); + titleLabel.Margin = new Padding(0, 6, 12, 0); + + driveChip = new Label(); + driveChip.AutoSize = true; + driveChip.Font = new Font("Segoe UI", 8.5f, FontStyle.Bold); + driveChip.ForeColor = WindowTheme.MutedColor; + driveChip.BackColor = WindowTheme.InputBg; + driveChip.Padding = new Padding(8, 3, 8, 3); + driveChip.TextAlign = ContentAlignment.MiddleCenter; + driveChip.Margin = new Padding(0, 9, 8, 0); statusPill = new Label(); statusPill.AutoSize = true; @@ -1032,8 +1050,26 @@ public ProfileCard(TrayContext owner, RemoteProfile p) statusPill.TextAlign = ContentAlignment.MiddleCenter; statusPill.Margin = new Padding(0, 8, 0, 0); - header.Controls.Add(titleLabel, 0, 0); - header.Controls.Add(statusPill, 1, 0); + overflowBtn = MainWindow.MakeAction("⋯", null); + overflowBtn.Margin = new Padding(8, 6, 0, 0); + overflowBtn.MinimumSize = new Size(32, 28); + overflowMenu = new ContextMenuStrip(); + TrayMenuTheme.Apply(overflowMenu); + overflowMenu.Items.Add("Test profile", null, delegate { owner.TestProfile(Profile); }); + overflowMenu.Items.Add("Edit profile...", null, delegate { owner.EditProfile(Profile); }); + overflowMenu.Items.Add("Set as primary", null, delegate { owner.MakePrimaryProfile(Profile); }); + overflowAutoMount = new ToolStripMenuItem("Auto-mount at Pixelpipe start"); + overflowAutoMount.Click += delegate { owner.ToggleProfileAutoMount(Profile); ApplyLiveState(); }; + overflowMenu.Items.Add(overflowAutoMount); + overflowMenu.Items.Add(new ToolStripSeparator()); + overflowMenu.Items.Add("Remove profile", null, delegate { owner.RemoveProfile(Profile); }); + overflowBtn.Click += delegate { overflowMenu.Show(overflowBtn, new Point(0, overflowBtn.Height)); }; + + header.Controls.Add(statusDot, 0, 0); + header.Controls.Add(titleLabel, 1, 0); + header.Controls.Add(driveChip, 2, 0); + header.Controls.Add(statusPill, 3, 0); + header.Controls.Add(overflowBtn, 4, 0); remoteLabel = MakeLine(); driveLabel = MakeLine(); @@ -1081,24 +1117,11 @@ public ProfileCard(TrayContext owner, RemoteProfile p) primary.Controls.Add(unmount); primary.Controls.Add(openDrive); - FlowLayoutPanel secondary = new FlowLayoutPanel(); - secondary.AutoSize = true; - secondary.AutoSizeMode = AutoSizeMode.GrowAndShrink; - secondary.FlowDirection = FlowDirection.LeftToRight; - secondary.WrapContents = false; - secondary.Margin = new Padding(0, 4, 0, 0); - secondary.BackColor = CardBg; - - testBtn = MainWindow.MakeAction("Test", delegate { owner.TestProfile(Profile); }); - editBtn = MainWindow.MakeAction("Edit", delegate { owner.EditProfile(Profile); }); - setPrimaryBtn = MainWindow.MakeAction("Set primary", delegate { owner.MakePrimaryProfile(Profile); }); - autoMountBtn = MainWindow.MakeAction("Auto-mount: off", delegate { owner.ToggleProfileAutoMount(Profile); }); - removeBtn = MainWindow.MakeAction("Remove", delegate { owner.RemoveProfile(Profile); }); - secondary.Controls.Add(testBtn); - secondary.Controls.Add(editBtn); - secondary.Controls.Add(setPrimaryBtn); - secondary.Controls.Add(autoMountBtn); - secondary.Controls.Add(removeBtn); + // GUI-1 (v0.15.0): secondary actions (Test / Edit / Set + // primary / Auto-mount / Remove) moved to the header's ⋯ + // overflow menu. Trims the visible button count from 9 to 4 + // (Mount / Full cache / Unmount / Open) without losing any + // affordance. AddRow(layout, header); AddRow(layout, remoteLabel); @@ -1113,7 +1136,6 @@ public ProfileCard(TrayContext owner, RemoteProfile p) AddRow(layout, watchLabel); AddRow(layout, errorLabel); AddRow(layout, primary); - AddRow(layout, secondary); Root.Controls.Add(layout); ApplyLiveState(); @@ -1135,6 +1157,9 @@ public void ApplyLiveState() titleLabel.Text = Profile.Label + " (" + TrayContext.DisplayProvider(Profile.Provider) + ")"; statusPill.Text = mounted ? "MOUNTED" : "unmounted"; statusPill.BackColor = mounted ? MountedPill : UnmountedPill; + if (statusDot != null) statusDot.State = mounted ? StatusDot.DotColor.Ok : StatusDot.DotColor.Unknown; + if (driveChip != null) driveChip.Text = (Profile.DriveLetter ?? "?:").ToUpperInvariant(); + if (overflowAutoMount != null) overflowAutoMount.Checked = Profile.AutoMount; remoteLabel.Text = "Remote: " + Profile.Remote; driveLabel.Text = "Drive: " + owner.GetDriveRoot(Profile); @@ -1184,9 +1209,10 @@ public void ApplyLiveState() openDrive.Enabled = mounted; openDrive.Text = mounted ? "Open " + owner.GetDriveRoot(Profile) : "Open"; - editBtn.Enabled = !mounted; - removeBtn.Enabled = !mounted; - autoMountBtn.Text = "Auto-mount: " + (Profile.AutoMount ? "on" : "off"); + // Overflow menu items don't expose .Enabled the same way as + // Button, but the actions themselves no-op or error when + // mounted (see EditProfile / RemoveProfile guards), so it's + // fine to leave them enabled in the menu. } private static Label MakeLine() diff --git a/src/Pixelpipe.Profiles.cs b/src/Pixelpipe.Profiles.cs index 2ee25d4..c5fce44 100644 --- a/src/Pixelpipe.Profiles.cs +++ b/src/Pixelpipe.Profiles.cs @@ -280,12 +280,17 @@ private void EditProfile(RemoteProfile p) title.ForeColor = WindowTheme.FgColor; title.Margin = new Padding(0, 0, 0, 14); - FlowLayoutPanel body = new FlowLayoutPanel(); - body.Dock = DockStyle.Fill; - body.FlowDirection = FlowDirection.TopDown; - body.WrapContents = false; - body.AutoScroll = true; - body.BackColor = form.BackColor; + // GUI-5 (v0.15.0): four-tab layout — General / Bandwidth / + // Schedule / Watch — instead of one tall scrolling form. + // The field controls below are built unchanged; only the + // wrapping container moves from a flowing column to a + // TabControl, and each existing GroupBox lands in its own + // TabPage at the bottom of this method. The dialog's Save + // path still references the same field locals so the read- + // back / round-trip logic is identical. + TabControl tabs = new TabControl(); + tabs.Dock = DockStyle.Fill; + tabs.Padding = new Point(form.LogicalToDeviceUnits(12), form.LogicalToDeviceUnits(6)); // Core fields ------------------------------------------------ TableLayoutPanel grid = new TableLayoutPanel(); @@ -547,10 +552,17 @@ private void EditProfile(RemoteProfile p) watchStack.Controls.Add(watchHelp); watchGroup.Controls.Add(watchStack); - body.Controls.Add(grid); - body.Controls.Add(bwGroup); - body.Controls.Add(schedGroup); - body.Controls.Add(watchGroup); + // Each tab gets one of the existing group panels. Inner + // GroupBox titles stay so the visual border still reads, + // but the tab title is what the user uses to navigate. + TabPage generalPage = new TabPage("General"); generalPage.BackColor = form.BackColor; generalPage.ForeColor = WindowTheme.FgColor; generalPage.Padding = new Padding(12); generalPage.Controls.Add(grid); + TabPage bwPage = new TabPage("Bandwidth"); bwPage.BackColor = form.BackColor; bwPage.ForeColor = WindowTheme.FgColor; bwPage.Padding = new Padding(12); bwPage.Controls.Add(bwGroup); + TabPage schedPage = new TabPage("Schedule"); schedPage.BackColor = form.BackColor; schedPage.ForeColor = WindowTheme.FgColor; schedPage.Padding = new Padding(12); schedPage.Controls.Add(schedGroup); + TabPage watchPage = new TabPage("Watch"); watchPage.BackColor = form.BackColor; watchPage.ForeColor = WindowTheme.FgColor; watchPage.Padding = new Padding(12); watchPage.Controls.Add(watchGroup); + tabs.TabPages.Add(generalPage); + tabs.TabPages.Add(bwPage); + tabs.TabPages.Add(schedPage); + tabs.TabPages.Add(watchPage); FlowLayoutPanel footer = new FlowLayoutPanel(); footer.Dock = DockStyle.Fill; @@ -565,7 +577,7 @@ private void EditProfile(RemoteProfile p) footer.Controls.Add(save); root.Controls.Add(title, 0, 0); - root.Controls.Add(body, 0, 1); + root.Controls.Add(tabs, 0, 1); root.Controls.Add(footer, 0, 2); form.Controls.Add(root); form.AcceptButton = save; form.CancelButton = cancel;