Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard shortcut to switch sidebar navigator tabs #1382

Merged
merged 24 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f2d2e29
Fixed close tab and close window shortcut
FastestMolasses Jul 17, 2023
210884b
Replace .hidden modifier with .opacity modifier
FastestMolasses Jul 17, 2023
518c4ab
Fixed tab switching shortcuts not working
FastestMolasses Jul 17, 2023
3e07428
Undo changes
FastestMolasses Jul 17, 2023
3ac4741
Fixed the close button not being able to be pressed
FastestMolasses Jul 17, 2023
3cb05fb
Merge branch 'main'
FastestMolasses Jul 23, 2023
989ecb2
Update keyboard shortcuts to switch between sidebar navigator items
FastestMolasses Jul 24, 2023
85e31a7
Remove old shortcut
FastestMolasses Jul 24, 2023
395e922
Remove old code
FastestMolasses Jul 24, 2023
3a042b8
Small refactor
FastestMolasses Jul 24, 2023
d9f514e
New drag gestures, refactors
FastestMolasses Aug 27, 2023
2e7379e
Merge remote-tracking branch 'origin' into switch-tabs
FastestMolasses Aug 27, 2023
b2f5634
Cleanup and added comments
FastestMolasses Aug 27, 2023
e728a7b
Merge main
FastestMolasses Aug 27, 2023
ecb34c6
Removed file
FastestMolasses Aug 30, 2023
02703c0
Fixed variable bindings
FastestMolasses Aug 30, 2023
e13e2d0
Added ObservedObject to NavigatorSidebarViewModel
FastestMolasses Aug 30, 2023
89395a9
Limit index shortcuts to 9
FastestMolasses Aug 30, 2023
72a7184
Refactors
FastestMolasses Sep 8, 2023
2612387
Refactored swapping code
FastestMolasses Sep 11, 2023
b250c2f
Merge branch 'main' into switch-tabs
FastestMolasses Sep 13, 2023
5ddc478
Fixed variable that wasnt renamed
FastestMolasses Sep 13, 2023
8a7815b
Lint fixes
FastestMolasses Sep 15, 2023
40f6356
Remove comment
FastestMolasses Sep 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
2B7AC06B282452FB0082A5B8 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B7AC06A282452FB0082A5B8 /* Media.xcassets */; };
2BE487EF28245162003F3F64 /* FinderSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE487EE28245162003F3F64 /* FinderSync.swift */; };
2BE487F428245162003F3F64 /* OpenWithCodeEdit.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */; };
3E0196732A3921AC002648D8 /* codeedit_shell_integration.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */; };
3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */; };
474397C52893AC4B00518C8C /* codeedit-midnight.json in Resources */ = {isa = PBXBuildFile; fileRef = 474397C42893AC4B00518C8C /* codeedit-midnight.json */; };
Expand Down Expand Up @@ -517,6 +518,7 @@
2BE487EC28245162003F3F64 /* OpenWithCodeEdit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenWithCodeEdit.appex; sourceTree = BUILT_PRODUCTS_DIR; };
2BE487EE28245162003F3F64 /* FinderSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinderSync.swift; sourceTree = "<group>"; };
2BE487F028245162003F3F64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarViewModel.swift; sourceTree = "<group>"; };
3E0196722A3921AC002648D8 /* codeedit_shell_integration.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.zsh; sourceTree = "<group>"; };
3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.bash; sourceTree = "<group>"; };
474397C42893AC4B00518C8C /* codeedit-midnight.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "codeedit-midnight.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1063,6 +1065,7 @@
581550CB29FBD30400684881 /* OutlineView */,
286471AC27ED52950039369D /* ProjectNavigator */,
201169D52837B29600F92B46 /* SourceControlNavigator */,
30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */,
B67660682AA972D400CD56B0 /* Models */,
B67660692AA972DC00CD56B0 /* Views */,
);
Expand Down Expand Up @@ -3076,6 +3079,7 @@
6C6BD6F629CD145F00235D17 /* ExtensionInfo.swift in Sources */,
58F2EB05292FB2B0004A9BDE /* Settings.swift in Sources */,
6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */,
30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */,
B6E41C9429DEAE260088F9F4 /* SourceControlAccount.swift in Sources */,
2806E9022979588B000040F4 /* Contributor.swift in Sources */,
58D01C98293167DC00C5B6B4 /* String+RemoveOccurrences.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/Rearrange",
"state" : {
"revision" : "0fb658e721c68495f6340c211cc6d4719e6b52d8",
"version" : "1.6.0"
"revision" : "8f97f721d8a08c6e01ab9f7460e53819bef72dfa",
"version" : "1.5.3"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class DirectoryEventStream {
_ eventFlags: UnsafePointer<FSEventStreamEventFlags>,
_ eventIds: UnsafePointer<FSEventStreamEventId>
) {
var eventPaths = eventPaths.bindMemory(to: UnsafePointer<CChar>.self, capacity: numEvents)
let eventPaths = eventPaths.bindMemory(to: UnsafePointer<CChar>.self, capacity: numEvents)
austincondiff marked this conversation as resolved.
Show resolved Hide resolved
for idx in 0..<numEvents {
let pathPtr = eventPaths.advanced(by: idx).pointee
let path = String(cString: pathPtr)
Expand Down
272 changes: 195 additions & 77 deletions CodeEdit/Features/CodeEditUI/Views/AreaTabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,30 @@ struct AreaTabBar<Tab: AreaTab>: View {
@Environment(\.controlActiveState)
private var activeState

var items: [Tab]

@Binding var items: [Tab]
@Binding var selection: Tab?

var position: SettingsData.SidebarTabBarPosition

@State private var hasChangedLocation: Bool = false
@State private var draggingItem: SidebarDockIcon?
@State private var drugItemLocation: CGPoint?
@State private var tabLocations: [Tab: CGRect] = [:]
@State private var tabWidth: [Tab: CGFloat] = [:]
@State private var tabOffsets: [Tab: CGFloat] = [:]

/// The tab currently being dragged.
///
/// It will be `nil` when there is no tab dragged currently.
@State private var draggingTab: Tab?

/// The start location of dragging.
///
/// When there is no tab being dragged, it will be `nil`.
@State private var draggingStartLocation: CGFloat?

/// The last location of dragging.
///
/// This is used to determine the dragging direction.
/// - TODO: Check if I can use `value.translation` instead.
@State private var draggingLastLocation: CGFloat?

var body: some View {
if position == .top {
Expand Down Expand Up @@ -63,19 +78,15 @@ struct AreaTabBar<Tab: AreaTab>: View {
layout {
ForEach(items) { icon in
makeIcon(tab: icon, size: size)
.opacity(draggingItem?.imageName == icon.systemImage &&
hasChangedLocation &&
drugItemLocation != nil ? 0.0 : 1.0)
// .onDrop(
// of: [.utf8PlainText],
// delegate: InspectorAreaDockIconDelegate(
// item: icon,
// current: $draggingItem,
// icons: $icons,
// hasChangedLocation: $hasChangedLocation,
// drugItemLocation: $drugItemLocation
// )
// )
.offset(
x: (position == .top) ? (tabOffsets[icon] ?? 0) : 0,
y: (position == .side) ? (tabOffsets[icon] ?? 0) : 0
)
.background(makeTabItemGeometryReader(tab: icon))
.simultaneousGesture(makeAreaTabDragGesture(tab: icon))
// .opacity(draggingItem?.imageName == icon.systemImage &&
// hasChangedLocation &&
// drugItemLocation != nil ? 0.0 : 1.0)
}
if position == .side {
Spacer()
Expand All @@ -100,85 +111,192 @@ struct AreaTabBar<Tab: AreaTab>: View {
alignment: .center
)
.help(tab.title)
// .onDrag {
// if let index = icons.firstIndex(where: { $0.imageName == named }) {
// draggingItem = icons[index]
// }
// return .init(object: NSString(string: named))
// } preview: {
// RoundedRectangle(cornerRadius: .zero)
// .frame(width: .zero)
// }
}
.buttonStyle(.icon(isActive: tab == selection, size: nil))
}

private func getSafeImage(named: String, accessibilityDescription: String?) -> Image {
// We still use the NSImage init to check if a symbol with the name exists.
if NSImage(systemSymbolName: named, accessibilityDescription: nil) != nil {
return Image(systemName: named)
} else {
return Image(symbol: named)
}
}
private func makeAreaTabDragGesture(tab: Tab) -> some Gesture {
DragGesture(minimumDistance: 2, coordinateSpace: .global)
.onChanged({ value in
if draggingTab != tab {
initializeDragGesture(value: value, for: tab)
}

struct NavigatorToolbarButtonStyle: ButtonStyle {
var id: Int
var selection: Int
var activeState: ControlActiveState
var sidebarWidth: CGFloat
// Get the current cursor location
let currentLocation = (position == .top) ? value.location.x : value.location.y
guard let startLocation = draggingStartLocation,
let currentIndex = items.firstIndex(of: tab),
let currentTabWidth = tabWidth[tab],
let lastLocation = draggingLastLocation
else { return }

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(id == selection ? .accentColor : configuration.isPressed ? .primary : .secondary)
}
let dragDifference = currentLocation - lastLocation
tabOffsets[tab] = currentLocation - startLocation

// Check for swaps between adjacent tabs
// Left tab
swapTab(
tab: tab,
currentIndex: currentIndex,
currentLocation: currentLocation,
dragDifference: dragDifference,
currentTabWidth: currentTabWidth,
direction: .previous
)
// Right tab
swapTab(
tab: tab,
currentIndex: currentIndex,
currentLocation: currentLocation,
dragDifference: dragDifference,
currentTabWidth: currentTabWidth,
direction: .next
)

// Update the last dragging location if there's enough offset
let currentLocationOnAxis = ((position == .top) ? value.location.x : value.location.y)
if draggingLastLocation == nil || abs(currentLocationOnAxis - draggingLastLocation!) >= 10 {
draggingLastLocation = (position == .top) ? value.location.x : value.location.y
}
})
.onEnded({ _ in
draggingStartLocation = nil
draggingLastLocation = nil
withAnimation(.easeInOut(duration: 0.25)) {
tabOffsets = [:]
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
draggingTab = nil
}
})
}

private struct SidebarDockIcon: Identifiable, Equatable {
let imageName: String
let title: String
var id: Int
var disabled: Bool = false
private func initializeDragGesture(value: DragGesture.Value, for tab: Tab) {
draggingTab = tab
let initialLocation = position == .top ? value.startLocation.x : value.startLocation.y
draggingStartLocation = initialLocation
draggingLastLocation = initialLocation
}

private struct NavigatorSidebarDockIconDelegate: DropDelegate {
let item: SidebarDockIcon
@Binding var current: SidebarDockIcon?
@Binding var icons: [SidebarDockIcon]
@Binding var hasChangedLocation: Bool
@Binding var drugItemLocation: CGPoint?

func dropEntered(info: DropInfo) {
if current == nil {
current = item
drugItemLocation = info.location
enum SwapDirection {
case previous
case next
}

// swiftlint: disable function_parameter_count
private func swapTab(
tab: Tab,
currentIndex: Int,
currentLocation: CGFloat,
dragDifference: CGFloat,
currentTabWidth: CGFloat,
direction: SwapDirection
) {
// Determine the index to swap with based on direction
var swapIndex: Int?
if direction == .previous {
if currentIndex > 0 {
swapIndex = currentIndex - 1
}
} else {
if currentIndex < items.count - 1 {
swapIndex = currentIndex + 1
}
}

guard item != current, let current = current,
let from = icons.firstIndex(of: current),
let toIndex = icons.firstIndex(of: item) else { return }
// Validate the drag direction
let isValidDragDir = (direction == .previous && dragDifference < 0) ||
(direction == .next && dragDifference > 0)
guard let swapIndex = swapIndex, isValidDragDir else { return }

hasChangedLocation = true
drugItemLocation = info.location
// Get info about the tab to swap with
let swapTab = items[swapIndex]
guard let swapTabLocation = tabLocations[swapTab],
let swapTabWidth = tabWidth[swapTab]
else { return }

if icons[toIndex] != current {
icons.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
}
let isWithinBounds: Bool
if position == .top {
isWithinBounds = direction == .previous ?
isWithinPrevTopBounds(currentLocation, swapTabLocation, swapTabWidth) :
isWithinNextTopBounds(currentLocation, swapTabLocation, swapTabWidth, currentTabWidth)
} else {
isWithinBounds = direction == .previous ?
isWithinPrevBottomBounds(currentLocation, swapTabLocation, swapTabWidth) :
isWithinNextBottomBounds(currentLocation, swapTabLocation, swapTabWidth, currentTabWidth)
}

func dropExited(info: DropInfo) {
drugItemLocation = nil
// Swap tab positions
if isWithinBounds {
let changing = swapTabWidth - 1
draggingStartLocation! += direction == .previous ? -changing : changing
withAnimation {
tabOffsets[tab]! += direction == .previous ? changing : -changing
items.swapAt(currentIndex, swapIndex)
}
}
}
// swiftlint: enable function_parameter_count

func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
private func isWithinPrevTopBounds(
_ curLocation: CGFloat, _ swapLocation: CGRect, _ swapWidth: CGFloat
) -> Bool {
return curLocation < max(
swapLocation.maxX - swapWidth * 0.1,
swapLocation.minX + swapWidth * 0.9
)
}

private func isWithinNextTopBounds(
_ curLocation: CGFloat, _ swapLocation: CGRect, _ swapWidth: CGFloat, _ curWidth: CGFloat
) -> Bool {
return curLocation > min(
swapLocation.minX + swapWidth * 0.1,
swapLocation.maxX - curWidth * 0.9
)
}

private func isWithinPrevBottomBounds(
_ curLocation: CGFloat, _ swapLocation: CGRect, _ swapWidth: CGFloat
) -> Bool {
return curLocation < max(
swapLocation.maxY - swapWidth * 0.1,
swapLocation.minY + swapWidth * 0.9
)
}

private func isWithinNextBottomBounds(
_ curLocation: CGFloat, _ swapLocation: CGRect, _ swapWidth: CGFloat, _ curWidth: CGFloat
) -> Bool {
return curLocation > min(
swapLocation.minY + swapWidth * 0.1,
swapLocation.maxY - curWidth * 0.9
)
}

private func makeTabItemGeometryReader(tab: Tab) -> some View {
GeometryReader { geometry in
Rectangle()
.foregroundColor(.clear)
.onAppear {
self.tabWidth[tab] = geometry.size.width
self.tabLocations[tab] = geometry.frame(in: .global)
}
.onChange(of: geometry.frame(in: .global)) { newFrame in
self.tabLocations[tab] = newFrame
}
.onChange(of: geometry.size.width) { newWidth in
self.tabWidth[tab] = newWidth
}
}
}

func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
drugItemLocation = nil
current = nil
return true
private func getSafeImage(named: String, accessibilityDescription: String?) -> Image {
// We still use the NSImage init to check if a symbol with the name exists.
if NSImage(systemSymbolName: named, accessibilityDescription: nil) != nil {
return Image(systemName: named)
} else {
return Image(symbol: named)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
var workspace: WorkspaceDocument?
var quickOpenPanel: OverlayPanel?
var commandPalettePanel: OverlayPanel?
var navigatorSidebarViewModel: NavigatorSidebarViewModel?

var splitViewController: NSSplitViewController!

Expand Down Expand Up @@ -79,13 +80,18 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer
let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer)

let navigatorView = SettingsInjector {
NavigatorAreaView(workspace: workspace)
let navigatorViewModel = NavigatorSidebarViewModel()
FastestMolasses marked this conversation as resolved.
Show resolved Hide resolved
navigatorSidebarViewModel = navigatorViewModel

let settingsView = SettingsInjector {
NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel)
.environmentObject(workspace)
.environmentObject(workspace.editorManager)
}

let navigator = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: navigatorView))
let navigator = NSSplitViewItem(
sidebarWithViewController: NSHostingController(rootView: settingsView)
)
navigator.titlebarSeparatorStyle = .none
navigator.minimumThickness = Self.minSidebarWidth
navigator.collapseBehavior = .useConstraints
Expand Down
Loading