Skip to content

Commit b7827d7

Browse files
committed
Improving Utility Area Terminal
1 parent 26b6abe commit b7827d7

6 files changed

Lines changed: 277 additions & 142 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// NewGroupDropDelegate.swift
3+
// CodeEdit
4+
//
5+
// Created by Gustavo Soré on 30/06/25.
6+
//
7+
8+
import SwiftUI
9+
import UniformTypeIdentifiers
10+
11+
/// Drop delegate responsible for handling the case when a terminal is dropped
12+
/// outside of any existing group — i.e., it should create a new group with the dropped terminal.
13+
struct NewGroupDropDelegate: DropDelegate {
14+
/// The view model that manages terminal groups and selection state.
15+
let viewModel: UtilityAreaViewModel
16+
17+
/// Validates whether the drop operation includes terminal data that this delegate can handle.
18+
///
19+
/// - Parameter info: The drop information provided by the system.
20+
/// - Returns: `true` if the drop contains a valid terminal item type.
21+
func validateDrop(info: DropInfo) -> Bool {
22+
info.hasItemsConforming(to: [UTType.terminal.identifier])
23+
}
24+
25+
/// Performs the drop by creating a new group and moving the terminal into it.
26+
///
27+
/// - Parameter info: The drop information containing the dragged item.
28+
/// - Returns: `true` if the drop was successfully handled.
29+
func performDrop(info: DropInfo) -> Bool {
30+
// Extract the first item provider that conforms to the terminal type.
31+
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else {
32+
return false
33+
}
34+
35+
// Load and decode the terminal drag information.
36+
item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
37+
guard let data = data,
38+
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data),
39+
let terminal = viewModel.terminalGroups
40+
.flatMap({ $0.terminals })
41+
.first(where: { $0.id == dragInfo.terminalID }) else {
42+
return
43+
}
44+
45+
// Perform the group creation and terminal movement on the main thread.
46+
DispatchQueue.main.async {
47+
withAnimation(.easeInOut(duration: 0.2)) {
48+
// Optional logic to clean up old location (if needed).
49+
viewModel.finalizeMoveTerminal(terminal, toGroup: UUID(), before: nil)
50+
51+
// Create a new group containing the dropped terminal.
52+
viewModel.createGroup(with: [terminal])
53+
54+
// Reset drag-related state.
55+
viewModel.dragOverTerminalID = nil
56+
viewModel.draggedTerminalID = nil
57+
}
58+
}
59+
}
60+
61+
return true
62+
}
63+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// TerminalDropDelegate.swift
3+
// CodeEdit
4+
//
5+
// Created by Gustavo Soré on 30/06/25.
6+
//
7+
8+
import SwiftUI
9+
import UniformTypeIdentifiers
10+
11+
/// Handles drop interactions for a terminal inside a specific group,
12+
/// allowing for reordering or moving between groups.
13+
struct TerminalDropDelegate: DropDelegate {
14+
/// The ID of the group where the drop target resides.
15+
let groupID: UUID
16+
17+
/// The shared view model managing terminal groups and selection state.
18+
let viewModel: UtilityAreaViewModel
19+
20+
/// The ID of the terminal that is the drop destination, or `nil` if dropping at the end.
21+
let destinationTerminalID: UUID?
22+
23+
/// Validates if the drop contains terminal data.
24+
///
25+
/// - Parameter info: The current drop context.
26+
/// - Returns: `true` if the item conforms to the terminal type.
27+
func validateDrop(info: DropInfo) -> Bool {
28+
info.hasItemsConforming(to: [UTType.terminal.identifier])
29+
}
30+
31+
/// Called when the drop enters a new target.
32+
/// Sets the drag state in the view model for UI feedback.
33+
///
34+
/// - Parameter info: The drop context.
35+
func dropEntered(info: DropInfo) {
36+
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return }
37+
38+
item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
39+
guard let data = data,
40+
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data) else { return }
41+
42+
DispatchQueue.main.async {
43+
withAnimation {
44+
viewModel.draggedTerminalID = dragInfo.terminalID
45+
viewModel.dragOverTerminalID = destinationTerminalID
46+
}
47+
}
48+
}
49+
}
50+
51+
/// Called continuously as the drop is updated over the view.
52+
/// Updates drag-over visual feedback.
53+
///
54+
/// - Parameter info: The drop context.
55+
/// - Returns: A drop proposal that defines the type of drop operation (e.g., move).
56+
func dropUpdated(info: DropInfo) -> DropProposal? {
57+
DispatchQueue.main.async {
58+
withAnimation(.easeInOut(duration: 0.2)) {
59+
viewModel.dragOverTerminalID = destinationTerminalID
60+
}
61+
}
62+
63+
return DropProposal(operation: .move)
64+
}
65+
66+
/// Called when the drop is performed.
67+
/// Decodes the dragged terminal and triggers its relocation in the model.
68+
///
69+
/// - Parameter info: The drop context with the drag payload.
70+
/// - Returns: `true` if the drop was handled successfully.
71+
func performDrop(info: DropInfo) -> Bool {
72+
guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false }
73+
74+
item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in
75+
guard let data = data,
76+
let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data),
77+
let terminal = viewModel.terminalGroups
78+
.flatMap({ $0.terminals })
79+
.first(where: { $0.id == dragInfo.terminalID }) else { return }
80+
81+
DispatchQueue.main.async {
82+
withAnimation(.easeInOut(duration: 0.2)) {
83+
viewModel.finalizeMoveTerminal(
84+
terminal,
85+
toGroup: groupID,
86+
before: destinationTerminalID
87+
)
88+
viewModel.dragOverTerminalID = nil
89+
viewModel.draggedTerminalID = nil
90+
}
91+
}
92+
}
93+
94+
return true
95+
}
96+
}

CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,33 @@
88
import SwiftUI
99
import UniformTypeIdentifiers
1010

11+
/// A view that displays a terminal group with a header and a list of its terminals.
12+
/// Supports editing the group name, collapsing, drag-and-drop, and inline terminal row management.
1113
struct UtilityAreaTerminalGroupView: View {
14+
/// The index of the group within the terminalGroups array.
1215
let index: Int
16+
17+
/// Whether this group is currently selected in the UI.
1318
let isGroupSelected: Bool
19+
20+
/// The shared view model that manages terminal groups and their state.
1421
@EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel
22+
23+
/// Manages focus on a specific terminal row for keyboard navigation or editing.
1524
@FocusState private var focusedTerminalID: UUID?
1625

1726
var body: some View {
1827
VStack(alignment: .leading, spacing: 0) {
19-
HStack(spacing: 4) {
2028

29+
// MARK: - Group Header
30+
31+
HStack(spacing: 4) {
2132
Image(systemName: "square.on.square")
2233
.font(.system(size: 14, weight: .medium))
2334
.frame(width: 20, height: 20)
2435
.foregroundStyle(.primary.opacity(0.6))
2536

37+
// Editable group name when in edit mode
2638
if utilityAreaViewModel.editingGroupID == utilityAreaViewModel.terminalGroups[index].id {
2739
TextField("", text: Binding(
2840
get: { utilityAreaViewModel.terminalGroups[index].name },
@@ -47,7 +59,12 @@ struct UtilityAreaTerminalGroupView: View {
4759
utilityAreaViewModel.editingGroupID = nil
4860
}
4961
} else {
50-
Text(utilityAreaViewModel.terminalGroups[index].name.isEmpty ? "terminals" : utilityAreaViewModel.terminalGroups[index].name)
62+
// Display group name normally
63+
Text(
64+
utilityAreaViewModel.terminalGroups[index].name.isEmpty
65+
? "terminals"
66+
: utilityAreaViewModel.terminalGroups[index].name
67+
)
5168
.lineLimit(1)
5269
.truncationMode(.middle)
5370
.font(.system(size: 13, weight: .regular))
@@ -63,7 +80,12 @@ struct UtilityAreaTerminalGroupView: View {
6380

6481
Spacer()
6582

66-
Image(systemName: utilityAreaViewModel.terminalGroups[index].isCollapsed ? "chevron.right" : "chevron.down")
83+
// Expand/collapse toggle
84+
Image(
85+
systemName: utilityAreaViewModel.terminalGroups[index].isCollapsed
86+
? "chevron.right"
87+
: "chevron.down"
88+
)
6789
.font(.system(size: 11, weight: .medium))
6890
.foregroundColor(.secondary)
6991
}
@@ -76,8 +98,7 @@ struct UtilityAreaTerminalGroupView: View {
7698
}
7799
}
78100
.onDrag {
79-
// utilityAreaViewModel.draggedTerminalID = terminal.id
80-
101+
// Optional: dragging the entire group (stubbed terminal ID)
81102
let dragInfo = TerminalDragInfo(terminalID: .init())
82103
let provider = NSItemProvider()
83104
do {
@@ -90,10 +111,13 @@ struct UtilityAreaTerminalGroupView: View {
90111
return nil
91112
}
92113
} catch {
93-
print("Erro ao codificar dragInfo: \(error)")
114+
print("Failed to encode dragInfo: \(error)")
94115
}
95116
return provider
96117
}
118+
119+
// MARK: - Terminal Rows (if group is expanded)
120+
97121
if !utilityAreaViewModel.terminalGroups[index].isCollapsed {
98122
VStack(spacing: 0) {
99123
ForEach(utilityAreaViewModel.terminalGroups[index].terminals, id: \.id) { terminal in
@@ -107,17 +131,15 @@ struct UtilityAreaTerminalGroupView: View {
107131

108132
let dragInfo = TerminalDragInfo(terminalID: terminal.id)
109133
let provider = NSItemProvider()
110-
do {
111-
let data = try JSONEncoder().encode(dragInfo)
112-
provider.registerDataRepresentation(
113-
forTypeIdentifier: UTType.terminal.identifier,
114-
visibility: .all
115-
) { completion in
116-
completion(data, nil)
117-
return nil
118-
}
119-
} catch {
120-
print("❌ Erro ao codificar dragInfo: \(error)")
134+
guard let data = try? JSONEncoder().encode(dragInfo) else {
135+
return provider
136+
}
137+
provider.registerDataRepresentation(
138+
forTypeIdentifier: UTType.terminal.identifier,
139+
visibility: .all
140+
) { completion in
141+
completion(data, nil)
142+
return nil
121143
}
122144
return provider
123145
}
@@ -130,7 +152,10 @@ struct UtilityAreaTerminalGroupView: View {
130152
)
131153
)
132154
.transition(.opacity.combined(with: .move(edge: .top)))
133-
.animation(.easeInOut(duration: 0.2), value: utilityAreaViewModel.terminalGroups[index].isCollapsed)
155+
.animation(
156+
.easeInOut(duration: 0.2),
157+
value: utilityAreaViewModel.terminalGroups[index].isCollapsed
158+
)
134159
}
135160
}
136161
}
@@ -147,6 +172,9 @@ struct UtilityAreaTerminalGroupView: View {
147172
}
148173
}
149174

175+
// MARK: - Preview
176+
177+
/// Preview provider for `UtilityAreaTerminalGroupView`, showing a mock terminal group with two terminals.
150178
struct TerminalGroupViewPreviews: PreviewProvider {
151179
static var previews: some View {
152180
let terminal = UtilityAreaTerminal(
@@ -165,7 +193,7 @@ struct TerminalGroupViewPreviews: PreviewProvider {
165193

166194
let utilityAreaViewModel = UtilityAreaViewModel()
167195
utilityAreaViewModel.terminalGroups = [
168-
UtilityAreaTerminalGroup(name: "Grupo de Preview", terminals: [terminal, terminal2])
196+
UtilityAreaTerminalGroup(name: "Preview Group", terminals: [terminal, terminal2])
169197
]
170198
utilityAreaViewModel.selectedTerminals = [terminal.id]
171199

@@ -182,6 +210,7 @@ struct TerminalGroupViewPreviews: PreviewProvider {
182210
}
183211
}
184212

213+
// Wrapper view to render the preview group.
185214
private struct TerminalGroupViewPreviewWrapper: View {
186215
@EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel
187216
@FocusState private var focusedTerminalID: UUID?

0 commit comments

Comments
 (0)