From b1fbbe5dfe7a0fb44129ef5a83fac8d14028a042 Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Tue, 6 May 2025 17:27:02 +0200 Subject: [PATCH 1/9] Added initial hide interface button - Hide interface and shortcut will store the interface state and hide it - Show interface will restore the interface state --- .../CodeEditWindowController.swift | 6 +++ .../WindowCommands/ViewCommands.swift | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 3d581f079..846632700 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -14,6 +14,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @Published var inspectorCollapsed = false @Published var toolbarCollapsed = false + // These variables store the state of the windows when using "Hide interface" + @Published var prevNavigatorCollapsed: Bool? + @Published var prevInspectorCollapsed: Bool? + @Published var prevUtilityAreaCollapsed: Bool? + @Published var prevToolbarCollapsed: Bool? + private var panelOpen = false var observers: [NSKeyValueObservation] = [] diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index fc6b8b72d..1caef2aa9 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -111,6 +111,44 @@ extension ViewCommands { windowController?.toolbarCollapsed ?? true } + var isAnythingVisible: Bool { + !navigatorCollapsed || !inspectorCollapsed || !utilityAreaCollapsed || !toolbarCollapsed + } + + func toggleInterface(shouldHide: Bool) { + // When hiding, store how the interface looks now + if shouldHide { + storeInterfaceVisibilityState() + } + + // Check what each elemtent state should be + let navigatorTargetState = shouldHide ? true : (windowController?.prevNavigatorCollapsed ?? false) + let inspectorTargetState = shouldHide ? true : (windowController?.prevInspectorCollapsed ?? false) + let utilityAreaTargetState = shouldHide ? true : (windowController?.prevUtilityAreaCollapsed ?? false) + let toolbarTargetState = shouldHide ? true : (windowController?.prevToolbarCollapsed ?? true) + + // Toggle only the parts that need to change + if navigatorCollapsed != navigatorTargetState { + windowController?.toggleFirstPanel() + } + if inspectorCollapsed != inspectorTargetState { + windowController?.toggleLastPanel() + } + if utilityAreaCollapsed != utilityAreaTargetState { + CommandManager.shared.executeCommand("open.drawer") + } + if toolbarCollapsed != toolbarTargetState { + windowController?.toggleToolbar() + } + } + + func storeInterfaceVisibilityState() { + windowController?.prevNavigatorCollapsed = navigatorCollapsed + windowController?.prevInspectorCollapsed = inspectorCollapsed + windowController?.prevUtilityAreaCollapsed = utilityAreaCollapsed + windowController?.prevToolbarCollapsed = toolbarCollapsed + } + var body: some View { Button("\(navigatorCollapsed ? "Show" : "Hide") Navigator") { windowController?.toggleFirstPanel() @@ -135,6 +173,12 @@ extension ViewCommands { } .disabled(windowController == nil) .keyboardShortcut("t", modifiers: [.option, .command]) + + Button("\(isAnythingVisible ? "Hide" : "Show") Interface") { + toggleInterface(shouldHide: isAnythingVisible) + } + .disabled(windowController == nil) + .keyboardShortcut("i", modifiers: [.option, .command]) } } } From 52df1856b577fcb2a2416658850938fbc41bf3f2 Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Wed, 7 May 2025 11:17:26 +0200 Subject: [PATCH 2/9] Hiding interface now remembers better - When hiding interface it will better be able to handle hidden elements being enabled, and then showing the interface again. - Moved some logic from ViewCommands to CodeEditWindowController, as it seems like a more appropriate place. - Added a bool shouldAnimate to function toggleFirstPanel. This is by default on, and thus using the default animation. When explicitly called with off, the function will not animate. --- .../CodeEditWindowController.swift | 97 ++++++++++++++++++- .../CodeEditWindowControllerExtensions.swift | 15 ++- .../WindowCommands/ViewCommands.swift | 45 ++------- 3 files changed, 114 insertions(+), 43 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 846632700..3b9be23f6 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -10,9 +10,10 @@ import SwiftUI import Combine final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject, NSWindowDelegate { - @Published var navigatorCollapsed = false - @Published var inspectorCollapsed = false - @Published var toolbarCollapsed = false + @Published var navigatorCollapsed: Bool = false + @Published var inspectorCollapsed: Bool = false + @Published var toolbarCollapsed: Bool = false + @Published var interfaceHidden: Bool = false // These variables store the state of the windows when using "Hide interface" @Published var prevNavigatorCollapsed: Bool? @@ -209,4 +210,94 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs workspace = nil return true } + + /// Function for toggling the interface elements on or off + /// + /// - Parameter shouldHide: Pass `true` to hide all interface panels (and remember their current states), + /// or `false` to restore them to how they were before hiding. + func toggleInterface(shouldHide: Bool) { + interfaceHidden = shouldHide + + // When hiding, store how the interface looks now + if shouldHide { + storeInterfaceCollapseState() + } + + // Determine the desired collapsed/visible state for every interface element + let navigatorTargetState = determineDesiredCollapseState( + shouldHide: shouldHide, + currentlyCollapsed: navigatorCollapsed, + previouslyCollapsed: prevNavigatorCollapsed, + ) + let inspectorTargetState = determineDesiredCollapseState( + shouldHide: shouldHide, + currentlyCollapsed: inspectorCollapsed, + previouslyCollapsed: prevInspectorCollapsed, + ) + let utilityAreaTargetState = determineDesiredCollapseState( + shouldHide: shouldHide, + currentlyCollapsed: workspace?.utilityAreaModel?.isCollapsed ?? true, + previouslyCollapsed: prevUtilityAreaCollapsed, + ) + let toolbarTargetState = determineDesiredCollapseState( + shouldHide: shouldHide, + currentlyCollapsed: toolbarCollapsed, + previouslyCollapsed: prevToolbarCollapsed, + ) + + // Toggle only the parts that need to change + if navigatorCollapsed != navigatorTargetState { + toggleFirstPanel(shouldAnimate: false) + } + if inspectorCollapsed != inspectorTargetState { + toggleLastPanel() + } + if workspace?.utilityAreaModel?.isCollapsed != utilityAreaTargetState { + CommandManager.shared.executeCommand("open.drawer") + } + if toolbarCollapsed != toolbarTargetState { + toggleToolbar() + } + + // If enabling interface, reset the visibility states + if !shouldHide { + resetStoredInterfaceCollapseState() + } + } + + /// Calculates the collapse state an interface element should have after a hide / show toggle. + /// - Parameters: + /// - shouldHide: `true` when we’re hiding the whole interface. + /// - currentlyCollapsed: The panels current state + /// - previouslyCollapsed: The state we saved the last time we hid the UI, if any. + /// - Returns: `true` for visible element, `false` for collapsed element + func determineDesiredCollapseState(shouldHide: Bool, currentlyCollapsed: Bool, previouslyCollapsed: Bool?) -> Bool { + // If ShouldHide, everything should close + if shouldHide { return true } + + // If currently visible and !shouldHide, it should not collapse + if !currentlyCollapsed && !shouldHide { return false } + + // If we have a previous state, return that one. + if let remembered = previouslyCollapsed { return remembered } + + // If there is no stored state, return the current state. + return currentlyCollapsed + } + + /// Function for storing the current interface visibility states + func storeInterfaceCollapseState() { + prevNavigatorCollapsed = navigatorCollapsed + prevInspectorCollapsed = inspectorCollapsed + prevUtilityAreaCollapsed = workspace?.utilityAreaModel?.isCollapsed + prevToolbarCollapsed = toolbarCollapsed + } + + /// Function for resetting the stored interface visibility states + func resetStoredInterfaceCollapseState() { + prevNavigatorCollapsed = nil + prevInspectorCollapsed = nil + prevUtilityAreaCollapsed = nil + prevToolbarCollapsed = nil + } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 88e7dbc9b..f5865e176 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -11,8 +11,21 @@ import Combine extension CodeEditWindowController { @objc func toggleFirstPanel() { + toggleFirstPanel(shouldAnimate: true) + } + + /// Toggles the navigator pane, optionally without animation. + func toggleFirstPanel(shouldAnimate: Bool = true) { guard let firstSplitView = splitViewController?.splitViewItems.first else { return } - firstSplitView.animator().isCollapsed.toggle() + + if shouldAnimate { + // Standard animated toggle + firstSplitView.animator().isCollapsed.toggle() + } else { + // Instant toggle (no animation) + firstSplitView.isCollapsed.toggle() + } + splitViewController?.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) } diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 1caef2aa9..4e37d8357 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -111,42 +111,9 @@ extension ViewCommands { windowController?.toolbarCollapsed ?? true } - var isAnythingVisible: Bool { - !navigatorCollapsed || !inspectorCollapsed || !utilityAreaCollapsed || !toolbarCollapsed - } - - func toggleInterface(shouldHide: Bool) { - // When hiding, store how the interface looks now - if shouldHide { - storeInterfaceVisibilityState() - } - - // Check what each elemtent state should be - let navigatorTargetState = shouldHide ? true : (windowController?.prevNavigatorCollapsed ?? false) - let inspectorTargetState = shouldHide ? true : (windowController?.prevInspectorCollapsed ?? false) - let utilityAreaTargetState = shouldHide ? true : (windowController?.prevUtilityAreaCollapsed ?? false) - let toolbarTargetState = shouldHide ? true : (windowController?.prevToolbarCollapsed ?? true) - - // Toggle only the parts that need to change - if navigatorCollapsed != navigatorTargetState { - windowController?.toggleFirstPanel() - } - if inspectorCollapsed != inspectorTargetState { - windowController?.toggleLastPanel() - } - if utilityAreaCollapsed != utilityAreaTargetState { - CommandManager.shared.executeCommand("open.drawer") - } - if toolbarCollapsed != toolbarTargetState { - windowController?.toggleToolbar() - } - } - - func storeInterfaceVisibilityState() { - windowController?.prevNavigatorCollapsed = navigatorCollapsed - windowController?.prevInspectorCollapsed = inspectorCollapsed - windowController?.prevUtilityAreaCollapsed = utilityAreaCollapsed - windowController?.prevToolbarCollapsed = toolbarCollapsed + var isInterfaceHidden: Bool { + return windowController?.interfaceHidden ?? false + //navigatorCollapsed && inspectorCollapsed && utilityAreaCollapsed && toolbarCollapsed } var body: some View { @@ -174,11 +141,11 @@ extension ViewCommands { .disabled(windowController == nil) .keyboardShortcut("t", modifiers: [.option, .command]) - Button("\(isAnythingVisible ? "Hide" : "Show") Interface") { - toggleInterface(shouldHide: isAnythingVisible) + Button("\(isInterfaceHidden ? "Show" : "Hide") Interface") { + windowController?.toggleInterface(shouldHide: !isInterfaceHidden) } .disabled(windowController == nil) - .keyboardShortcut("i", modifiers: [.option, .command]) + .keyboardShortcut(".", modifiers: .command) } } } From 775ec998ebf4c69de95508278b987481687d5ddd Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Thu, 8 May 2025 22:47:17 +0200 Subject: [PATCH 3/9] interfaceHidden will now update itself on changes - Removed need for interfaceHidden bool - Removed need for resetting stored interface state - Added function isInterfaceStillHidden, checking whether "at least" the previous elements are visible again, taking other elements into account - Hidden animation for lastpanel --- .../CodeEditWindowController.swift | 40 ++++++++++--------- .../CodeEditWindowControllerExtensions.swift | 14 ++++++- .../WindowCommands/ViewCommands.swift | 3 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 3b9be23f6..cc18c881c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -13,7 +13,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs @Published var navigatorCollapsed: Bool = false @Published var inspectorCollapsed: Bool = false @Published var toolbarCollapsed: Bool = false - @Published var interfaceHidden: Bool = false // These variables store the state of the windows when using "Hide interface" @Published var prevNavigatorCollapsed: Bool? @@ -211,13 +210,31 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs return true } + var utilityAreaCollapsed: Bool { + workspace?.utilityAreaModel?.isCollapsed ?? true + } + + /// Returns `true` if at least one panel that was visible is still collapsed, meaning the interface is still hidden + func isInterfaceStillHidden() -> Bool { + // If the interface is already un-hidden, we can short-circuit. + guard let prevNav = prevNavigatorCollapsed, + let prevInsp = prevInspectorCollapsed, + let prevUtil = prevUtilityAreaCollapsed, + let prevTool = prevToolbarCollapsed + else { return false } + + // True when any panel that was previously visible is collapsed + return (!prevNav && navigatorCollapsed) || + (!prevInsp && inspectorCollapsed) || + (!prevUtil && utilityAreaCollapsed) || + (!prevTool && toolbarCollapsed) + } + /// Function for toggling the interface elements on or off /// /// - Parameter shouldHide: Pass `true` to hide all interface panels (and remember their current states), /// or `false` to restore them to how they were before hiding. func toggleInterface(shouldHide: Bool) { - interfaceHidden = shouldHide - // When hiding, store how the interface looks now if shouldHide { storeInterfaceCollapseState() @@ -236,7 +253,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs ) let utilityAreaTargetState = determineDesiredCollapseState( shouldHide: shouldHide, - currentlyCollapsed: workspace?.utilityAreaModel?.isCollapsed ?? true, + currentlyCollapsed: utilityAreaCollapsed, previouslyCollapsed: prevUtilityAreaCollapsed, ) let toolbarTargetState = determineDesiredCollapseState( @@ -250,7 +267,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs toggleFirstPanel(shouldAnimate: false) } if inspectorCollapsed != inspectorTargetState { - toggleLastPanel() + toggleLastPanel(shouldAnimate: false) } if workspace?.utilityAreaModel?.isCollapsed != utilityAreaTargetState { CommandManager.shared.executeCommand("open.drawer") @@ -258,11 +275,6 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs if toolbarCollapsed != toolbarTargetState { toggleToolbar() } - - // If enabling interface, reset the visibility states - if !shouldHide { - resetStoredInterfaceCollapseState() - } } /// Calculates the collapse state an interface element should have after a hide / show toggle. @@ -292,12 +304,4 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs prevUtilityAreaCollapsed = workspace?.utilityAreaModel?.isCollapsed prevToolbarCollapsed = toolbarCollapsed } - - /// Function for resetting the stored interface visibility states - func resetStoredInterfaceCollapseState() { - prevNavigatorCollapsed = nil - prevInspectorCollapsed = nil - prevUtilityAreaCollapsed = nil - prevToolbarCollapsed = nil - } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index f5865e176..1877e3973 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -31,12 +31,22 @@ extension CodeEditWindowController { @objc func toggleLastPanel() { + toggleLastPanel(shouldAnimate: true) + } + + func toggleLastPanel(shouldAnimate: Bool = true) { guard let lastSplitView = splitViewController?.splitViewItems.last else { return } - NSAnimationContext.runAnimationGroup { _ in - lastSplitView.animator().isCollapsed.toggle() + if shouldAnimate { + // Standard animated toggle + NSAnimationContext.runAnimationGroup { _ in + lastSplitView.animator().isCollapsed.toggle() + } + } else { + // Instant toggle (no animation) + lastSplitView.isCollapsed.toggle() } splitViewController?.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) diff --git a/CodeEdit/Features/WindowCommands/ViewCommands.swift b/CodeEdit/Features/WindowCommands/ViewCommands.swift index 4e37d8357..69854c9d5 100644 --- a/CodeEdit/Features/WindowCommands/ViewCommands.swift +++ b/CodeEdit/Features/WindowCommands/ViewCommands.swift @@ -112,8 +112,7 @@ extension ViewCommands { } var isInterfaceHidden: Bool { - return windowController?.interfaceHidden ?? false - //navigatorCollapsed && inspectorCollapsed && utilityAreaCollapsed && toolbarCollapsed + return windowController?.isInterfaceStillHidden() ?? false } var body: some View { From 3f5cd70f0f591d9f7185374ee79895352c8a204b Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Fri, 9 May 2025 18:10:57 +0200 Subject: [PATCH 4/9] Updated logic to when no panels are active - If no panels are active, and interface hasn't been hidden, all panels will show when toggle interface is clicked - Renamed objc functions with prefix 'objc' - Re-added resetStoredInterfaceCollapseState - turns out it was necessary --- .../CodeEditWindowController+Toolbar.swift | 4 +-- .../CodeEditWindowController.swift | 29 +++++++++++++++---- .../CodeEditWindowControllerExtensions.swift | 4 +-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index e7d5f0282..738c2c6b3 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -92,7 +92,7 @@ extension CodeEditWindowController { toolbarItem.toolTip = "Hide or show the Navigator" toolbarItem.isBordered = true toolbarItem.target = self - toolbarItem.action = #selector(self.toggleFirstPanel) + toolbarItem.action = #selector(self.objcToggleFirstPanel) toolbarItem.image = NSImage( systemSymbolName: "sidebar.leading", accessibilityDescription: nil @@ -106,7 +106,7 @@ extension CodeEditWindowController { toolbarItem.toolTip = "Hide or show the Inspectors" toolbarItem.isBordered = true toolbarItem.target = self - toolbarItem.action = #selector(self.toggleLastPanel) + toolbarItem.action = #selector(self.objcToggleLastPanel) toolbarItem.image = NSImage( systemSymbolName: "sidebar.trailing", accessibilityDescription: nil diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index cc18c881c..1490696ab 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -221,13 +221,19 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs let prevInsp = prevInspectorCollapsed, let prevUtil = prevUtilityAreaCollapsed, let prevTool = prevToolbarCollapsed - else { return false } + else { + return navigatorCollapsed && navigatorCollapsed && toolbarCollapsed && utilityAreaCollapsed + } + + let stillHidden = (!prevNav && navigatorCollapsed) || + (!prevInsp && inspectorCollapsed) || + (!prevUtil && utilityAreaCollapsed) || + (!prevTool && toolbarCollapsed) + + if !stillHidden { resetStoredInterfaceCollapseState() } // True when any panel that was previously visible is collapsed - return (!prevNav && navigatorCollapsed) || - (!prevInsp && inspectorCollapsed) || - (!prevUtil && utilityAreaCollapsed) || - (!prevTool && toolbarCollapsed) + return stillHidden } /// Function for toggling the interface elements on or off @@ -287,7 +293,10 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs // If ShouldHide, everything should close if shouldHide { return true } - // If currently visible and !shouldHide, it should not collapse + // If currently collapsed, and there is no previous state, show it. + if previouslyCollapsed == nil && currentlyCollapsed { return false } + + // If currently visible and !shouldHide, it should not collapse. if !currentlyCollapsed && !shouldHide { return false } // If we have a previous state, return that one. @@ -304,4 +313,12 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs prevUtilityAreaCollapsed = workspace?.utilityAreaModel?.isCollapsed prevToolbarCollapsed = toolbarCollapsed } + + /// Function for resetting the stored interface visibility states + func resetStoredInterfaceCollapseState() { + prevNavigatorCollapsed = nil + prevInspectorCollapsed = nil + prevUtilityAreaCollapsed = nil + prevToolbarCollapsed = nil + } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 1877e3973..2a53cb9b3 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -10,7 +10,7 @@ import Combine extension CodeEditWindowController { @objc - func toggleFirstPanel() { + func objcToggleFirstPanel() { toggleFirstPanel(shouldAnimate: true) } @@ -30,7 +30,7 @@ extension CodeEditWindowController { } @objc - func toggleLastPanel() { + func objcToggleLastPanel() { toggleLastPanel(shouldAnimate: true) } From 0ac2baed915cdcf51e0f93aeeee06aee18a6b7b8 Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Sat, 10 May 2025 22:24:48 +0200 Subject: [PATCH 5/9] Disabled animation for utility area when hiding interface - Added command "open.drawer.no.animation" to command manager. This will toggle the utility area, without an animation. - Added option possibility of no animation to SplitViewItem.Update - Added struct SplitViewItemCanAnimateViewTraitKey and function splitViewCanAnimate to SplitViewModifiers. These optionally allow disabling animations for SplitViews. - Updated "Hide Interface", WorkspaceView and UtilityAreaViewModel to accommodate these changes --- .../Controllers/CodeEditWindowController.swift | 2 +- .../Features/SplitView/Model/SplitViewItem.swift | 8 +++++++- .../SplitView/Views/SplitViewModifiers.swift | 8 ++++++++ .../StatusBarToggleUtilityAreaButton.swift | 12 ++++++++++++ .../ViewModels/UtilityAreaViewModel.swift | 6 +++++- CodeEdit/WorkspaceView.swift | 1 + 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 1490696ab..8f1d4e091 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -276,7 +276,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs toggleLastPanel(shouldAnimate: false) } if workspace?.utilityAreaModel?.isCollapsed != utilityAreaTargetState { - CommandManager.shared.executeCommand("open.drawer") + CommandManager.shared.executeCommand("open.drawer.no.animation") } if toolbarCollapsed != toolbarTargetState { toggleToolbar() diff --git a/CodeEdit/Features/SplitView/Model/SplitViewItem.swift b/CodeEdit/Features/SplitView/Model/SplitViewItem.swift index 4229bf5e4..9f8521e80 100644 --- a/CodeEdit/Features/SplitView/Model/SplitViewItem.swift +++ b/CodeEdit/Features/SplitView/Model/SplitViewItem.swift @@ -45,9 +45,15 @@ class SplitViewItem: ObservableObject { /// - Parameter child: the view corresponding to the SplitViewItem. func update(child: _VariadicView.Children.Element) { self.item.canCollapse = child[SplitViewItemCanCollapseViewTraitKey.self] + let canAnimate = child[SplitViewItemCanAnimateViewTraitKey.self] DispatchQueue.main.async { self.observers = [] - self.item.animator().isCollapsed = child[SplitViewItemCollapsedViewTraitKey.self].wrappedValue + let collapsed = child[SplitViewItemCollapsedViewTraitKey.self].wrappedValue + if canAnimate { + self.item.animator().isCollapsed = collapsed + } else { + self.item.isCollapsed = collapsed + } self.item.holdingPriority = child[SplitViewHoldingPriorityTraitKey.self] self.observers = self.createObservers() } diff --git a/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift b/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift index 3df0c7828..95f4e01bb 100644 --- a/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift +++ b/CodeEdit/Features/SplitView/Views/SplitViewModifiers.swift @@ -23,6 +23,10 @@ struct SplitViewHoldingPriorityTraitKey: _ViewTraitKey { static var defaultValue: NSLayoutConstraint.Priority = .defaultLow } +struct SplitViewItemCanAnimateViewTraitKey: _ViewTraitKey { + static var defaultValue: Bool { true } +} + extension View { func collapsed(_ value: Binding) -> some View { self @@ -43,4 +47,8 @@ extension View { self ._trait(SplitViewHoldingPriorityTraitKey.self, priority) } + + func splitViewCanAnimate(_ enabled: Binding) -> some View { + self._trait(SplitViewItemCanAnimateViewTraitKey.self, enabled.wrappedValue) + } } diff --git a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift index 1e5c2d15d..6fbbb6810 100644 --- a/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift +++ b/CodeEdit/Features/StatusBar/Views/StatusBarItems/StatusBarToggleUtilityAreaButton.swift @@ -31,6 +31,12 @@ internal struct StatusBarToggleUtilityAreaButton: View { id: "open.drawer", command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel() } ) + CommandManager.shared.addCommand( + name: "Toggle Utility Area Without Animation", + title: "Toggle Utility Area Without Animation", + id: "open.drawer.no.animation", + command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel(animation: false) } + ) } } .onAppear { @@ -40,6 +46,12 @@ internal struct StatusBarToggleUtilityAreaButton: View { id: "open.drawer", command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel() } ) + CommandManager.shared.addCommand( + name: "Toggle Utility Area Without Animation", + title: "Toggle Utility Area Without Animation", + id: "open.drawer.no.animation", + command: { [weak utilityAreaViewModel] in utilityAreaViewModel?.togglePanel(animation: false) } + ) } } } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index cd6fdf2b3..0cc075464 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -21,6 +21,9 @@ class UtilityAreaViewModel: ObservableObject { /// Indicates whether debugger is collapse or not @Published var isCollapsed: Bool = false + /// Indicates whether collapse animation should be enabled when utility area is toggled + @Published var animateCollapse: Bool = true + /// Returns true when the drawer is visible @Published var isMaximized: Bool = false @@ -47,7 +50,8 @@ class UtilityAreaViewModel: ObservableObject { workspace.addToWorkspaceState(key: .utilityAreaMaximized, value: isMaximized) } - func togglePanel() { + func togglePanel(animation: Bool = true) { + self.animateCollapse = animation self.isMaximized = false self.isCollapsed.toggle() } diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 69b957bcd..d9e2aa1b0 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -68,6 +68,7 @@ struct WorkspaceView: View { Rectangle() .collapsable() .collapsed($utilityAreaViewModel.isCollapsed) + .splitViewCanAnimate($utilityAreaViewModel.animateCollapse) .opacity(0) .frame(idealHeight: 260) .frame(minHeight: 100) From a3dadc7c622f57dc1c9a6825b1087719705a6b5e Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Tue, 13 May 2025 22:24:51 +0200 Subject: [PATCH 6/9] Rewrote and moved hide interface logic - The logic for hide interface has been moved to a new file, CodeEditWindowsController+Panels. - The function for toggling first panel and last panel has also been moved to said file. - The logic for hide interface is now much more simplified, dynamic and easier to maintain. --- .../CodeEditWindowController+Panels.swift | 177 ++++++++++++++++++ .../CodeEditWindowController.swift | 112 ----------- .../CodeEditWindowControllerExtensions.swift | 43 ----- 3 files changed, 177 insertions(+), 155 deletions(-) create mode 100644 CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift new file mode 100644 index 000000000..1549650b1 --- /dev/null +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift @@ -0,0 +1,177 @@ +// +// CodeEditWindowController+Panels.swift +// CodeEdit +// +// Created by Simon Kudsk on 11/05/2025. +// + +import SwiftUI + +extension CodeEditWindowController { + @objc + func objcToggleFirstPanel() { + toggleFirstPanel(shouldAnimate: true) + } + + /// Toggles the navigator pane, optionally without animation. + func toggleFirstPanel(shouldAnimate: Bool = true) { + guard let firstSplitView = splitViewController?.splitViewItems.first else { return } + + if shouldAnimate { + // Standard animated toggle + firstSplitView.animator().isCollapsed.toggle() + } else { + // Instant toggle (no animation) + firstSplitView.isCollapsed.toggle() + } + + splitViewController?.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) + } + + @objc + func objcToggleLastPanel() { + toggleLastPanel(shouldAnimate: true) + } + + func toggleLastPanel(shouldAnimate: Bool = true) { + guard let lastSplitView = splitViewController?.splitViewItems.last else { + return + } + + if shouldAnimate { + // Standard animated toggle + NSAnimationContext.runAnimationGroup { _ in + lastSplitView.animator().isCollapsed.toggle() + } + } else { + // Instant toggle (no animation) + lastSplitView.isCollapsed.toggle() + } + + splitViewController?.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + } + + // PanelDescriptor, used for an array of panels, for use with "Hide interface". + private struct PanelDescriptor { + /// Returns the current `isCollapsed` value for the panel. + let isCollapsed: () -> Bool + /// Returns the last stored previous state (or `nil` if none). + let getPrevCollapsed: () -> Bool? + /// Stores a new previous state (`nil` to clear). + let setPrevCollapsed: (Bool?) -> Void + /// Performs the actual toggle action for the panel. + let toggle: () -> Void + } + + // The panels which "Hide interface" should interact with. + private var panels: [PanelDescriptor] { + [ + PanelDescriptor( + isCollapsed: { self.navigatorCollapsed }, + getPrevCollapsed: { self.prevNavigatorCollapsed }, + setPrevCollapsed: { self.prevNavigatorCollapsed = $0 }, + toggle: { self.toggleFirstPanel(shouldAnimate: false) } + ), + PanelDescriptor( + isCollapsed: { self.inspectorCollapsed }, + getPrevCollapsed: { self.prevInspectorCollapsed }, + setPrevCollapsed: { self.prevInspectorCollapsed = $0 }, + toggle: { self.toggleLastPanel(shouldAnimate: false) } + ), + PanelDescriptor( + isCollapsed: { self.workspace?.utilityAreaModel?.isCollapsed ?? true }, + getPrevCollapsed: { self.prevUtilityAreaCollapsed }, + setPrevCollapsed: { self.prevUtilityAreaCollapsed = $0 }, + toggle: { CommandManager.shared.executeCommand("open.drawer.no.animation") } + ), + PanelDescriptor( + isCollapsed: { self.toolbarCollapsed }, + getPrevCollapsed: { self.prevToolbarCollapsed }, + setPrevCollapsed: { self.prevToolbarCollapsed = $0 }, + toggle: { self.toggleToolbar() } + ) + ] + } + + /// Returns `true` if at least one panel that was visible is still collapsed, meaning the interface is still hidden + func isInterfaceStillHidden() -> Bool { + // Some panels do not yet have a remembered state + if panels.contains(where: { $0.getPrevCollapsed() == nil }) { + // Hidden only if all panels are collapsed + return panels.allSatisfy { $0.isCollapsed() } + } + + // All panels have a remembered state. Check if any that were visible are still collapsed + let stillHidden = panels.contains { descriptor in + guard let prev = descriptor.getPrevCollapsed() else { return false } + return !prev && descriptor.isCollapsed() + } + + // If the interface has been restored, reset the remembered states + if !stillHidden { + resetStoredInterfaceCollapseState() + } + + return stillHidden + } + + /// Function for toggling the interface elements on or off + /// + /// - Parameter shouldHide: Pass `true` to hide all interface panels (and remember their current states), + /// or `false` to restore them to how they were before hiding. + func toggleInterface(shouldHide: Bool) { + // Store the current layout before hiding + if shouldHide { + storeInterfaceCollapseState() + } + + // Iterate over all panels and update their state as needed + for panel in panels { + let targetState = determineDesiredCollapseState( + shouldHide: shouldHide, + currentlyCollapsed: panel.isCollapsed(), + previouslyCollapsed: panel.getPrevCollapsed(), + ) + if panel.isCollapsed() != targetState { + panel.toggle() + } + } + } + + /// Calculates the collapse state an interface element should have after a hide / show toggle. + /// - Parameters: + /// - shouldHide: `true` when we’re hiding the whole interface. + /// - currentlyCollapsed: The panels current state + /// - previouslyCollapsed: The state we saved the last time we hid the UI, if any. + /// - Returns: `true` for visible element, `false` for collapsed element + func determineDesiredCollapseState(shouldHide: Bool, currentlyCollapsed: Bool, previouslyCollapsed: Bool?) -> Bool { + // If ShouldHide, everything should close + if shouldHide { + return true + } + + // If not hiding, and not currently collapsed, the panel should remain as such. + if !currentlyCollapsed { + return false + } + + // If the panel is currently collapsed and we are "showing" or "restoring": + // Option 1: Restore to its previously remembered state if available. + // Option 2: If no previously remembered state, default to making it visible (not collapsed). + return previouslyCollapsed ?? false + } + + /// Function for storing the current interface visibility states + func storeInterfaceCollapseState() { + for panel in panels { + panel.setPrevCollapsed(panel.isCollapsed()) + } + } + + /// Function for resetting the stored interface visibility states + func resetStoredInterfaceCollapseState() { + for panel in panels { + panel.setPrevCollapsed(nil) + } + } +} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 8f1d4e091..18634b950 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -209,116 +209,4 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs workspace = nil return true } - - var utilityAreaCollapsed: Bool { - workspace?.utilityAreaModel?.isCollapsed ?? true - } - - /// Returns `true` if at least one panel that was visible is still collapsed, meaning the interface is still hidden - func isInterfaceStillHidden() -> Bool { - // If the interface is already un-hidden, we can short-circuit. - guard let prevNav = prevNavigatorCollapsed, - let prevInsp = prevInspectorCollapsed, - let prevUtil = prevUtilityAreaCollapsed, - let prevTool = prevToolbarCollapsed - else { - return navigatorCollapsed && navigatorCollapsed && toolbarCollapsed && utilityAreaCollapsed - } - - let stillHidden = (!prevNav && navigatorCollapsed) || - (!prevInsp && inspectorCollapsed) || - (!prevUtil && utilityAreaCollapsed) || - (!prevTool && toolbarCollapsed) - - if !stillHidden { resetStoredInterfaceCollapseState() } - - // True when any panel that was previously visible is collapsed - return stillHidden - } - - /// Function for toggling the interface elements on or off - /// - /// - Parameter shouldHide: Pass `true` to hide all interface panels (and remember their current states), - /// or `false` to restore them to how they were before hiding. - func toggleInterface(shouldHide: Bool) { - // When hiding, store how the interface looks now - if shouldHide { - storeInterfaceCollapseState() - } - - // Determine the desired collapsed/visible state for every interface element - let navigatorTargetState = determineDesiredCollapseState( - shouldHide: shouldHide, - currentlyCollapsed: navigatorCollapsed, - previouslyCollapsed: prevNavigatorCollapsed, - ) - let inspectorTargetState = determineDesiredCollapseState( - shouldHide: shouldHide, - currentlyCollapsed: inspectorCollapsed, - previouslyCollapsed: prevInspectorCollapsed, - ) - let utilityAreaTargetState = determineDesiredCollapseState( - shouldHide: shouldHide, - currentlyCollapsed: utilityAreaCollapsed, - previouslyCollapsed: prevUtilityAreaCollapsed, - ) - let toolbarTargetState = determineDesiredCollapseState( - shouldHide: shouldHide, - currentlyCollapsed: toolbarCollapsed, - previouslyCollapsed: prevToolbarCollapsed, - ) - - // Toggle only the parts that need to change - if navigatorCollapsed != navigatorTargetState { - toggleFirstPanel(shouldAnimate: false) - } - if inspectorCollapsed != inspectorTargetState { - toggleLastPanel(shouldAnimate: false) - } - if workspace?.utilityAreaModel?.isCollapsed != utilityAreaTargetState { - CommandManager.shared.executeCommand("open.drawer.no.animation") - } - if toolbarCollapsed != toolbarTargetState { - toggleToolbar() - } - } - - /// Calculates the collapse state an interface element should have after a hide / show toggle. - /// - Parameters: - /// - shouldHide: `true` when we’re hiding the whole interface. - /// - currentlyCollapsed: The panels current state - /// - previouslyCollapsed: The state we saved the last time we hid the UI, if any. - /// - Returns: `true` for visible element, `false` for collapsed element - func determineDesiredCollapseState(shouldHide: Bool, currentlyCollapsed: Bool, previouslyCollapsed: Bool?) -> Bool { - // If ShouldHide, everything should close - if shouldHide { return true } - - // If currently collapsed, and there is no previous state, show it. - if previouslyCollapsed == nil && currentlyCollapsed { return false } - - // If currently visible and !shouldHide, it should not collapse. - if !currentlyCollapsed && !shouldHide { return false } - - // If we have a previous state, return that one. - if let remembered = previouslyCollapsed { return remembered } - - // If there is no stored state, return the current state. - return currentlyCollapsed - } - - /// Function for storing the current interface visibility states - func storeInterfaceCollapseState() { - prevNavigatorCollapsed = navigatorCollapsed - prevInspectorCollapsed = inspectorCollapsed - prevUtilityAreaCollapsed = workspace?.utilityAreaModel?.isCollapsed - prevToolbarCollapsed = toolbarCollapsed - } - - /// Function for resetting the stored interface visibility states - func resetStoredInterfaceCollapseState() { - prevNavigatorCollapsed = nil - prevInspectorCollapsed = nil - prevUtilityAreaCollapsed = nil - prevToolbarCollapsed = nil - } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 2a53cb9b3..baade6dfd 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -9,49 +9,6 @@ import SwiftUI import Combine extension CodeEditWindowController { - @objc - func objcToggleFirstPanel() { - toggleFirstPanel(shouldAnimate: true) - } - - /// Toggles the navigator pane, optionally without animation. - func toggleFirstPanel(shouldAnimate: Bool = true) { - guard let firstSplitView = splitViewController?.splitViewItems.first else { return } - - if shouldAnimate { - // Standard animated toggle - firstSplitView.animator().isCollapsed.toggle() - } else { - // Instant toggle (no animation) - firstSplitView.isCollapsed.toggle() - } - - splitViewController?.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) - } - - @objc - func objcToggleLastPanel() { - toggleLastPanel(shouldAnimate: true) - } - - func toggleLastPanel(shouldAnimate: Bool = true) { - guard let lastSplitView = splitViewController?.splitViewItems.last else { - return - } - - if shouldAnimate { - // Standard animated toggle - NSAnimationContext.runAnimationGroup { _ in - lastSplitView.animator().isCollapsed.toggle() - } - } else { - // Instant toggle (no animation) - lastSplitView.isCollapsed.toggle() - } - - splitViewController?.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) - } - /// These are example items that added as commands to command palette func registerCommands() { CommandManager.shared.addCommand( From 2ba29d91d3b290fcbc4b22af638d7b91a92218bd Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Tue, 13 May 2025 22:53:02 +0200 Subject: [PATCH 7/9] Asynchronous reset of stored state - In isInterfaceStillHidden(), resetStoredInterfaceCollapseState() will now be called asynchronously to avoid a SwiftUI update warning --- .../Controllers/CodeEditWindowController+Panels.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift index 1549650b1..2ed5f3791 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift @@ -109,7 +109,9 @@ extension CodeEditWindowController { // If the interface has been restored, reset the remembered states if !stillHidden { - resetStoredInterfaceCollapseState() + DispatchQueue.main.async { [weak self] in + self?.resetStoredInterfaceCollapseState() + } } return stillHidden From ecfdf701322131a445d89c8b6af7954ff841e646 Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Wed, 14 May 2025 09:44:46 +0200 Subject: [PATCH 8/9] Removed comma - Removed a comma that was causing the "Testing CodeEdit" task to fail --- .../Documents/Controllers/CodeEditWindowController+Panels.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift index 2ed5f3791..667f19d5c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Panels.swift @@ -132,7 +132,7 @@ extension CodeEditWindowController { let targetState = determineDesiredCollapseState( shouldHide: shouldHide, currentlyCollapsed: panel.isCollapsed(), - previouslyCollapsed: panel.getPrevCollapsed(), + previouslyCollapsed: panel.getPrevCollapsed() ) if panel.isCollapsed() != targetState { panel.toggle() From 7b205fb123190167992641082f60037e4c871160 Mon Sep 17 00:00:00 2001 From: Simon Kudsk <10168417+SimonKudsk@users.noreply.github.com> Date: Wed, 14 May 2025 22:44:13 +0200 Subject: [PATCH 9/9] Added UI Tests for HideInterfaceTest behaviour --- .../Other Tests/HideInterfaceTests.swift | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 CodeEditUITests/Other Tests/HideInterfaceTests.swift diff --git a/CodeEditUITests/Other Tests/HideInterfaceTests.swift b/CodeEditUITests/Other Tests/HideInterfaceTests.swift new file mode 100644 index 000000000..bf804747c --- /dev/null +++ b/CodeEditUITests/Other Tests/HideInterfaceTests.swift @@ -0,0 +1,184 @@ +// +// HiderInterfaceTests.swift +// CodeEditUITests +// +// Created by Simon Kudsk on 14/05/2025. +// + +import XCTest +final class HideInterfaceUITests: XCTestCase { + + // MARK: – Setup + private var app: XCUIApplication! + private var path: String! + + override func setUp() async throws { + try await MainActor.run { + (app, path) = try App.launchWithTempDir() + } + } + + /// List of the panels to test with + private let allPanels: () -> [String] = { + ["Navigator", "Inspector", "Utility Area", "Toolbar"] + } + + // MARK: – Tests + + /// Test 1: Ensure each panel can show and hide individually. + func testPanelsShowAndHideIndividually() { + let viewMenu = app.menuBars.menuBarItems["View"] + for panel in allPanels() { + // Show panel + let showItem = "Show \(panel)" + if viewMenu.menuItems[showItem].exists { + viewMenu.menuItems[showItem].click() + } + + // Verify panel is visible + viewMenu.click() + XCTAssertTrue(viewMenu.menuItems["Hide \(panel)"].exists, "\(panel) should be visible after show") + + // Hide panel and verify it being hidden + viewMenu.menuItems[("Hide \(panel)")].click() + viewMenu.click() + XCTAssertTrue(viewMenu.menuItems["Show \(panel)"].exists, "\(panel) should be hidden after hide") + } + } + + /// Test 2: Hide interface hides all panels. + func testHideInterfaceHidesAllPanels() { + let viewMenu = app.menuBars.menuBarItems["View"] + // Ensure all panels are shown + for panel in allPanels() { + let showItem = "Show \(panel)" + if viewMenu.menuItems[showItem].exists { + viewMenu.menuItems[showItem].click() + } + } + + // Hide interface + viewMenu.menuItems[("Hide Interface")].click() + + // Verify all panels are hidden + viewMenu.click() + for panel in allPanels() { + XCTAssertTrue(viewMenu.menuItems["Show \(panel)"].exists, "\(panel) should be hidden") + } + } + + /// Test 3: Show interface shows all panels when none are visible. + func testShowInterfaceShowsAllWhenNoneVisible() { + let viewMenu = app.menuBars.menuBarItems["View"] + // Ensure all panels are hidden + for panel in allPanels() { + let hideItem = "Hide \(panel)" + if viewMenu.menuItems[hideItem].exists { + viewMenu.menuItems[hideItem].click() + } + } + + // Verify button says Show Interface + viewMenu.click() + XCTAssertTrue(viewMenu.menuItems["Show Interface"].exists, "Interface button should say Show Interface") + + // Show interface without waiting + viewMenu.menuItems[("Show Interface")].click() + + // Verify all panels are shown + viewMenu.click() + for panel in allPanels() { + XCTAssertTrue( + viewMenu.menuItems["Hide \(panel)"].exists, + "\(panel) should be visible after showing interface" + ) + } + } + + /// Test 4: Show interface restores previous panel state. + func testShowInterfaceRestoresPreviousState() { + let viewMenu = app.menuBars.menuBarItems["View"] + let initialOpen = ["Navigator", "Toolbar"] + + // Set initial state + for panel in allPanels() { + let item = initialOpen.contains(panel) ? "Show \(panel)" : "Hide \(panel)" + if viewMenu.menuItems[item].exists { + viewMenu.menuItems[item].click() + } + } + + // Hide then show interface + viewMenu.menuItems[("Hide Interface")].click() + viewMenu.menuItems[("Show Interface")].click() + + // Verify only initial panels are shown + viewMenu.click() + for panel in allPanels() { + let shouldBeVisible = initialOpen.contains(panel) + XCTAssertEqual(viewMenu.menuItems["Hide \(panel)"].exists, shouldBeVisible, "\(panel) visibility mismatch") + } + } + + /// Test 5: Individual toggles after hide update the interface button. + func testIndividualTogglesUpdateInterfaceButton() { + let viewMenu = app.menuBars.menuBarItems["View"] + let initialOpen = ["Navigator", "Toolbar"] + + // Set initial visibility + for panel in allPanels() { + let item = initialOpen.contains(panel) ? "Show \(panel)" : "Hide \(panel)" + if viewMenu.menuItems[item].exists { + viewMenu.menuItems[item].click() + } + } + + // Hide interface + viewMenu.menuItems[("Hide Interface")].click() + + // Individually enable initial panels + for panel in initialOpen { + viewMenu.menuItems[("Show \(panel)")].click() + } + + // Verify interface button resets to Hide Interface + viewMenu.click() + XCTAssertTrue( + viewMenu.menuItems["Hide Interface"].exists, + "Interface should say hide interface when all previous panels are enabled again" + ) + } + + /// Test 6: Partial show after hide restores correct panels. + func testPartialShowAfterHideRestoresCorrectPanels() { + let viewMenu = app.menuBars.menuBarItems["View"] + let initialOpen = ["Navigator", "Toolbar"] + + // Set initial visibility + for panel in allPanels() { + let item = initialOpen.contains(panel) ? "Show \(panel)" : "Hide \(panel)" + if viewMenu.menuItems[item].exists { + viewMenu.menuItems[item].click() + } + } + + // Hide interface + viewMenu.menuItems[("Hide Interface")].click() + + // Individually enable navigator and inspector + for panel in ["Navigator", "Inspector"] { + viewMenu.menuItems[("Show \(panel)")].click() + } + // Show interface + viewMenu.menuItems[("Show Interface")].click() + + // Verify correct panels are shown + viewMenu.click() + for panel in ["Navigator", "Inspector", "Toolbar"] { + XCTAssertTrue(viewMenu.menuItems["Hide \(panel)"].exists, "\(panel) should be visible") + } + + // Utility Area should remain hidden + XCTAssertTrue(viewMenu.menuItems["Show Utility Area"].exists, "Utility Area should be hidden") + } +}