From b7928b296531635d70a333e935d06b255951bec2 Mon Sep 17 00:00:00 2001 From: DaxxSec Date: Wed, 13 May 2026 10:20:02 -0600 Subject: [PATCH] =?UTF-8?q?feat(ui):=20toolbar=20trim=20=E2=80=94=20collap?= =?UTF-8?q?se=20Configure/Clone/Rename=20into=20"More=20=E2=96=BE"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback after merging the redesign: "too many buttons" in the top toolbar. Counting per-pill: Start | New + Import | Configure + Clone + Rename | Delete — seven persistent toolbar buttons for what in practice fall into three usage tiers: - Primary daily: Start, New - Common: Import, Delete - Occasional: Configure, Clone, Rename This PR consolidates the third tier into a single "More ▾" overflow button that pops a popup menu with the three items. Visible button count drops 7 → 5; the three actions are still one click + one tap away, and their @IBAction selectors / keyboard shortcuts / menu-bar entries all keep working unchanged because the menu items target the exact same selectors the buttons did. New toolbar: [▶ Start] [+ New | ↧ Import] [More ▾] [🗑 Delete] Implementation: - `makeMoreActionsButton()` builds the overflow trigger. - `showMoreActionsMenu(_:)` pops the menu and propagates each wrapped button's `isEnabled` state to its menu item, so a disabled Configure stays disabled in the menu (same enabled rules `updateButtonStates` already maintains). - The Configure / Clone / Rename buttons are still `@IBOutlet` bound — only their pill placement changed. Removing them from any prior pill container before the new layout pass prevents ghost duplicates if the layout function fires twice. Co-Authored-By: Claude Opus 4.7 (1M context) --- SecVF/VMLibraryWindowController.swift | 68 ++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/SecVF/VMLibraryWindowController.swift b/SecVF/VMLibraryWindowController.swift index 2b4515c..c4cc472 100644 --- a/SecVF/VMLibraryWindowController.swift +++ b/SecVF/VMLibraryWindowController.swift @@ -645,6 +645,58 @@ class VMLibraryWindowController: NSWindowController, /// /// The pill's frame is sized to fit its buttons; the caller positions /// the pill's origin. + /// Build the toolbar's "More ▾" overflow button — collapses the + /// Configure / Clone / Rename trio that used to take three slots + /// in the toolbar into a single popup-menu trigger. Each menu item + /// fires the same @IBAction selector the original button did, so + /// keyboard shortcuts and any other selector-based dispatch paths + /// (menu bar, scriptability) keep working unchanged. + private func makeMoreActionsButton() -> NSButton { + let btn = NSButton(title: "More ▾", target: self, + action: #selector(showMoreActionsMenu(_:))) + btn.isBordered = false + btn.bezelStyle = .regularSquare + btn.font = NSFont.systemFont(ofSize: LayoutConstants.fontSizeBody, weight: .medium) + btn.setAccessibilityLabel("More VM actions") + btn.toolTip = "VM actions: Configure, Clone, Rename" + // Match the same width / styling the wrapped button-pill + // container expects (buttonW = 80 inside the pill, set there). + return btn + } + + @objc private func showMoreActionsMenu(_ sender: NSButton) { + let menu = NSMenu(title: "More VM actions") + + if let cfg = configureButton { + let item = NSMenuItem(title: "Configure…", + action: #selector(configureVM(_:)), + keyEquivalent: "") + item.target = self + item.isEnabled = cfg.isEnabled + menu.addItem(item) + } + if let clone = cloneButton { + let item = NSMenuItem(title: "Clone…", + action: #selector(cloneVM(_:)), + keyEquivalent: "") + item.target = self + item.isEnabled = clone.isEnabled + menu.addItem(item) + } + if let rename = renameButton { + let item = NSMenuItem(title: "Rename…", + action: #selector(renameVM(_:)), + keyEquivalent: "") + item.target = self + item.isEnabled = rename.isEnabled + menu.addItem(item) + } + + // Position the menu just below the button's bottom-left. + let origin = NSPoint(x: 0, y: sender.bounds.height + 2) + menu.popUp(positioning: nil, at: origin, in: sender) + } + private func makeButtonPillContainer(_ buttons: [NSButton], borderColor: NSColor = AppColors.borderOD, fillColor: NSColor = AppColors.backgroundButton.withAlphaComponent(0.55)) -> NSView { @@ -1251,16 +1303,30 @@ class VMLibraryWindowController: NSWindowController, toolbarPillContainers.forEach { $0.removeFromSuperview() } + // Configure / Clone / Rename collapsed into a single "More ▾" + // overflow button. They were three persistent toolbar buttons + // for actions taken occasionally; the menu version saves visible + // pill width without losing the action paths (each menu item + // calls the same @IBAction the original button did). + let moreButton = makeMoreActionsButton() + let pills: [NSView] = [ makeButtonPillContainer([startButton].compactMap { $0 }, borderColor: AppColors.accentODGlow.withAlphaComponent(0.7), fillColor: AppColors.accentOD.withAlphaComponent(0.18)), makeButtonPillContainer([newButton, importButton].compactMap { $0 }), - makeButtonPillContainer([configureButton, cloneButton, renameButton].compactMap { $0 }), + makeButtonPillContainer([moreButton]), makeButtonPillContainer([deleteButton].compactMap { $0 }, borderColor: AppColors.accentRed.withAlphaComponent(0.6)), ] + // The wrapped Configure / Clone / Rename buttons no longer + // appear in the toolbar but stay as @IBOutlet bindings so the + // menu can fire their @IBAction selectors. Pull them out of + // any prior pill container so they don't paint twice. + [configureButton, cloneButton, renameButton].compactMap { $0 } + .forEach { $0.removeFromSuperview() } + var px = tableX for pill in pills { // Each pill is `buttonHeight + 2` tall. Center it on toolbarY by