From a268464c364fa70c1b9cc9cf6d151a9fd7286b8e Mon Sep 17 00:00:00 2001 From: Sore Date: Sat, 28 Jun 2025 14:05:57 -0300 Subject: [PATCH 01/25] Terminal Group FIrst COmmit --- .../UtilityAreaTerminalSidebar.swift | 180 +++++++-- .../UtilityAreaTerminalTab.swift | 67 ---- .../UtilityAreaTerminalView.swift | 12 +- .../ViewModels/UtilityAreaViewModel.swift | 347 +++++++++++++----- .../UtilityArea/Views/GroupTitleEditor.swift | 70 ++++ .../Views/UtilityAreaTerminalTab.swift | 130 +++++++ CodeEdit/Info.plist | 20 + 7 files changed, 637 insertions(+), 189 deletions(-) delete mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift create mode 100644 CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift create mode 100644 CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 1c61d8bb4..9e3434613 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -1,37 +1,125 @@ -// -// UtilityAreaTerminalSidebar.swift -// CodeEdit -// -// Created by Khan Winter on 8/19/24. -// +// UtilityAreaTerminalSidebar.swift +// Com ScrollView + VStack e suporte a drag & drop com preview e grupos colapsáveis import SwiftUI +import UniformTypeIdentifiers + +extension UTType { + static let terminal = UTType(exportedAs: "dev.codeedit.terminal") +} + +struct TerminalDragInfo: Codable { + let terminalID: UUID +} + +struct InsertionIndicator: View { + var body: some View { + Rectangle() + .fill(Color.accentColor) + .frame(height: 2) + .padding(.horizontal, 6) + .transition(.opacity) + } +} -/// The view that displays the list of available terminals in the utility area. -/// See ``UtilityAreaTerminalView`` for use. struct UtilityAreaTerminalSidebar: View { @EnvironmentObject private var workspace: WorkspaceDocument @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + @FocusState private var focusedTerminalID: UUID? var body: some View { - List(selection: $utilityAreaViewModel.selectedTerminals) { - ForEach(utilityAreaViewModel.terminals, id: \.self.id) { terminal in - UtilityAreaTerminalTab( - terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, - isSelected: utilityAreaViewModel.selectedTerminals.contains(terminal.id), - selectedIDs: utilityAreaViewModel.selectedTerminals - ) - .tag(terminal.id) - .listRowSeparator(.hidden) - } - .onMove { [weak utilityAreaViewModel] (source, destination) in - utilityAreaViewModel?.reorderTerminals(from: source, to: destination) + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(utilityAreaViewModel.terminalGroups.enumerated()), id: \.element.id) { index, group in + let isEditing = utilityAreaViewModel.editingGroupID == group.id + let isGroupSelected = group.terminals.contains { utilityAreaViewModel.selectedTerminals.contains($0.id) } + let groupID = group.id + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 4) { + Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + + GroupTitleEditor( + index: index, + group: group, + isEditing: isEditing, + viewModel: utilityAreaViewModel + ) + + Spacer() + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.25)) { + utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() + } + } + + if !group.isCollapsed { + VStack(spacing: 0) { + ForEach(group.terminals, id: \.id) { terminal in + VStack(spacing: 0) { + if utilityAreaViewModel.dragOverTerminalID == terminal.id { + InsertionIndicator() + } + + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: utilityAreaViewModel.removeTerminals, + focusedTerminalID: $focusedTerminalID + ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + )) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) + } + } + } + .padding(.bottom, 8) + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: nil + )) + } + } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) + ) + .cornerRadius(8) + .padding(.horizontal) + } } + .padding(.top) } - .focusedObject(utilityAreaViewModel) - .listStyle(.automatic) - .accentColor(.secondary) + .onDrop(of: [UTType.terminal.identifier], delegate: NewGroupDropDelegate(viewModel: utilityAreaViewModel)) + .background(Color(NSColor.controlBackgroundColor)) .contextMenu { Button("New Terminal") { utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) @@ -48,11 +136,6 @@ struct UtilityAreaTerminalSidebar: View { } } } - .onChange(of: utilityAreaViewModel.terminals) { newValue in - if newValue.isEmpty { - utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) - } - } .paneToolbar { PaneToolbarSection { Button { @@ -65,16 +148,47 @@ struct UtilityAreaTerminalSidebar: View { } label: { Image(systemName: "minus") } - .disabled(utilityAreaViewModel.terminals.count <= 1) - .opacity(utilityAreaViewModel.terminals.count <= 1 ? 0.5 : 1) + .disabled(utilityAreaViewModel.terminalGroups.flatMap { $0.terminals }.count <= 1) + .opacity(utilityAreaViewModel.terminalGroups.flatMap { $0.terminals }.count <= 1 ? 0.5 : 1) } Spacer() } .accessibilityElement(children: .contain) .accessibilityLabel("Terminals") + .background( + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + focusedTerminalID = nil + } + ) } } -#Preview { - UtilityAreaTerminalSidebar() +struct UtilityAreaTerminalSidebar_Previews: PreviewProvider { + static var previews: some View { + let terminal = UtilityAreaTerminal( + id: UUID(), + url: URL(string: "https://example.com")!, + title: "Terminal 1", + shell: .zsh + ) + + let utilityAreaViewModel = UtilityAreaViewModel() + utilityAreaViewModel.terminalGroups = [ + UtilityAreaTerminalGroup(name: "Grupo", terminals: [terminal]) + ] + utilityAreaViewModel.selectedTerminals = [terminal.id] + + let tabViewModel = UtilityAreaTabViewModel() + + let workspace = WorkspaceDocument() + workspace.setValue(URL(string: "file:///mock/path")!, forKey: "fileURL") + + return UtilityAreaTerminalSidebar() + .environmentObject(utilityAreaViewModel) + .environmentObject(tabViewModel) + .environmentObject(workspace) + .frame(width: 300, height: 400) + } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift deleted file mode 100644 index 2ee68eb73..000000000 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// UtilityAreaTerminalTab.swift -// CodeEdit -// -// Created by Austin Condiff on 5/26/23. -// - -import SwiftUI - -struct UtilityAreaTerminalTab: View { - @ObservedObject var terminal: UtilityAreaTerminal - - var removeTerminals: (_ ids: Set) -> Void - - var isSelected: Bool - - var selectedIDs: Set - - @FocusState private var isFocused: Bool - - var body: some View { - let terminalTitle = Binding( - get: { - self.terminal.title - }, set: { - if $0.trimmingCharacters(in: .whitespaces) == "" && !isFocused { - self.terminal.title = self.terminal.terminalTitle - self.terminal.customTitle = false - } else { - self.terminal.title = $0 - self.terminal.customTitle = true - } - } - ) - - Label { - if #available(macOS 14, *) { - // Fix the icon misplacement issue introduced since macOS 14 - TextField("Name", text: terminalTitle) - .focused($isFocused) - } else { - // A padding is needed for macOS 13 - TextField("Name", text: terminalTitle) - .focused($isFocused) - .padding(.leading, -8) - } - } icon: { - Image(systemName: "terminal") - .accessibilityHidden(true) - } - .contextMenu { - Button("Rename...") { - isFocused = true - } - - if selectedIDs.contains(terminal.id) && selectedIDs.count > 1 { - Button("Kill Terminals") { - removeTerminals(selectedIDs) - } - } else { - Button("Kill Terminal") { - removeTerminals([terminal.id]) - } - } - } - } -} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 998ed620b..a2679289a 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -158,7 +158,7 @@ struct UtilityAreaTerminalView: View { } .colorScheme(terminalColorScheme) } leadingSidebar: { _ in - UtilityAreaTerminalSidebar() + UtilityAreaTerminalSidebarWrapper(viewModel: utilityAreaViewModel) } .onAppear { guard let workspaceURL = workspace.fileURL else { @@ -183,3 +183,13 @@ struct UtilityAreaTerminalView: View { } } } + +struct UtilityAreaTerminalSidebarWrapper: View { + @ObservedObject var viewModel: UtilityAreaViewModel + + var body: some View { + + + return UtilityAreaTerminalSidebar() + } +} diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 0cc075464..36c678505 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -1,40 +1,131 @@ -// -// UtilityAreaViewModel.swift -// CodeEdit -// -// Created by Lukas Pistrol on 20.03.22. -// +// UtilityAreaViewModel.swift +// Atualizado para suportar drag-and-drop com reordenação visual e final com ScrollView + VStack import SwiftUI +import UniformTypeIdentifiers + +extension Shell { + var iconName: String { + switch self { + case .bash: return "terminal" + case .zsh: return "circle.lefthalf.filled" + } + } +} + +struct UtilityAreaTerminalGroup: Identifiable, Hashable { + var id = UUID() + var name: String = "Grupo" + var terminals: [UtilityAreaTerminal] = [] + var isCollapsed: Bool = false + + static func == (lhs: UtilityAreaTerminalGroup, rhs: UtilityAreaTerminalGroup) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} /// # UtilityAreaViewModel -/// /// A model class to host and manage data for the Utility area. class UtilityAreaViewModel: ObservableObject { @Published var selectedTab: UtilityAreaTab? = .terminal @Published var terminals: [UtilityAreaTerminal] = [] + @Published var terminalGroups: [UtilityAreaTerminalGroup] = [] { + didSet { + self.terminals = terminalGroups.flatMap { $0.terminals } + } + } - @Published var selectedTerminals: Set = [] + @Published var selectedTerminals: Set = [] + @Published var dragOverTerminalID: UUID? = nil + @Published var draggedTerminalID: UUID? = nil - /// 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 - - /// The current height of the drawer. Zero if hidden @Published var currentHeight: Double = 0 + + @Published var editingTerminalID: UUID? = nil - /// The tab bar items for the UtilityAreaView @Published var tabItems: [UtilityAreaTab] = UtilityAreaTab.allCases - - /// The tab bar view model for UtilityAreaTabView @Published var tabViewModel = UtilityAreaTabViewModel() + + @Published var editingGroupID: UUID? = nil + + // MARK: - Drag Support + + func previewMoveTerminal(_ terminalID: UUID, toGroup groupID: UUID, before destinationID: UUID?) { + guard let currentGroupIndex = terminalGroups.firstIndex(where: { $0.terminals.contains(where: { $0.id == terminalID }) }), + let currentTerminalIndex = terminalGroups[currentGroupIndex].terminals.firstIndex(where: { $0.id == terminalID }) else { + return + } + + let currentGroupID = terminalGroups[currentGroupIndex].id + if currentGroupID == groupID, + let destID = destinationID, + terminalGroups[currentGroupIndex].terminals.firstIndex(where: { $0.id == destID }) == currentTerminalIndex { + return + } + + let terminal = terminalGroups[currentGroupIndex].terminals[currentTerminalIndex] + terminalGroups[currentGroupIndex].terminals.remove(at: currentTerminalIndex) + terminalGroups[currentGroupIndex].terminals = terminalGroups[currentGroupIndex].terminals + + if let targetIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) { + var group = terminalGroups[targetIndex] + + if let destID = destinationID, + let destIndex = group.terminals.firstIndex(where: { $0.id == destID }) { + group.terminals.insert(terminal, at: destIndex) + } else { + group.terminals.append(terminal) + } + + terminalGroups[targetIndex] = group + } + } + + func finalizeMoveTerminal(_ terminal: UtilityAreaTerminal, toGroup groupID: UUID, before destinationID: UUID?) { + // Remove de qualquer grupo atual + for index in terminalGroups.indices { + terminalGroups[index].terminals.removeAll { $0.id == terminal.id } + } + + // Remove grupos vazios após a remoção + terminalGroups.removeAll { $0.terminals.isEmpty } + + // Adiciona ao grupo destino + guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { + print("⚠️ Grupo não encontrado para o drop.") + return + } + + if let destinationID, + let destinationIndex = terminalGroups[groupIndex].terminals.firstIndex(where: { $0.id == destinationID }) { + terminalGroups[groupIndex].terminals.insert(terminal, at: destinationIndex) + } else { + terminalGroups[groupIndex].terminals.append(terminal) + } + + // Atualiza seleção + if !selectedTerminals.contains(terminal.id) { + selectedTerminals = [terminal.id] + } + } + + private func removeTerminal(withID id: UUID) -> UtilityAreaTerminal? { + for index in terminalGroups.indices { + if let terminalIndex = terminalGroups[index].terminals.firstIndex(where: { $0.id == id }) { + return terminalGroups[index].terminals.remove(at: terminalIndex) + } + } + return nil + } // MARK: - State Restoration @@ -58,103 +149,183 @@ class UtilityAreaViewModel: ObservableObject { // MARK: - Terminal Management - /// Removes all terminals included in the given set and selects a new terminal if the selection was modified. - /// The new selection is either the same selection minus the ids removed, or if that's empty the last terminal. - /// - Parameter ids: A set of all terminal ids to remove. func removeTerminals(_ ids: Set) { - for (idx, terminal) in terminals.enumerated().reversed() - where ids.contains(terminal.id) { - TerminalCache.shared.removeCachedView(terminal.id) - terminals.remove(at: idx) + for index in terminalGroups.indices { + terminalGroups[index].terminals.removeAll { ids.contains($0.id) } } - var newSelection = selectedTerminals.subtracting(ids) + // Remove grupos vazios + terminalGroups.removeAll { $0.terminals.isEmpty } - if newSelection.isEmpty, let terminal = terminals.last { - newSelection = [terminal.id] + // Atualiza seleção + selectedTerminals.subtract(ids) + if selectedTerminals.isEmpty, + let last = terminalGroups.last?.terminals.last { + selectedTerminals = [last.id] } - - selectedTerminals = newSelection } - /// Update a terminal's title. - /// - Parameters: - /// - id: The id of the terminal to update. - /// - title: The title to set. If left `nil`, will set the terminal's - /// ``UtilityAreaTerminal/customTitle`` to `false`. func updateTerminal(_ id: UUID, title: String?) { - guard let terminal = terminals.first(where: { $0.id == id }) else { return } - if let newTitle = title { - if !terminal.customTitle { - terminal.title = newTitle + for index in terminalGroups.indices { + if let terminalIndex = terminalGroups[index].terminals.firstIndex(where: { $0.id == id }) { + if let newTitle = title { + terminalGroups[index].terminals[terminalIndex].title = newTitle + terminalGroups[index].terminals[terminalIndex].terminalTitle = newTitle + } else { + terminalGroups[index].terminals[terminalIndex].customTitle = false + } + break } - terminal.terminalTitle = newTitle - } else { - terminal.customTitle = false } } - /// Create a new terminal if there are no existing terminals. - /// Will not perform any action if terminals exist in the ``terminals`` array. - /// - Parameter workspaceURL: The base url of the workspace, to initialize terminals.l func initializeTerminals(workspaceURL: URL) { - guard terminals.isEmpty else { return } + guard terminalGroups.flatMap({ $0.terminals }).isEmpty else { return } addTerminal(rootURL: workspaceURL) } - /// Add a new terminal to the workspace and selects it. - /// - Parameters: - /// - shell: The shell to use, `nil` if auto-detect the default shell. - /// - rootURL: The url to start the new terminal at. If left `nil` defaults to the user's home directory. - func addTerminal(shell: Shell? = nil, rootURL: URL?) { - let id = UUID() - - terminals.append( - UtilityAreaTerminal( - id: id, - url: rootURL ?? URL(filePath: "~/"), - title: shell?.rawValue ?? "terminal", - shell: shell - ) + func addTerminal(to groupID: UUID? = nil, shell: Shell? = nil, rootURL: URL?) { + let newTerminal = UtilityAreaTerminal( + id: UUID(), + url: rootURL ?? URL(filePath: "~/"), + title: shell?.rawValue ?? "terminal", + shell: shell ) - selectedTerminals = [id] + if let groupID, let index = terminalGroups.firstIndex(where: { $0.id == groupID }) { + terminalGroups[index].terminals.append(newTerminal) + } else { + terminalGroups.append(.init(name: "New Group", terminals: [newTerminal])) + } + + selectedTerminals = [newTerminal.id] } - /// Replaces the terminal with a given ID, killing the shell and restarting it at the same directory. - /// - /// Terminals being replaced will have the `SIGKILL` signal sent to the running shell. The new terminal will - /// inherit the same `url` and `shell` parameters from the old one. - /// - Parameter replacing: The ID of a terminal to replace with a new terminal. func replaceTerminal(_ replacing: UUID) { - guard let index = terminals.firstIndex(where: { $0.id == replacing }) else { - return + for index in terminalGroups.indices { + if let idx = terminalGroups[index].terminals.firstIndex(where: { $0.id == replacing }) { + let url = terminalGroups[index].terminals[idx].url + let shell = terminalGroups[index].terminals[idx].shell + if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { + kill(shellPid, SIGKILL) + } + let newTerminal = UtilityAreaTerminal( + id: UUID(), + url: url, + title: shell?.rawValue ?? "terminal", + shell: shell + ) + terminalGroups[index].terminals[idx] = newTerminal + TerminalCache.shared.removeCachedView(replacing) + selectedTerminals = [newTerminal.id] + break + } } + } + + func reorderTerminals(from source: IndexSet, to destination: Int) { + terminals.move(fromOffsets: source, toOffset: destination) + } - let id = UUID() - let url = terminals[index].url - let shell = terminals[index].shell - if let shellPid = TerminalCache.shared.getTerminalView(replacing)?.process.shellPid { - kill(shellPid, SIGKILL) + func moveTerminal(_ terminal: UtilityAreaTerminal, toGroup targetGroupID: UUID, at index: Int) { + for index in terminalGroups.indices { + terminalGroups[index].terminals.removeAll { $0.id == terminal.id } } + if let idx = terminalGroups.firstIndex(where: { $0.id == targetGroupID }) { + terminalGroups[idx].terminals.insert(terminal, at: index) + } + } - terminals[index] = UtilityAreaTerminal( - id: id, - url: url, - title: shell?.rawValue ?? "terminal", - shell: shell - ) - TerminalCache.shared.removeCachedView(replacing) + func createGroup(with terminals: [UtilityAreaTerminal]) { + terminalGroups.append(.init(name: "Group", terminals: terminals)) + } +} + +struct TerminalDropDelegate: DropDelegate { + let groupID: UUID + let viewModel: UtilityAreaViewModel + let destinationTerminalID: UUID? - selectedTerminals = [id] - return + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [UTType.terminal.identifier]) } - /// Reorders terminals in the ``utilityAreaViewModel``. - /// - Parameters: - /// - source: The source indices. - /// - destination: The destination indices. - func reorderTerminals(from source: IndexSet, to destination: Int) { - terminals.move(fromOffsets: source, toOffset: destination) + func dropEntered(info: DropInfo) { + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return } + + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data) else { return } + + DispatchQueue.main.async { + withAnimation { + viewModel.draggedTerminalID = dragInfo.terminalID + viewModel.dragOverTerminalID = destinationTerminalID + } + } + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.dragOverTerminalID = destinationTerminalID + } + } + + return .init(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false } + + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), + let terminal = viewModel.terminalGroups.flatMap({ + $0.terminals + }).first( where: { + $0.id == dragInfo.terminalID + }) else { return } + + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.finalizeMoveTerminal(terminal, toGroup: groupID, before: destinationTerminalID) + viewModel.dragOverTerminalID = nil + viewModel.draggedTerminalID = nil + } + } + } + return true + } +} + +struct NewGroupDropDelegate: DropDelegate { + let viewModel: UtilityAreaViewModel + + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [UTType.terminal.identifier]) + } + + func performDrop(info: DropInfo) -> Bool { + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false } + + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), + let terminal = viewModel.terminalGroups.flatMap({ $0.terminals }).first(where: { $0.id == dragInfo.terminalID }) else { + return + } + + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.finalizeMoveTerminal(terminal, toGroup: UUID(), before: nil) + viewModel.createGroup(with: [terminal]) + viewModel.dragOverTerminalID = nil + viewModel.draggedTerminalID = nil + } + } + } + return true } } diff --git a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift new file mode 100644 index 000000000..38918baa4 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift @@ -0,0 +1,70 @@ +// +// GroupTitleEditor.swift +// CodeEdit +// +// Created by Gustavo Soré on 28/06/25. +// + +import SwiftUI + +struct GroupTitleEditor: View { + let index: Int + let group: UtilityAreaTerminalGroup + let isEditing: Bool + @ObservedObject var viewModel: UtilityAreaViewModel + @FocusState private var isFocused: Bool + + var body: some View { + if isEditing { + TextField( + "", + text: Binding( + get: { + viewModel.terminalGroups[safe: index]?.name ?? "" + }, + set: { newValue in + if viewModel.terminalGroups.indices.contains(index) { + viewModel.terminalGroups[index].name = newValue + } + } + ), + onCommit: { + viewModel.editingGroupID = nil + } + ) + .font(.caption) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(Color.white) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + ) + .textFieldStyle(.plain) + .frame(maxWidth: .infinity) + .focused($isFocused) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + isFocused = true + } + } + .onChange(of: isFocused) { focused in + if !focused { + viewModel.editingGroupID = nil + } + } + } else { + Text(group.name.isEmpty ? "Grupo sem nome" : group.name) + .font(.caption) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture(count: 2).onEnded { + viewModel.editingGroupID = group.id + } + ) + } + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift new file mode 100644 index 000000000..d75d72bc4 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift @@ -0,0 +1,130 @@ +// +// UtilityAreaTerminalTab.swift +// CodeEdit +// +// Created by Gustavo Soré on 28/06/25. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct DoubleClickableText: View { + let text: String + let isSelected: Bool + let onDoubleClick: () -> Void + + var body: some View { + Text(text.isEmpty ? "Terminal sem nome" : text) + .lineLimit(1) + .truncationMode(.middle) + .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) + .foregroundColor(isSelected ? .white : .secondary) + .contentShape(Rectangle()) // garante que clique fora do texto funcione + .simultaneousGesture( + TapGesture(count: 2) + .onEnded { onDoubleClick() } + ) + } +} + +struct UtilityAreaTerminalTab: View { + let terminal: UtilityAreaTerminal + let removeTerminals: (Set) -> Void + @FocusState.Binding var focusedTerminalID: UUID? + + @EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel + @State private var isHovering = false + + var isSelected: Bool { + utilityAreaViewModel.selectedTerminals.contains(terminal.id) + } + + var body: some View { + HStack(spacing: 8) { + Image(systemName: terminal.shell?.iconName ?? "terminal") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(isSelected ? Color.white : Color.secondary) + .frame(width: 20, height: 20) + + terminalTitleView() + + Spacer() + + if isHovering { + Button { + removeTerminals([terminal.id]) + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.secondary) + .font(.system(size: 12)) + .padding(.trailing, 4) + } + .buttonStyle(.borderless) + } + } + .padding(.horizontal, 6) + .padding(.vertical, 4) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isSelected ? Color.blue : + utilityAreaViewModel.dragOverTerminalID == terminal.id ? Color.blue.opacity(0.15) : .clear) + ) + .contentShape(Rectangle()) + .onHover { hovering in + isHovering = hovering + } + .simultaneousGesture( + TapGesture(count: 1).onEnded { + utilityAreaViewModel.selectedTerminals = [terminal.id] + } + ) + .animation(.easeInOut(duration: 0.15), value: isHovering) + } + + @ViewBuilder + private func terminalTitleView() -> some View { + if utilityAreaViewModel.editingTerminalID == terminal.id { + TextField("", text: Binding( + get: { terminal.title }, + set: { newTitle in + guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return } + utilityAreaViewModel.updateTerminal(terminal.id, title: newTitle) + } + )) + .textFieldStyle(.plain) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(Color.white) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + ) + .focused($focusedTerminalID, equals: terminal.id) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + focusedTerminalID = terminal.id + } + } + .onSubmit { + utilityAreaViewModel.editingTerminalID = nil + focusedTerminalID = nil + } + .onChange(of: focusedTerminalID) { newValue in + if newValue != terminal.id { + utilityAreaViewModel.editingTerminalID = nil + } + } + } else { + DoubleClickableText( + text: terminal.title, + isSelected: isSelected + ) { + utilityAreaViewModel.editingTerminalID = terminal.id + focusedTerminalID = terminal.id + } + } + } +} diff --git a/CodeEdit/Info.plist b/CodeEdit/Info.plist index 962625428..9b12ec2a8 100644 --- a/CodeEdit/Info.plist +++ b/CodeEdit/Info.plist @@ -1288,5 +1288,25 @@ https://github.com/CodeEditApp/CodeEdit/releases/latest/download/appcast.xml SUPublicEDKey /vAnxnK9wj4IqnUt6wS9EN3Ug69zHb+S/Pb9CyZuwa0= + UTExportedTypeDeclarations + + + UTTypeIdentifier + dev.codeedit.terminal + UTTypeConformsTo + + public.data + + UTTypeDescription + Terminal Drag Info + UTTypeTagSpecification + + public.filename-extension + + term + + + + From 1a6437adeac3f9cd1597ffb0eb48348e425ed968 Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 18:49:12 -0300 Subject: [PATCH 02/25] Set group only when has more then 1 terminal. --- .../UtilityAreaTerminalSidebar.swift | 171 +++++++++++------- 1 file changed, 103 insertions(+), 68 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 9e3434613..f6853cbbb 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -34,86 +34,121 @@ struct UtilityAreaTerminalSidebar: View { let isEditing = utilityAreaViewModel.editingGroupID == group.id let isGroupSelected = group.terminals.contains { utilityAreaViewModel.selectedTerminals.contains($0.id) } let groupID = group.id - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 4) { - Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondary) - - GroupTitleEditor( - index: index, - group: group, - isEditing: isEditing, - viewModel: utilityAreaViewModel - ) - - Spacer() - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .contentShape(Rectangle()) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.25)) { - utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() + if group.terminals.count == 1 { + let terminal = group.terminals[0] + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: utilityAreaViewModel.removeTerminals, + focusedTerminalID: $focusedTerminalID + ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") } + return provider } + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + )) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) + } else { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 4) { + Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + + GroupTitleEditor( + index: index, + group: group, + isEditing: isEditing, + viewModel: utilityAreaViewModel + ) + + Spacer() + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.25)) { + utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() + } + } - if !group.isCollapsed { - VStack(spacing: 0) { - ForEach(group.terminals, id: \.id) { terminal in - VStack(spacing: 0) { - if utilityAreaViewModel.dragOverTerminalID == terminal.id { - InsertionIndicator() - } + if !group.isCollapsed { + VStack(spacing: 0) { + ForEach(group.terminals, id: \.id) { terminal in + VStack(spacing: 0) { + if utilityAreaViewModel.dragOverTerminalID == terminal.id { + InsertionIndicator() + } - UtilityAreaTerminalTab( - terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, - focusedTerminalID: $focusedTerminalID - ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: utilityAreaViewModel.removeTerminals, + focusedTerminalID: $focusedTerminalID + ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") + return provider } - return provider + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + )) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - )) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } } + .padding(.bottom, 8) + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: nil + )) } - .padding(.bottom, 8) - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: nil - )) } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) + ) + .cornerRadius(8) + .padding(.horizontal) } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) - ) - .cornerRadius(8) - .padding(.horizontal) } } .padding(.top) From c64f8d3ea3b35e180a59f999a77c27738f033589 Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 21:14:10 -0300 Subject: [PATCH 03/25] Organizing Terminal Views --- .../UtilityAreaTerminalSidebar.swift | 119 ++---------------- .../UtilityArea/Views/GroupTitleEditor.swift | 33 +++++ .../Views/Terminal/TerminalGroupView.swift | 102 +++++++++++++++ .../Views/Terminal/TerminalListView.swift | 77 ++++++++++++ .../Terminal/TerminalTabDragDropView.swift | 99 +++++++++++++++ .../Views/UtilityAreaTerminalTab.swift | 45 ++++++- 6 files changed, 364 insertions(+), 111 deletions(-) create mode 100644 CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift create mode 100644 CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift create mode 100644 CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index f6853cbbb..63ba38f72 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -31,123 +31,22 @@ struct UtilityAreaTerminalSidebar: View { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(Array(utilityAreaViewModel.terminalGroups.enumerated()), id: \.element.id) { index, group in - let isEditing = utilityAreaViewModel.editingGroupID == group.id - let isGroupSelected = group.terminals.contains { utilityAreaViewModel.selectedTerminals.contains($0.id) } - let groupID = group.id if group.terminals.count == 1 { let terminal = group.terminals[0] - UtilityAreaTerminalTab( + TerminalTabDragDropView( terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, + group: group, + viewModel: utilityAreaViewModel, focusedTerminalID: $focusedTerminalID ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") - } - return provider - } - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - )) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } else { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 4) { - Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondary) - - GroupTitleEditor( - index: index, - group: group, - isEditing: isEditing, - viewModel: utilityAreaViewModel - ) - - Spacer() - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .contentShape(Rectangle()) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.25)) { - utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() - } - } - - if !group.isCollapsed { - VStack(spacing: 0) { - ForEach(group.terminals, id: \.id) { terminal in - VStack(spacing: 0) { - if utilityAreaViewModel.dragOverTerminalID == terminal.id { - InsertionIndicator() - } - - UtilityAreaTerminalTab( - terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, - focusedTerminalID: $focusedTerminalID - ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") - } - return provider - } - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - )) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) - } - } - } - .padding(.bottom, 8) - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: nil - )) - } - } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) + TerminalGroupView( + index: index, + group: group, + isGroupSelected: true, + utilityAreaViewModel: utilityAreaViewModel, + focusedTerminalID: $focusedTerminalID ) - .cornerRadius(8) - .padding(.horizontal) } } } diff --git a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift index 38918baa4..e732814f4 100644 --- a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift +++ b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift @@ -68,3 +68,36 @@ struct GroupTitleEditor: View { } } } + +#Preview { + GroupTitleEditorPreviewWrapper() +} + +private struct GroupTitleEditorPreviewWrapper: View { + @StateObject private var viewModel = UtilityAreaViewModel() + @FocusState private var dummyFocus: Bool + + private let group = UtilityAreaTerminalGroup( + id: UUID(), + name: "Grupo de Preview", + terminals: [] + ) + + var body: some View { + GroupTitleEditor( + index: 0, + group: group, + isEditing: viewModel.editingGroupID == group.id, + viewModel: viewModel + ) + .environmentObject(viewModel) + .padding() + .frame(width: 280) + .onAppear { + if viewModel.terminalGroups.isEmpty { + viewModel.terminalGroups = [group] + viewModel.editingGroupID = nil + } + } + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift new file mode 100644 index 000000000..ae74733be --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift @@ -0,0 +1,102 @@ +// +// TerminalGroupView.swift +// CodeEdit +// +// Created by Gustavo Soré on 29/06/25. +// + +import SwiftUI + +struct TerminalGroupView: View { + let index: Int + let group: UtilityAreaTerminalGroup + let isGroupSelected: Bool + @ObservedObject var utilityAreaViewModel: UtilityAreaViewModel + @FocusState.Binding var focusedTerminalID: UUID? + + var body: some View { + let isEditing = utilityAreaViewModel.editingGroupID == group.id + + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 4) { + Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + + GroupTitleEditor( + index: index, + group: group, + isEditing: isEditing, + viewModel: utilityAreaViewModel + ) + + Spacer() + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.25)) { + utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() + } + } + + if !group.isCollapsed { + TerminalListView( + group: group, + utilityAreaViewModel: utilityAreaViewModel, + focusedTerminalID: $focusedTerminalID + ) + } + } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) + ) + .cornerRadius(8) + .padding(.horizontal) + } +} + +struct TerminalGroupViewPreviews: PreviewProvider { + static var previews: some View { + let terminal = UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal Preview", + shell: .zsh + ) + + let utilityAreaViewModel = UtilityAreaViewModel() + utilityAreaViewModel.terminalGroups = [ + UtilityAreaTerminalGroup(name: "Grupo de Preview", terminals: [terminal]) + ] + utilityAreaViewModel.selectedTerminals = [terminal.id] + + let tabViewModel = UtilityAreaTabViewModel() + + let workspace = WorkspaceDocument() + workspace.setValue(URL(string: "file:///mock/path")!, forKey: "fileURL") + + return TerminalGroupViewPreviewWrapper() + .environmentObject(utilityAreaViewModel) + .environmentObject(tabViewModel) + .environmentObject(workspace) + .frame(width: 320) + } +} + +private struct TerminalGroupViewPreviewWrapper: View { + @EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel + @FocusState private var focusedTerminalID: UUID? + + var body: some View { + TerminalGroupView( + index: 0, + group: utilityAreaViewModel.terminalGroups[0], + isGroupSelected: true, + utilityAreaViewModel: utilityAreaViewModel, + focusedTerminalID: $focusedTerminalID + ) + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift new file mode 100644 index 000000000..ecec65834 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift @@ -0,0 +1,77 @@ +// +// TerminalListView.swift +// CodeEdit +// +// Created by Gustavo Soré on 29/06/25. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct TerminalListView: View { + let group: UtilityAreaTerminalGroup + @ObservedObject var utilityAreaViewModel: UtilityAreaViewModel + @FocusState.Binding var focusedTerminalID: UUID? + + var body: some View { + VStack(spacing: 0) { + ForEach(group.terminals, id: \.id) { terminal in + VStack(spacing: 0) { + TerminalTabDragDropView( + terminal: terminal, + group: group, + viewModel: utilityAreaViewModel, + focusedTerminalID: $focusedTerminalID + ) + } + } + } + .padding(.bottom, 8) + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: nil + )) + } +} + +#Preview { + TerminalListViewPreviewWrapper() +} + +private struct TerminalListViewPreviewWrapper: View { + @StateObject private var viewModel = UtilityAreaViewModel() + @FocusState private var focusedTerminalID: UUID? + + private let mockGroup: UtilityAreaTerminalGroup = { + let terminal1 = UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/Users/preview/mock1"), + title: "Terminal 1", + shell: .zsh + ) + + let terminal2 = UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/Users/preview/mock2"), + title: "Terminal 2", + shell: .zsh + ) + + return UtilityAreaTerminalGroup( + id: UUID(), + name: "Preview Group", + terminals: [terminal1, terminal2] + ) + }() + + var body: some View { + TerminalListView( + group: mockGroup, + utilityAreaViewModel: viewModel, + focusedTerminalID: $focusedTerminalID + ) + .environmentObject(viewModel) + .frame(width: 300) + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift new file mode 100644 index 000000000..0b0970cd8 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift @@ -0,0 +1,99 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct TerminalTabDragDropView: View { + let terminal: UtilityAreaTerminal + let group: UtilityAreaTerminalGroup + @ObservedObject var viewModel: UtilityAreaViewModel + @FocusState.Binding var focusedTerminalID: UUID? + + var body: some View { + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: viewModel.removeTerminals, + focusedTerminalID: $focusedTerminalID + ) + .onDrag { + viewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } + .onDrop( + of: [UTType.terminal.identifier], + delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: viewModel, + destinationTerminalID: terminal.id + ) + ) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) + } +} + +#Preview { + TerminalTabDragDropViewPreviewWrapper() +} + +private struct TerminalTabDragDropViewPreviewWrapper: View { + @StateObject private var viewModel = UtilityAreaViewModel() + @StateObject private var tabViewModel = UtilityAreaTabViewModel() + @FocusState private var focusedTerminalID: UUID? + + private var terminal: UtilityAreaTerminal { + UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal 1", + shell: .zsh + ) + } + + private var group: UtilityAreaTerminalGroup { + UtilityAreaTerminalGroup( + id: UUID(), + name: "Grupo de Preview", + terminals: [ + UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal 1", + shell: .zsh + ), + UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal 1", + shell: .zsh + ) + ] + ) + } + + var body: some View { + TerminalTabDragDropView( + terminal: terminal, + group: group, + viewModel: viewModel, + focusedTerminalID: $focusedTerminalID + ) + .environmentObject(viewModel) + .environmentObject(tabViewModel) + .environmentObject(WorkspaceDocument()) + .frame(width: 300) + } +} diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift index d75d72bc4..a1a83835a 100644 --- a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift +++ b/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift @@ -63,7 +63,7 @@ struct UtilityAreaTerminalTab: View { } } .padding(.horizontal, 6) - .padding(.vertical, 4) + .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 6) .fill(isSelected ? Color.blue : @@ -128,3 +128,46 @@ struct UtilityAreaTerminalTab: View { } } } + +#Preview { + UtilityAreaTerminalTabPreviewWrapper() +} + +private struct UtilityAreaTerminalTabPreviewWrapper: View { + @StateObject private var viewModel = UtilityAreaViewModel() + @StateObject private var tabViewModel = UtilityAreaTabViewModel() + @FocusState private var focusedTerminalID: UUID? + + private let terminal = UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal Preview", + shell: .zsh + ) + + private let workspace: WorkspaceDocument = { + let workspace = WorkspaceDocument() + workspace.setValue(URL(string: "file:///mock/path")!, forKey: "fileURL") + return workspace + }() + + init() { + viewModel.terminalGroups = [ + UtilityAreaTerminalGroup(name: "Preview Group", terminals: [terminal]) + ] + viewModel.selectedTerminals = [terminal.id] + } + + var body: some View { + UtilityAreaTerminalTab( + terminal: terminal, + removeTerminals: { _ in }, + focusedTerminalID: $focusedTerminalID + ) + .environmentObject(viewModel) + .environmentObject(tabViewModel) + .environmentObject(workspace) + .frame(width: 280) + .padding() + } +} From 3502405fe3a4507a37d607f469ec8a5fce819b1f Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 21:41:56 -0300 Subject: [PATCH 04/25] Improving Terminal Views --- .../UtilityAreaTerminalSidebar.swift | 2 -- .../ViewModels/UtilityAreaViewModel.swift | 3 ++- .../UtilityArea/Views/GroupTitleEditor.swift | 17 ++++++++--------- .../Views/Terminal/TerminalGroupView.swift | 18 +++++++++++------- .../Views/Terminal/TerminalListView.swift | 4 +--- .../Terminal/TerminalTabDragDropView.swift | 9 ++++----- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index 63ba38f72..fb6dc4fed 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -36,7 +36,6 @@ struct UtilityAreaTerminalSidebar: View { TerminalTabDragDropView( terminal: terminal, group: group, - viewModel: utilityAreaViewModel, focusedTerminalID: $focusedTerminalID ) } else { @@ -44,7 +43,6 @@ struct UtilityAreaTerminalSidebar: View { index: index, group: group, isGroupSelected: true, - utilityAreaViewModel: utilityAreaViewModel, focusedTerminalID: $focusedTerminalID ) } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 36c678505..e8b520fcb 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -56,7 +56,8 @@ class UtilityAreaViewModel: ObservableObject { @Published var tabViewModel = UtilityAreaTabViewModel() @Published var editingGroupID: UUID? = nil - + @FocusState private var focusedTerminalID: UUID? + // MARK: - Drag Support func previewMoveTerminal(_ terminalID: UUID, toGroup groupID: UUID, before destinationID: UUID?) { diff --git a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift index e732814f4..f9892dd6a 100644 --- a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift +++ b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift @@ -11,7 +11,7 @@ struct GroupTitleEditor: View { let index: Int let group: UtilityAreaTerminalGroup let isEditing: Bool - @ObservedObject var viewModel: UtilityAreaViewModel + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState private var isFocused: Bool var body: some View { @@ -20,16 +20,16 @@ struct GroupTitleEditor: View { "", text: Binding( get: { - viewModel.terminalGroups[safe: index]?.name ?? "" + utilityAreaViewModel.terminalGroups[safe: index]?.name ?? "" }, set: { newValue in - if viewModel.terminalGroups.indices.contains(index) { - viewModel.terminalGroups[index].name = newValue + if utilityAreaViewModel.terminalGroups.indices.contains(index) { + utilityAreaViewModel.terminalGroups[index].name = newValue } } ), onCommit: { - viewModel.editingGroupID = nil + utilityAreaViewModel.editingGroupID = nil } ) .font(.caption) @@ -51,7 +51,7 @@ struct GroupTitleEditor: View { } .onChange(of: isFocused) { focused in if !focused { - viewModel.editingGroupID = nil + utilityAreaViewModel.editingGroupID = nil } } } else { @@ -62,7 +62,7 @@ struct GroupTitleEditor: View { .contentShape(Rectangle()) .simultaneousGesture( TapGesture(count: 2).onEnded { - viewModel.editingGroupID = group.id + utilityAreaViewModel.editingGroupID = group.id } ) } @@ -87,8 +87,7 @@ private struct GroupTitleEditorPreviewWrapper: View { GroupTitleEditor( index: 0, group: group, - isEditing: viewModel.editingGroupID == group.id, - viewModel: viewModel + isEditing: viewModel.editingGroupID == group.id ) .environmentObject(viewModel) .padding() diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift index ae74733be..2ce2ad612 100644 --- a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift @@ -11,7 +11,7 @@ struct TerminalGroupView: View { let index: Int let group: UtilityAreaTerminalGroup let isGroupSelected: Bool - @ObservedObject var utilityAreaViewModel: UtilityAreaViewModel + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState.Binding var focusedTerminalID: UUID? var body: some View { @@ -26,8 +26,7 @@ struct TerminalGroupView: View { GroupTitleEditor( index: index, group: group, - isEditing: isEditing, - viewModel: utilityAreaViewModel + isEditing: isEditing ) Spacer() @@ -44,7 +43,6 @@ struct TerminalGroupView: View { if !group.isCollapsed { TerminalListView( group: group, - utilityAreaViewModel: utilityAreaViewModel, focusedTerminalID: $focusedTerminalID ) } @@ -66,10 +64,17 @@ struct TerminalGroupViewPreviews: PreviewProvider { title: "Terminal Preview", shell: .zsh ) + + let terminal2 = UtilityAreaTerminal( + id: UUID(), + url: URL(fileURLWithPath: "/mock"), + title: "Terminal Preview", + shell: .zsh + ) let utilityAreaViewModel = UtilityAreaViewModel() utilityAreaViewModel.terminalGroups = [ - UtilityAreaTerminalGroup(name: "Grupo de Preview", terminals: [terminal]) + UtilityAreaTerminalGroup(name: "Grupo de Preview", terminals: [terminal, terminal2]) ] utilityAreaViewModel.selectedTerminals = [terminal.id] @@ -94,8 +99,7 @@ private struct TerminalGroupViewPreviewWrapper: View { TerminalGroupView( index: 0, group: utilityAreaViewModel.terminalGroups[0], - isGroupSelected: true, - utilityAreaViewModel: utilityAreaViewModel, + isGroupSelected: false, focusedTerminalID: $focusedTerminalID ) } diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift index ecec65834..ebc183935 100644 --- a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift @@ -10,7 +10,7 @@ import UniformTypeIdentifiers struct TerminalListView: View { let group: UtilityAreaTerminalGroup - @ObservedObject var utilityAreaViewModel: UtilityAreaViewModel + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState.Binding var focusedTerminalID: UUID? var body: some View { @@ -20,7 +20,6 @@ struct TerminalListView: View { TerminalTabDragDropView( terminal: terminal, group: group, - viewModel: utilityAreaViewModel, focusedTerminalID: $focusedTerminalID ) } @@ -68,7 +67,6 @@ private struct TerminalListViewPreviewWrapper: View { var body: some View { TerminalListView( group: mockGroup, - utilityAreaViewModel: viewModel, focusedTerminalID: $focusedTerminalID ) .environmentObject(viewModel) diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift index 0b0970cd8..d56d1c583 100644 --- a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift +++ b/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift @@ -4,17 +4,17 @@ import UniformTypeIdentifiers struct TerminalTabDragDropView: View { let terminal: UtilityAreaTerminal let group: UtilityAreaTerminalGroup - @ObservedObject var viewModel: UtilityAreaViewModel + @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState.Binding var focusedTerminalID: UUID? var body: some View { UtilityAreaTerminalTab( terminal: terminal, - removeTerminals: viewModel.removeTerminals, + removeTerminals: utilityAreaViewModel.removeTerminals, focusedTerminalID: $focusedTerminalID ) .onDrag { - viewModel.draggedTerminalID = terminal.id + utilityAreaViewModel.draggedTerminalID = terminal.id let dragInfo = TerminalDragInfo(terminalID: terminal.id) let provider = NSItemProvider() @@ -36,7 +36,7 @@ struct TerminalTabDragDropView: View { of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( groupID: group.id, - viewModel: viewModel, + viewModel: utilityAreaViewModel, destinationTerminalID: terminal.id ) ) @@ -88,7 +88,6 @@ private struct TerminalTabDragDropViewPreviewWrapper: View { TerminalTabDragDropView( terminal: terminal, group: group, - viewModel: viewModel, focusedTerminalID: $focusedTerminalID ) .environmentObject(viewModel) From 04b9e44fb59e27eff6b8fc4fa18ed3ae186b0cc7 Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 21:43:53 -0300 Subject: [PATCH 05/25] Improving Terminal Views --- .../{Views/Terminal => TerminalUtility}/TerminalGroupView.swift | 0 .../{Views/Terminal => TerminalUtility}/TerminalListView.swift | 0 .../Terminal => TerminalUtility}/TerminalTabDragDropView.swift | 0 .../{Views => TerminalUtility}/UtilityAreaTerminalTab.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename CodeEdit/Features/UtilityArea/{Views/Terminal => TerminalUtility}/TerminalGroupView.swift (100%) rename CodeEdit/Features/UtilityArea/{Views/Terminal => TerminalUtility}/TerminalListView.swift (100%) rename CodeEdit/Features/UtilityArea/{Views/Terminal => TerminalUtility}/TerminalTabDragDropView.swift (100%) rename CodeEdit/Features/UtilityArea/{Views => TerminalUtility}/UtilityAreaTerminalTab.swift (100%) diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalGroupView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/Terminal/TerminalGroupView.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/TerminalGroupView.swift diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/Terminal/TerminalListView.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift diff --git a/CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/Terminal/TerminalTabDragDropView.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift diff --git a/CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift similarity index 100% rename from CodeEdit/Features/UtilityArea/Views/UtilityAreaTerminalTab.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift From 4583d6fbf922af6c7da94a4d9cb962c5e8d69376 Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 23:17:53 -0300 Subject: [PATCH 06/25] Improving Terminal Views --- .../TerminalUtility/TerminalListView.swift | 34 ++++++- .../TerminalTabDragDropView.swift | 98 ------------------- ...swift => UtilityAreaTerminalRowView.swift} | 13 ++- .../UtilityAreaTerminalSidebar.swift | 32 +++++- .../ViewModels/UtilityAreaViewModel.swift | 9 -- 5 files changed, 67 insertions(+), 119 deletions(-) delete mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift rename CodeEdit/Features/UtilityArea/TerminalUtility/{UtilityAreaTerminalTab.swift => UtilityAreaTerminalRowView.swift} (94%) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift index ebc183935..c9e195b18 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift @@ -9,7 +9,7 @@ import SwiftUI import UniformTypeIdentifiers struct TerminalListView: View { - let group: UtilityAreaTerminalGroup + @State var group: UtilityAreaTerminalGroup @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState.Binding var focusedTerminalID: UUID? @@ -17,11 +17,39 @@ struct TerminalListView: View { VStack(spacing: 0) { ForEach(group.terminals, id: \.id) { terminal in VStack(spacing: 0) { - TerminalTabDragDropView( + UtilityAreaTerminalRowView( terminal: terminal, - group: group, focusedTerminalID: $focusedTerminalID ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } + .onDrop( + of: [UTType.terminal.identifier], + delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + ) + ) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift deleted file mode 100644 index d56d1c583..000000000 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalTabDragDropView.swift +++ /dev/null @@ -1,98 +0,0 @@ -import SwiftUI -import UniformTypeIdentifiers - -struct TerminalTabDragDropView: View { - let terminal: UtilityAreaTerminal - let group: UtilityAreaTerminalGroup - @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - @FocusState.Binding var focusedTerminalID: UUID? - - var body: some View { - UtilityAreaTerminalTab( - terminal: terminal, - removeTerminals: utilityAreaViewModel.removeTerminals, - focusedTerminalID: $focusedTerminalID - ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") - } - return provider - } - .onDrop( - of: [UTType.terminal.identifier], - delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - ) - ) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) - } -} - -#Preview { - TerminalTabDragDropViewPreviewWrapper() -} - -private struct TerminalTabDragDropViewPreviewWrapper: View { - @StateObject private var viewModel = UtilityAreaViewModel() - @StateObject private var tabViewModel = UtilityAreaTabViewModel() - @FocusState private var focusedTerminalID: UUID? - - private var terminal: UtilityAreaTerminal { - UtilityAreaTerminal( - id: UUID(), - url: URL(fileURLWithPath: "/mock"), - title: "Terminal 1", - shell: .zsh - ) - } - - private var group: UtilityAreaTerminalGroup { - UtilityAreaTerminalGroup( - id: UUID(), - name: "Grupo de Preview", - terminals: [ - UtilityAreaTerminal( - id: UUID(), - url: URL(fileURLWithPath: "/mock"), - title: "Terminal 1", - shell: .zsh - ), - UtilityAreaTerminal( - id: UUID(), - url: URL(fileURLWithPath: "/mock"), - title: "Terminal 1", - shell: .zsh - ) - ] - ) - } - - var body: some View { - TerminalTabDragDropView( - terminal: terminal, - group: group, - focusedTerminalID: $focusedTerminalID - ) - .environmentObject(viewModel) - .environmentObject(tabViewModel) - .environmentObject(WorkspaceDocument()) - .frame(width: 300) - } -} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift similarity index 94% rename from CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index a1a83835a..73943ac05 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalTab.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -1,5 +1,5 @@ // -// UtilityAreaTerminalTab.swift +// UtilityAreaTerminalRowView.swift // CodeEdit // // Created by Gustavo Soré on 28/06/25. @@ -27,9 +27,8 @@ struct DoubleClickableText: View { } } -struct UtilityAreaTerminalTab: View { +struct UtilityAreaTerminalRowView: View { let terminal: UtilityAreaTerminal - let removeTerminals: (Set) -> Void @FocusState.Binding var focusedTerminalID: UUID? @EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel @@ -40,8 +39,9 @@ struct UtilityAreaTerminalTab: View { } var body: some View { + HStack(spacing: 8) { - Image(systemName: terminal.shell?.iconName ?? "terminal") + Image(systemName: "terminal") .font(.system(size: 14, weight: .medium)) .foregroundStyle(isSelected ? Color.white : Color.secondary) .frame(width: 20, height: 20) @@ -52,7 +52,7 @@ struct UtilityAreaTerminalTab: View { if isHovering { Button { - removeTerminals([terminal.id]) + utilityAreaViewModel.removeTerminals([terminal.id]) } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.secondary) @@ -159,9 +159,8 @@ private struct UtilityAreaTerminalTabPreviewWrapper: View { } var body: some View { - UtilityAreaTerminalTab( + UtilityAreaTerminalRowView( terminal: terminal, - removeTerminals: { _ in }, focusedTerminalID: $focusedTerminalID ) .environmentObject(viewModel) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index fb6dc4fed..cae407dbe 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -33,11 +33,39 @@ struct UtilityAreaTerminalSidebar: View { ForEach(Array(utilityAreaViewModel.terminalGroups.enumerated()), id: \.element.id) { index, group in if group.terminals.count == 1 { let terminal = group.terminals[0] - TerminalTabDragDropView( + UtilityAreaTerminalRowView( terminal: terminal, - group: group, focusedTerminalID: $focusedTerminalID ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } + .onDrop( + of: [UTType.terminal.identifier], + delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + ) + ) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } else { TerminalGroupView( index: index, diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index e8b520fcb..57dfb4875 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -4,15 +4,6 @@ import SwiftUI import UniformTypeIdentifiers -extension Shell { - var iconName: String { - switch self { - case .bash: return "terminal" - case .zsh: return "circle.lefthalf.filled" - } - } -} - struct UtilityAreaTerminalGroup: Identifiable, Hashable { var id = UUID() var name: String = "Grupo" From 7a9609ef2c3e2edfaae7c2f09372bc2ec94c9c39 Mon Sep 17 00:00:00 2001 From: Sore Date: Sun, 29 Jun 2025 23:56:35 -0300 Subject: [PATCH 07/25] Improving Terminal Views --- .../TerminalUtility/TerminalListView.swift | 103 ------------------ ...ift => UtilityAreaTerminalGroupView.swift} | 61 +++++++++-- .../UtilityAreaTerminalRowView.swift | 2 +- .../UtilityAreaTerminalSidebar.swift | 5 +- 4 files changed, 54 insertions(+), 117 deletions(-) delete mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift rename CodeEdit/Features/UtilityArea/TerminalUtility/{TerminalGroupView.swift => UtilityAreaTerminalGroupView.swift} (52%) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift deleted file mode 100644 index c9e195b18..000000000 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalListView.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// TerminalListView.swift -// CodeEdit -// -// Created by Gustavo Soré on 29/06/25. -// - -import SwiftUI -import UniformTypeIdentifiers - -struct TerminalListView: View { - @State var group: UtilityAreaTerminalGroup - @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - @FocusState.Binding var focusedTerminalID: UUID? - - var body: some View { - VStack(spacing: 0) { - ForEach(group.terminals, id: \.id) { terminal in - VStack(spacing: 0) { - UtilityAreaTerminalRowView( - terminal: terminal, - focusedTerminalID: $focusedTerminalID - ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") - } - return provider - } - .onDrop( - of: [UTType.terminal.identifier], - delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - ) - ) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) - } - } - } - .padding(.bottom, 8) - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: nil - )) - } -} - -#Preview { - TerminalListViewPreviewWrapper() -} - -private struct TerminalListViewPreviewWrapper: View { - @StateObject private var viewModel = UtilityAreaViewModel() - @FocusState private var focusedTerminalID: UUID? - - private let mockGroup: UtilityAreaTerminalGroup = { - let terminal1 = UtilityAreaTerminal( - id: UUID(), - url: URL(fileURLWithPath: "/Users/preview/mock1"), - title: "Terminal 1", - shell: .zsh - ) - - let terminal2 = UtilityAreaTerminal( - id: UUID(), - url: URL(fileURLWithPath: "/Users/preview/mock2"), - title: "Terminal 2", - shell: .zsh - ) - - return UtilityAreaTerminalGroup( - id: UUID(), - name: "Preview Group", - terminals: [terminal1, terminal2] - ) - }() - - var body: some View { - TerminalListView( - group: mockGroup, - focusedTerminalID: $focusedTerminalID - ) - .environmentObject(viewModel) - .frame(width: 300) - } -} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift similarity index 52% rename from CodeEdit/Features/UtilityArea/TerminalUtility/TerminalGroupView.swift rename to CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 2ce2ad612..01e2cd3a7 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/TerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -1,18 +1,19 @@ // -// TerminalGroupView.swift +// UtilityAreaTerminalGroupView.swift // CodeEdit // // Created by Gustavo Soré on 29/06/25. // import SwiftUI +import UniformTypeIdentifiers -struct TerminalGroupView: View { +struct UtilityAreaTerminalGroupView: View { let index: Int let group: UtilityAreaTerminalGroup let isGroupSelected: Bool @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - @FocusState.Binding var focusedTerminalID: UUID? + @FocusState private var focusedTerminalID: UUID? var body: some View { let isEditing = utilityAreaViewModel.editingGroupID == group.id @@ -41,10 +42,51 @@ struct TerminalGroupView: View { } if !group.isCollapsed { - TerminalListView( - group: group, - focusedTerminalID: $focusedTerminalID - ) + VStack(spacing: 0) { + ForEach(group.terminals, id: \.id) { terminal in + VStack(spacing: 0) { + UtilityAreaTerminalRowView( + terminal: terminal, + focusedTerminalID: $focusedTerminalID + ) + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } + .onDrop( + of: [UTType.terminal.identifier], + delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + ) + ) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) + } + } + } + .padding(.bottom, 8) + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: focusedTerminalID + )) } } .background( @@ -96,11 +138,10 @@ private struct TerminalGroupViewPreviewWrapper: View { @FocusState private var focusedTerminalID: UUID? var body: some View { - TerminalGroupView( + UtilityAreaTerminalGroupView( index: 0, group: utilityAreaViewModel.terminalGroups[0], - isGroupSelected: false, - focusedTerminalID: $focusedTerminalID + isGroupSelected: false ) } } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index 73943ac05..7a41a4533 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -80,7 +80,7 @@ struct UtilityAreaTerminalRowView: View { ) .animation(.easeInOut(duration: 0.15), value: isHovering) } - + @ViewBuilder private func terminalTitleView() -> some View { if utilityAreaViewModel.editingTerminalID == terminal.id { diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index cae407dbe..a831ef3ae 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -67,11 +67,10 @@ struct UtilityAreaTerminalSidebar: View { .transition(.opacity.combined(with: .move(edge: .top))) .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } else { - TerminalGroupView( + UtilityAreaTerminalGroupView( index: index, group: group, - isGroupSelected: true, - focusedTerminalID: $focusedTerminalID + isGroupSelected: true ) } } From 06158f1ee1f807110ff0db4dccb4df5244509097 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 00:11:53 -0300 Subject: [PATCH 08/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 01e2cd3a7..0ad726445 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -49,44 +49,44 @@ struct UtilityAreaTerminalGroupView: View { terminal: terminal, focusedTerminalID: $focusedTerminalID ) - .onDrag { - utilityAreaViewModel.draggedTerminalID = terminal.id - - let dragInfo = TerminalDragInfo(terminalID: terminal.id) - let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") - } - return provider - } - .onDrop( - of: [UTType.terminal.identifier], - delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: terminal.id - ) - ) - .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) +// .onDrag { +// utilityAreaViewModel.draggedTerminalID = terminal.id +// +// let dragInfo = TerminalDragInfo(terminalID: terminal.id) +// let provider = NSItemProvider() +// do { +// let data = try JSONEncoder().encode(dragInfo) +// provider.registerDataRepresentation( +// forTypeIdentifier: UTType.terminal.identifier, +// visibility: .all +// ) { completion in +// completion(data, nil) +// return nil +// } +// } catch { +// print("❌ Erro ao codificar dragInfo: \(error)") +// } +// return provider +// } +// .onDrop( +// of: [UTType.terminal.identifier], +// delegate: TerminalDropDelegate( +// groupID: group.id, +// viewModel: utilityAreaViewModel, +// destinationTerminalID: terminal.id +// ) +// ) +// .transition(.opacity.combined(with: .move(edge: .top))) +// .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } } } .padding(.bottom, 8) - .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, - viewModel: utilityAreaViewModel, - destinationTerminalID: focusedTerminalID - )) +// .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( +// groupID: group.id, +// viewModel: utilityAreaViewModel, +// destinationTerminalID: focusedTerminalID +// )) } } .background( From d6f1978043d581f386fe871d1219d896050946b3 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 00:36:07 -0300 Subject: [PATCH 09/25] Improving Terminal Views --- .../UtilityAreaTerminalSidebar.swift | 7 ++++++- .../ViewModels/UtilityAreaViewModel.swift | 15 +++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index a831ef3ae..e35d11f12 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -77,7 +77,12 @@ struct UtilityAreaTerminalSidebar: View { } .padding(.top) } - .onDrop(of: [UTType.terminal.identifier], delegate: NewGroupDropDelegate(viewModel: utilityAreaViewModel)) + .onDrop( + of: [UTType.terminal.identifier], + delegate: NewGroupDropDelegate( + viewModel: utilityAreaViewModel + ) + ) .background(Color(NSColor.controlBackgroundColor)) .contextMenu { Button("New Terminal") { diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 57dfb4875..c1b7a4c81 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -35,20 +35,15 @@ class UtilityAreaViewModel: ObservableObject { @Published var selectedTerminals: Set = [] @Published var dragOverTerminalID: UUID? = nil @Published var draggedTerminalID: UUID? = nil - @Published var isCollapsed: Bool = false @Published var animateCollapse: Bool = true @Published var isMaximized: Bool = false @Published var currentHeight: Double = 0 - @Published var editingTerminalID: UUID? = nil - @Published var tabItems: [UtilityAreaTab] = UtilityAreaTab.allCases @Published var tabViewModel = UtilityAreaTabViewModel() - @Published var editingGroupID: UUID? = nil @FocusState private var focusedTerminalID: UUID? - // MARK: - Drag Support func previewMoveTerminal(_ terminalID: UUID, toGroup groupID: UUID, before destinationID: UUID?) { @@ -83,7 +78,15 @@ class UtilityAreaViewModel: ObservableObject { } func finalizeMoveTerminal(_ terminal: UtilityAreaTerminal, toGroup groupID: UUID, before destinationID: UUID?) { - // Remove de qualquer grupo atual + + let alreadyInGroup = terminalGroups.contains { group in + group.id == groupID && + group.terminals.count == 1 && + group.terminals.first?.id == terminal.id + } + + guard !alreadyInGroup else { return } + for index in terminalGroups.indices { terminalGroups[index].terminals.removeAll { $0.id == terminal.id } } From 6c28a9322532486ae28b7a4e5fe4f93e436eef49 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 01:04:55 -0300 Subject: [PATCH 10/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 17 +++++++++-------- .../UtilityArea/Views/GroupTitleEditor.swift | 13 ++++--------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 0ad726445..0dd7a2b56 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -20,9 +20,10 @@ struct UtilityAreaTerminalGroupView: View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { - Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondary) + + Image(systemName: "square.on.square") + .font(.system(size: 14, weight: .medium)) + .frame(width: 20, height: 20) GroupTitleEditor( index: index, @@ -31,6 +32,10 @@ struct UtilityAreaTerminalGroupView: View { ) Spacer() + + Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) } .padding(.horizontal, 8) .padding(.vertical, 6) @@ -82,6 +87,7 @@ struct UtilityAreaTerminalGroupView: View { } } .padding(.bottom, 8) + .padding(.leading, 16) // .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( // groupID: group.id, // viewModel: utilityAreaViewModel, @@ -89,12 +95,7 @@ struct UtilityAreaTerminalGroupView: View { // )) } } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(isGroupSelected ? Color.accentColor.opacity(0.12) : Color.clear) - ) .cornerRadius(8) - .padding(.horizontal) } } diff --git a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift index f9892dd6a..98dfe5fd4 100644 --- a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift +++ b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift @@ -55,16 +55,11 @@ struct GroupTitleEditor: View { } } } else { - Text(group.name.isEmpty ? "Grupo sem nome" : group.name) - .font(.caption) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) + Text(group.name) + .foregroundStyle(.primary.opacity(0.7)) + .lineLimit(1) + .font(.headline) .contentShape(Rectangle()) - .simultaneousGesture( - TapGesture(count: 2).onEnded { - utilityAreaViewModel.editingGroupID = group.id - } - ) } } } From 09c95eecddfde1d179c74b280a11d9bb5423af59 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 01:21:40 -0300 Subject: [PATCH 11/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 63 ++++++------ .../UtilityAreaTerminalSidebar.swift | 1 - .../UtilityArea/Views/GroupTitleEditor.swift | 97 ------------------- 3 files changed, 30 insertions(+), 131 deletions(-) delete mode 100644 CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 0dd7a2b56..6bcd10139 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -10,14 +10,13 @@ import UniformTypeIdentifiers struct UtilityAreaTerminalGroupView: View { let index: Int - let group: UtilityAreaTerminalGroup let isGroupSelected: Bool @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel @FocusState private var focusedTerminalID: UUID? var body: some View { + let group = utilityAreaViewModel.terminalGroups[index] let isEditing = utilityAreaViewModel.editingGroupID == group.id - VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { @@ -25,11 +24,11 @@ struct UtilityAreaTerminalGroupView: View { .font(.system(size: 14, weight: .medium)) .frame(width: 20, height: 20) - GroupTitleEditor( - index: index, - group: group, - isEditing: isEditing - ) + Text(group.name) + .foregroundStyle(.primary.opacity(0.7)) + .lineLimit(1) + .font(.headline) + .contentShape(Rectangle()) Spacer() @@ -45,7 +44,6 @@ struct UtilityAreaTerminalGroupView: View { utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() } } - if !group.isCollapsed { VStack(spacing: 0) { ForEach(group.terminals, id: \.id) { terminal in @@ -54,25 +52,25 @@ struct UtilityAreaTerminalGroupView: View { terminal: terminal, focusedTerminalID: $focusedTerminalID ) -// .onDrag { -// utilityAreaViewModel.draggedTerminalID = terminal.id -// -// let dragInfo = TerminalDragInfo(terminalID: terminal.id) -// let provider = NSItemProvider() -// do { -// let data = try JSONEncoder().encode(dragInfo) -// provider.registerDataRepresentation( -// forTypeIdentifier: UTType.terminal.identifier, -// visibility: .all -// ) { completion in -// completion(data, nil) -// return nil -// } -// } catch { -// print("❌ Erro ao codificar dragInfo: \(error)") -// } -// return provider -// } + .onDrag { + utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: terminal.id) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } // .onDrop( // of: [UTType.terminal.identifier], // delegate: TerminalDropDelegate( @@ -88,11 +86,11 @@ struct UtilityAreaTerminalGroupView: View { } .padding(.bottom, 8) .padding(.leading, 16) -// .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( -// groupID: group.id, -// viewModel: utilityAreaViewModel, -// destinationTerminalID: focusedTerminalID -// )) + .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( + groupID: group.id, + viewModel: utilityAreaViewModel, + destinationTerminalID: focusedTerminalID + )) } } .cornerRadius(8) @@ -141,7 +139,6 @@ private struct TerminalGroupViewPreviewWrapper: View { var body: some View { UtilityAreaTerminalGroupView( index: 0, - group: utilityAreaViewModel.terminalGroups[0], isGroupSelected: false ) } diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index e35d11f12..cb023f47b 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -69,7 +69,6 @@ struct UtilityAreaTerminalSidebar: View { } else { UtilityAreaTerminalGroupView( index: index, - group: group, isGroupSelected: true ) } diff --git a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift b/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift deleted file mode 100644 index 98dfe5fd4..000000000 --- a/CodeEdit/Features/UtilityArea/Views/GroupTitleEditor.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// GroupTitleEditor.swift -// CodeEdit -// -// Created by Gustavo Soré on 28/06/25. -// - -import SwiftUI - -struct GroupTitleEditor: View { - let index: Int - let group: UtilityAreaTerminalGroup - let isEditing: Bool - @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel - @FocusState private var isFocused: Bool - - var body: some View { - if isEditing { - TextField( - "", - text: Binding( - get: { - utilityAreaViewModel.terminalGroups[safe: index]?.name ?? "" - }, - set: { newValue in - if utilityAreaViewModel.terminalGroups.indices.contains(index) { - utilityAreaViewModel.terminalGroups[index].name = newValue - } - } - ), - onCommit: { - utilityAreaViewModel.editingGroupID = nil - } - ) - .font(.caption) - .padding(.horizontal, 4) - .padding(.vertical, 2) - .background(Color.white) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray.opacity(0.3), lineWidth: 1) - ) - .textFieldStyle(.plain) - .frame(maxWidth: .infinity) - .focused($isFocused) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - isFocused = true - } - } - .onChange(of: isFocused) { focused in - if !focused { - utilityAreaViewModel.editingGroupID = nil - } - } - } else { - Text(group.name) - .foregroundStyle(.primary.opacity(0.7)) - .lineLimit(1) - .font(.headline) - .contentShape(Rectangle()) - } - } -} - -#Preview { - GroupTitleEditorPreviewWrapper() -} - -private struct GroupTitleEditorPreviewWrapper: View { - @StateObject private var viewModel = UtilityAreaViewModel() - @FocusState private var dummyFocus: Bool - - private let group = UtilityAreaTerminalGroup( - id: UUID(), - name: "Grupo de Preview", - terminals: [] - ) - - var body: some View { - GroupTitleEditor( - index: 0, - group: group, - isEditing: viewModel.editingGroupID == group.id - ) - .environmentObject(viewModel) - .padding() - .frame(width: 280) - .onAppear { - if viewModel.terminalGroups.isEmpty { - viewModel.terminalGroups = [group] - viewModel.editingGroupID = nil - } - } - } -} From b8f73b3f7d0d3c32790ad61839ddef5244007bfa Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 01:29:40 -0300 Subject: [PATCH 12/25] Improving Terminal Views --- .../TerminalUtility/UtilityAreaTerminalGroupView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 6bcd10139..692306bf4 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -23,12 +23,14 @@ struct UtilityAreaTerminalGroupView: View { Image(systemName: "square.on.square") .font(.system(size: 14, weight: .medium)) .frame(width: 20, height: 20) + .foregroundStyle(.primary.opacity(0.6)) Text(group.name) - .foregroundStyle(.primary.opacity(0.7)) - .lineLimit(1) - .font(.headline) + .foregroundStyle(.primary.opacity(0.6)) .contentShape(Rectangle()) + .lineLimit(1) + .truncationMode(.middle) + .font(.system(size: 13)) Spacer() From f4de22579edc92cbc1a23ffb7fc7d5fdd2490f46 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 01:59:57 -0300 Subject: [PATCH 13/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 692306bf4..070de3824 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -25,12 +25,47 @@ struct UtilityAreaTerminalGroupView: View { .frame(width: 20, height: 20) .foregroundStyle(.primary.opacity(0.6)) - Text(group.name) - .foregroundStyle(.primary.opacity(0.6)) - .contentShape(Rectangle()) - .lineLimit(1) - .truncationMode(.middle) - .font(.system(size: 13)) + if utilityAreaViewModel.editingGroupID == group.id { + TextField("", text: Binding( + get: { group.name }, + set: { newTitle in + guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return } +// utilityAreaViewModel.updateTerminal(terminal.id, title: newTitle) + } + )) + .textFieldStyle(.plain) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background(Color.white) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + ) +// .focused($focusedTerminalID, equals: terminal.id) + .onAppear { +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { +// focusedTerminalID = terminal.id +// } + } + .onSubmit { + utilityAreaViewModel.editingGroupID = nil + } +// .onChange(of: focusedTerminalID) { newValue in +// if newValue != terminal.id { +// utilityAreaViewModel.editingTerminalID = nil +// } +// } + } else { + DoubleClickableText( + text: group.name, + isSelected: isGroupSelected + ) { + utilityAreaViewModel.editingGroupID = group.id + } + } Spacer() @@ -107,7 +142,7 @@ struct TerminalGroupViewPreviews: PreviewProvider { title: "Terminal Preview", shell: .zsh ) - + let terminal2 = UtilityAreaTerminal( id: UUID(), url: URL(fileURLWithPath: "/mock"), From 37a589c2661c522b629520509130d1c60ebc7851 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 02:02:48 -0300 Subject: [PATCH 14/25] Improving Terminal Views --- .../TerminalUtility/UtilityAreaTerminalRowView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index 7a41a4533..4a0b68dfc 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -19,7 +19,7 @@ struct DoubleClickableText: View { .truncationMode(.middle) .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) .foregroundColor(isSelected ? .white : .secondary) - .contentShape(Rectangle()) // garante que clique fora do texto funcione + .contentShape(Rectangle()) .simultaneousGesture( TapGesture(count: 2) .onEnded { onDoubleClick() } From 9202ebac674baed50ed7af6c24234b5a62d040a6 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 02:32:04 -0300 Subject: [PATCH 15/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 45 ++++++++----------- .../UtilityAreaTerminalRowView.swift | 39 ++++++---------- .../ViewModels/UtilityAreaViewModel.swift | 5 ++- 3 files changed, 35 insertions(+), 54 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 070de3824..95950e8e8 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -15,8 +15,6 @@ struct UtilityAreaTerminalGroupView: View { @FocusState private var focusedTerminalID: UUID? var body: some View { - let group = utilityAreaViewModel.terminalGroups[index] - let isEditing = utilityAreaViewModel.editingGroupID == group.id VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { @@ -25,12 +23,12 @@ struct UtilityAreaTerminalGroupView: View { .frame(width: 20, height: 20) .foregroundStyle(.primary.opacity(0.6)) - if utilityAreaViewModel.editingGroupID == group.id { + if utilityAreaViewModel.editingGroupID == utilityAreaViewModel.terminalGroups[index].id { TextField("", text: Binding( - get: { group.name }, + get: { utilityAreaViewModel.terminalGroups[index].name }, set: { newTitle in guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return } -// utilityAreaViewModel.updateTerminal(terminal.id, title: newTitle) + utilityAreaViewModel.terminalGroups[index].name = newTitle } )) .textFieldStyle(.plain) @@ -44,32 +42,27 @@ struct UtilityAreaTerminalGroupView: View { RoundedRectangle(cornerRadius: 4) .stroke(Color.gray.opacity(0.3), lineWidth: 1) ) -// .focused($focusedTerminalID, equals: terminal.id) - .onAppear { -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { -// focusedTerminalID = terminal.id -// } - } .onSubmit { utilityAreaViewModel.editingGroupID = nil } -// .onChange(of: focusedTerminalID) { newValue in -// if newValue != terminal.id { -// utilityAreaViewModel.editingTerminalID = nil -// } -// } } else { - DoubleClickableText( - text: group.name, - isSelected: isGroupSelected - ) { - utilityAreaViewModel.editingGroupID = group.id - } + Text(utilityAreaViewModel.terminalGroups[index].name.isEmpty ? "terminals" : utilityAreaViewModel.terminalGroups[index].name) + .lineLimit(1) + .truncationMode(.middle) + .font(.system(size: 13, weight: .regular)) + .foregroundColor(.secondary) + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture(count: 2) + .onEnded { + utilityAreaViewModel.editingGroupID = utilityAreaViewModel.terminalGroups[index].id + } + ) } Spacer() - Image(systemName: group.isCollapsed ? "chevron.right" : "chevron.down") + Image(systemName: utilityAreaViewModel.terminalGroups[index].isCollapsed ? "chevron.right" : "chevron.down") .font(.system(size: 11, weight: .medium)) .foregroundColor(.secondary) } @@ -81,9 +74,9 @@ struct UtilityAreaTerminalGroupView: View { utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() } } - if !group.isCollapsed { + if !utilityAreaViewModel.terminalGroups[index].isCollapsed { VStack(spacing: 0) { - ForEach(group.terminals, id: \.id) { terminal in + ForEach(utilityAreaViewModel.terminalGroups[index].terminals, id: \.id) { terminal in VStack(spacing: 0) { UtilityAreaTerminalRowView( terminal: terminal, @@ -124,7 +117,7 @@ struct UtilityAreaTerminalGroupView: View { .padding(.bottom, 8) .padding(.leading, 16) .onDrop(of: [UTType.terminal.identifier], delegate: TerminalDropDelegate( - groupID: group.id, + groupID: utilityAreaViewModel.terminalGroups[index].id, viewModel: utilityAreaViewModel, destinationTerminalID: focusedTerminalID )) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index 4a0b68dfc..5cb220da1 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -8,25 +8,6 @@ import SwiftUI import UniformTypeIdentifiers -struct DoubleClickableText: View { - let text: String - let isSelected: Bool - let onDoubleClick: () -> Void - - var body: some View { - Text(text.isEmpty ? "Terminal sem nome" : text) - .lineLimit(1) - .truncationMode(.middle) - .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) - .foregroundColor(isSelected ? .white : .secondary) - .contentShape(Rectangle()) - .simultaneousGesture( - TapGesture(count: 2) - .onEnded { onDoubleClick() } - ) - } -} - struct UtilityAreaTerminalRowView: View { let terminal: UtilityAreaTerminal @FocusState.Binding var focusedTerminalID: UUID? @@ -118,13 +99,19 @@ struct UtilityAreaTerminalRowView: View { } } } else { - DoubleClickableText( - text: terminal.title, - isSelected: isSelected - ) { - utilityAreaViewModel.editingTerminalID = terminal.id - focusedTerminalID = terminal.id - } + Text(terminal.title.isEmpty ? "Terminal" : terminal.title) + .lineLimit(1) + .truncationMode(.middle) + .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) + .foregroundColor(isSelected ? .white : .secondary) + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture(count: 2) + .onEnded { + utilityAreaViewModel.editingTerminalID = terminal.id + focusedTerminalID = terminal.id + } + ) } } } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index c1b7a4c81..4e58a9878 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -189,8 +189,9 @@ class UtilityAreaViewModel: ObservableObject { if let groupID, let index = terminalGroups.firstIndex(where: { $0.id == groupID }) { terminalGroups[index].terminals.append(newTerminal) + terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" } else { - terminalGroups.append(.init(name: "New Group", terminals: [newTerminal])) + terminalGroups.append(.init(name: "2 Terminals", terminals: [newTerminal])) } selectedTerminals = [newTerminal.id] @@ -232,7 +233,7 @@ class UtilityAreaViewModel: ObservableObject { } func createGroup(with terminals: [UtilityAreaTerminal]) { - terminalGroups.append(.init(name: "Group", terminals: terminals)) + terminalGroups.append(.init(name: "\(terminalGroups.count) Terminals", terminals: terminals)) } } From 482440b5a22792a1f6c4301a444bf72e401c5172 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 03:00:45 -0300 Subject: [PATCH 16/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 1 + .../ViewModels/UtilityAreaViewModel.swift | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 95950e8e8..739aa4c56 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -29,6 +29,7 @@ struct UtilityAreaTerminalGroupView: View { set: { newTitle in guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return } utilityAreaViewModel.terminalGroups[index].name = newTitle + utilityAreaViewModel.terminalGroups[index].userName = true } )) .textFieldStyle(.plain) diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 4e58a9878..0ee8ce06f 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -9,6 +9,7 @@ struct UtilityAreaTerminalGroup: Identifiable, Hashable { var name: String = "Grupo" var terminals: [UtilityAreaTerminal] = [] var isCollapsed: Bool = false + var userName: Bool = false static func == (lhs: UtilityAreaTerminalGroup, rhs: UtilityAreaTerminalGroup) -> Bool { lhs.id == rhs.id @@ -79,6 +80,8 @@ class UtilityAreaViewModel: ObservableObject { func finalizeMoveTerminal(_ terminal: UtilityAreaTerminal, toGroup groupID: UUID, before destinationID: UUID?) { + print("finalizeMoveTerminal") + let alreadyInGroup = terminalGroups.contains { group in group.id == groupID && group.terminals.count == 1 && @@ -111,6 +114,12 @@ class UtilityAreaViewModel: ObservableObject { if !selectedTerminals.contains(terminal.id) { selectedTerminals = [terminal.id] } + + for index in terminalGroups.indices { + if !terminalGroups[index].userName { + terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" + } + } } private func removeTerminal(withID id: UUID) -> UtilityAreaTerminal? { @@ -180,6 +189,9 @@ class UtilityAreaViewModel: ObservableObject { } func addTerminal(to groupID: UUID? = nil, shell: Shell? = nil, rootURL: URL?) { + + print("Did add temrinal") + let newTerminal = UtilityAreaTerminal( id: UUID(), url: rootURL ?? URL(filePath: "~/"), From 4dea50eaba37707a893b61df1e7f4c838f09b340 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 03:16:47 -0300 Subject: [PATCH 17/25] Improving Terminal Views --- .../UtilityAreaTerminalGroupView.swift | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index 739aa4c56..ca930f2ab 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -75,6 +75,25 @@ struct UtilityAreaTerminalGroupView: View { utilityAreaViewModel.terminalGroups[index].isCollapsed.toggle() } } + .onDrag { +// utilityAreaViewModel.draggedTerminalID = terminal.id + + let dragInfo = TerminalDragInfo(terminalID: .init()) + let provider = NSItemProvider() + do { + let data = try JSONEncoder().encode(dragInfo) + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil + } + } catch { + print("❌ Erro ao codificar dragInfo: \(error)") + } + return provider + } if !utilityAreaViewModel.terminalGroups[index].isCollapsed { VStack(spacing: 0) { ForEach(utilityAreaViewModel.terminalGroups[index].terminals, id: \.id) { terminal in @@ -102,16 +121,16 @@ struct UtilityAreaTerminalGroupView: View { } return provider } -// .onDrop( -// of: [UTType.terminal.identifier], -// delegate: TerminalDropDelegate( -// groupID: group.id, -// viewModel: utilityAreaViewModel, -// destinationTerminalID: terminal.id -// ) -// ) -// .transition(.opacity.combined(with: .move(edge: .top))) -// .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) + .onDrop( + of: [UTType.terminal.identifier], + delegate: TerminalDropDelegate( + groupID: utilityAreaViewModel.terminalGroups[index].id, + viewModel: utilityAreaViewModel, + destinationTerminalID: terminal.id + ) + ) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut(duration: 0.2), value: utilityAreaViewModel.terminalGroups[index].isCollapsed) } } } From a1d691364fe7c082cc3bd7e132f86b3158926afe Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 03:28:03 -0300 Subject: [PATCH 18/25] Improving Terminal Views --- .../UtilityAreaTerminalRowView.swift | 55 ++----------------- .../ViewModels/UtilityAreaViewModel.swift | 1 - 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index 5cb220da1..c73688eaa 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -64,55 +64,12 @@ struct UtilityAreaTerminalRowView: View { @ViewBuilder private func terminalTitleView() -> some View { - if utilityAreaViewModel.editingTerminalID == terminal.id { - TextField("", text: Binding( - get: { terminal.title }, - set: { newTitle in - guard !newTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return } - utilityAreaViewModel.updateTerminal(terminal.id, title: newTitle) - } - )) - .textFieldStyle(.plain) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.black) - .padding(.horizontal, 4) - .padding(.vertical, 2) - .background(Color.white) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color.gray.opacity(0.3), lineWidth: 1) - ) - .focused($focusedTerminalID, equals: terminal.id) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - focusedTerminalID = terminal.id - } - } - .onSubmit { - utilityAreaViewModel.editingTerminalID = nil - focusedTerminalID = nil - } - .onChange(of: focusedTerminalID) { newValue in - if newValue != terminal.id { - utilityAreaViewModel.editingTerminalID = nil - } - } - } else { - Text(terminal.title.isEmpty ? "Terminal" : terminal.title) - .lineLimit(1) - .truncationMode(.middle) - .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) - .foregroundColor(isSelected ? .white : .secondary) - .contentShape(Rectangle()) - .simultaneousGesture( - TapGesture(count: 2) - .onEnded { - utilityAreaViewModel.editingTerminalID = terminal.id - focusedTerminalID = terminal.id - } - ) - } + Text(terminal.title.isEmpty ? "Terminal" : terminal.title) + .lineLimit(1) + .truncationMode(.middle) + .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) + .foregroundColor(isSelected ? .white : .secondary) + .contentShape(Rectangle()) } } diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 0ee8ce06f..e81adccda 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -174,7 +174,6 @@ class UtilityAreaViewModel: ObservableObject { if let terminalIndex = terminalGroups[index].terminals.firstIndex(where: { $0.id == id }) { if let newTitle = title { terminalGroups[index].terminals[terminalIndex].title = newTitle - terminalGroups[index].terminals[terminalIndex].terminalTitle = newTitle } else { terminalGroups[index].terminals[terminalIndex].customTitle = false } From 26b6abeae11222f2a08712bfc70e4c8b96646565 Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 05:48:18 -0300 Subject: [PATCH 19/25] Improving Terminal Views --- .../UtilityAreaTerminalView.swift | 75 ++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index a2679289a..c0ed320f4 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -91,39 +91,80 @@ struct UtilityAreaTerminalView: View { var body: some View { UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { tabState in ZStack { - // Keeps the sidebar from changing sizes because TerminalEmulatorView takes a µs to load in - HStack { Spacer() } + if let selectedTerminal = getSelectedTerminal(), + let group = utilityAreaViewModel.terminalGroups.first(where: { $0.terminals.contains(selectedTerminal) }) { - if let selectedTerminal = getSelectedTerminal() { GeometryReader { geometry in let containerHeight = geometry.size.height + let containerWidth = geometry.size.width let totalFontHeight = fontTotalHeight(nsFont: font).rounded(.up) let constrainedHeight = containerHeight - containerHeight.truncatingRemainder( dividingBy: totalFontHeight ) - VStack(spacing: 0) { - Spacer(minLength: 0).frame(minHeight: 0) - TerminalEmulatorView( - url: selectedTerminal.url, - terminalID: selectedTerminal.id, - shellType: selectedTerminal.shell, - onTitleChange: { [weak selectedTerminal] newTitle in - guard let id = selectedTerminal?.id else { return } - // This can be called whenever, even in a view update so it needs to be dispatched. - DispatchQueue.main.async { [weak utilityAreaViewModel] in - utilityAreaViewModel?.updateTerminal(id, title: newTitle) + + if group.terminals.count == 1 { + VStack(spacing: 0) { + TerminalEmulatorView( + url: selectedTerminal.url, + terminalID: selectedTerminal.id, + shellType: selectedTerminal.shell, + onTitleChange: { [weak selectedTerminal] newTitle in + guard let id = selectedTerminal?.id else { return } + DispatchQueue.main.async { [weak utilityAreaViewModel] in + utilityAreaViewModel?.updateTerminal(id, title: newTitle) + } + } + ) + .frame(height: max(0, constrainedHeight - 1)) + .id(selectedTerminal.id) + .padding(.horizontal, 4) + } + } else { + VStack { + ScrollView(.horizontal, showsIndicators: true) { + HStack(spacing: 0.5) { + Rectangle() + .frame(width: 2) + .foregroundStyle(.gray.opacity(0.2)) + ForEach(group.terminals, id: \.id) { terminal in + TerminalEmulatorView( + url: terminal.url, + terminalID: terminal.id, + shellType: terminal.shell, + onTitleChange: { [weak terminal] newTitle in + guard let id = terminal?.id else { return } + DispatchQueue.main.async { [weak utilityAreaViewModel] in + utilityAreaViewModel?.updateTerminal(id, title: newTitle) + } + } + ) + .frame(height: max(0, constrainedHeight - 1)) + .frame(minWidth: 200, maxWidth: .infinity) + .id(terminal.id) + .padding(.horizontal, 8) + + Rectangle() + .frame(width: 2) + .foregroundStyle(.gray.opacity(0.2)) + } } + .frame(minWidth: containerWidth) + .frame(maxWidth: .infinity, alignment: .leading) } - ) - .frame(height: max(0, constrainedHeight - 1)) - .id(selectedTerminal.id) + + Rectangle() + .frame(height: 2) + .foregroundStyle(.gray.opacity(0.2)) + } } } + } else { CEContentUnavailableView("No Selection") } } .padding(.horizontal, 10) + .padding(.bottom, 10) .paneToolbar { PaneToolbarSection { UtilityAreaTerminalPicker( From b7827d7fb2ea6e828be10e187bcf90c586754f5e Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 06:52:02 -0300 Subject: [PATCH 20/25] Improving Utility Area Terminal --- .../Delegates/NewGroupDropDelegate.swift | 63 ++++++++++++ .../Delegates/TerminalDropDelegate.swift | 96 +++++++++++++++++++ .../UtilityAreaTerminalGroupView.swift | 67 +++++++++---- .../UtilityAreaTerminalRowView.swift | 31 ++++-- .../UtilityAreaTerminalSidebar.swift | 73 ++++++++------ .../ViewModels/UtilityAreaViewModel.swift | 89 ----------------- 6 files changed, 277 insertions(+), 142 deletions(-) create mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/NewGroupDropDelegate.swift create mode 100644 CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/TerminalDropDelegate.swift diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/NewGroupDropDelegate.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/NewGroupDropDelegate.swift new file mode 100644 index 000000000..07a255675 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/NewGroupDropDelegate.swift @@ -0,0 +1,63 @@ +// +// NewGroupDropDelegate.swift +// CodeEdit +// +// Created by Gustavo Soré on 30/06/25. +// + +import SwiftUI +import UniformTypeIdentifiers + +/// Drop delegate responsible for handling the case when a terminal is dropped +/// outside of any existing group — i.e., it should create a new group with the dropped terminal. +struct NewGroupDropDelegate: DropDelegate { + /// The view model that manages terminal groups and selection state. + let viewModel: UtilityAreaViewModel + + /// Validates whether the drop operation includes terminal data that this delegate can handle. + /// + /// - Parameter info: The drop information provided by the system. + /// - Returns: `true` if the drop contains a valid terminal item type. + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [UTType.terminal.identifier]) + } + + /// Performs the drop by creating a new group and moving the terminal into it. + /// + /// - Parameter info: The drop information containing the dragged item. + /// - Returns: `true` if the drop was successfully handled. + func performDrop(info: DropInfo) -> Bool { + // Extract the first item provider that conforms to the terminal type. + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { + return false + } + + // Load and decode the terminal drag information. + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), + let terminal = viewModel.terminalGroups + .flatMap({ $0.terminals }) + .first(where: { $0.id == dragInfo.terminalID }) else { + return + } + + // Perform the group creation and terminal movement on the main thread. + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + // Optional logic to clean up old location (if needed). + viewModel.finalizeMoveTerminal(terminal, toGroup: UUID(), before: nil) + + // Create a new group containing the dropped terminal. + viewModel.createGroup(with: [terminal]) + + // Reset drag-related state. + viewModel.dragOverTerminalID = nil + viewModel.draggedTerminalID = nil + } + } + } + + return true + } +} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/TerminalDropDelegate.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/TerminalDropDelegate.swift new file mode 100644 index 000000000..392a0c461 --- /dev/null +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/Delegates/TerminalDropDelegate.swift @@ -0,0 +1,96 @@ +// +// TerminalDropDelegate.swift +// CodeEdit +// +// Created by Gustavo Soré on 30/06/25. +// + +import SwiftUI +import UniformTypeIdentifiers + +/// Handles drop interactions for a terminal inside a specific group, +/// allowing for reordering or moving between groups. +struct TerminalDropDelegate: DropDelegate { + /// The ID of the group where the drop target resides. + let groupID: UUID + + /// The shared view model managing terminal groups and selection state. + let viewModel: UtilityAreaViewModel + + /// The ID of the terminal that is the drop destination, or `nil` if dropping at the end. + let destinationTerminalID: UUID? + + /// Validates if the drop contains terminal data. + /// + /// - Parameter info: The current drop context. + /// - Returns: `true` if the item conforms to the terminal type. + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [UTType.terminal.identifier]) + } + + /// Called when the drop enters a new target. + /// Sets the drag state in the view model for UI feedback. + /// + /// - Parameter info: The drop context. + func dropEntered(info: DropInfo) { + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return } + + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data) else { return } + + DispatchQueue.main.async { + withAnimation { + viewModel.draggedTerminalID = dragInfo.terminalID + viewModel.dragOverTerminalID = destinationTerminalID + } + } + } + } + + /// Called continuously as the drop is updated over the view. + /// Updates drag-over visual feedback. + /// + /// - Parameter info: The drop context. + /// - Returns: A drop proposal that defines the type of drop operation (e.g., move). + func dropUpdated(info: DropInfo) -> DropProposal? { + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.dragOverTerminalID = destinationTerminalID + } + } + + return DropProposal(operation: .move) + } + + /// Called when the drop is performed. + /// Decodes the dragged terminal and triggers its relocation in the model. + /// + /// - Parameter info: The drop context with the drag payload. + /// - Returns: `true` if the drop was handled successfully. + func performDrop(info: DropInfo) -> Bool { + guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false } + + item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in + guard let data = data, + let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), + let terminal = viewModel.terminalGroups + .flatMap({ $0.terminals }) + .first(where: { $0.id == dragInfo.terminalID }) else { return } + + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.2)) { + viewModel.finalizeMoveTerminal( + terminal, + toGroup: groupID, + before: destinationTerminalID + ) + viewModel.dragOverTerminalID = nil + viewModel.draggedTerminalID = nil + } + } + } + + return true + } +} diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift index ca930f2ab..a3cacfb1c 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalGroupView.swift @@ -8,21 +8,33 @@ import SwiftUI import UniformTypeIdentifiers +/// A view that displays a terminal group with a header and a list of its terminals. +/// Supports editing the group name, collapsing, drag-and-drop, and inline terminal row management. struct UtilityAreaTerminalGroupView: View { + /// The index of the group within the terminalGroups array. let index: Int + + /// Whether this group is currently selected in the UI. let isGroupSelected: Bool + + /// The shared view model that manages terminal groups and their state. @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + + /// Manages focus on a specific terminal row for keyboard navigation or editing. @FocusState private var focusedTerminalID: UUID? var body: some View { VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 4) { + // MARK: - Group Header + + HStack(spacing: 4) { Image(systemName: "square.on.square") .font(.system(size: 14, weight: .medium)) .frame(width: 20, height: 20) .foregroundStyle(.primary.opacity(0.6)) + // Editable group name when in edit mode if utilityAreaViewModel.editingGroupID == utilityAreaViewModel.terminalGroups[index].id { TextField("", text: Binding( get: { utilityAreaViewModel.terminalGroups[index].name }, @@ -47,7 +59,12 @@ struct UtilityAreaTerminalGroupView: View { utilityAreaViewModel.editingGroupID = nil } } else { - Text(utilityAreaViewModel.terminalGroups[index].name.isEmpty ? "terminals" : utilityAreaViewModel.terminalGroups[index].name) + // Display group name normally + Text( + utilityAreaViewModel.terminalGroups[index].name.isEmpty + ? "terminals" + : utilityAreaViewModel.terminalGroups[index].name + ) .lineLimit(1) .truncationMode(.middle) .font(.system(size: 13, weight: .regular)) @@ -63,7 +80,12 @@ struct UtilityAreaTerminalGroupView: View { Spacer() - Image(systemName: utilityAreaViewModel.terminalGroups[index].isCollapsed ? "chevron.right" : "chevron.down") + // Expand/collapse toggle + Image( + systemName: utilityAreaViewModel.terminalGroups[index].isCollapsed + ? "chevron.right" + : "chevron.down" + ) .font(.system(size: 11, weight: .medium)) .foregroundColor(.secondary) } @@ -76,8 +98,7 @@ struct UtilityAreaTerminalGroupView: View { } } .onDrag { -// utilityAreaViewModel.draggedTerminalID = terminal.id - + // Optional: dragging the entire group (stubbed terminal ID) let dragInfo = TerminalDragInfo(terminalID: .init()) let provider = NSItemProvider() do { @@ -90,10 +111,13 @@ struct UtilityAreaTerminalGroupView: View { return nil } } catch { - print("❌ Erro ao codificar dragInfo: \(error)") + print("❌ Failed to encode dragInfo: \(error)") } return provider } + + // MARK: - Terminal Rows (if group is expanded) + if !utilityAreaViewModel.terminalGroups[index].isCollapsed { VStack(spacing: 0) { ForEach(utilityAreaViewModel.terminalGroups[index].terminals, id: \.id) { terminal in @@ -107,17 +131,15 @@ struct UtilityAreaTerminalGroupView: View { let dragInfo = TerminalDragInfo(terminalID: terminal.id) let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") + guard let data = try? JSONEncoder().encode(dragInfo) else { + return provider + } + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil } return provider } @@ -130,7 +152,10 @@ struct UtilityAreaTerminalGroupView: View { ) ) .transition(.opacity.combined(with: .move(edge: .top))) - .animation(.easeInOut(duration: 0.2), value: utilityAreaViewModel.terminalGroups[index].isCollapsed) + .animation( + .easeInOut(duration: 0.2), + value: utilityAreaViewModel.terminalGroups[index].isCollapsed + ) } } } @@ -147,6 +172,9 @@ struct UtilityAreaTerminalGroupView: View { } } +// MARK: - Preview + +/// Preview provider for `UtilityAreaTerminalGroupView`, showing a mock terminal group with two terminals. struct TerminalGroupViewPreviews: PreviewProvider { static var previews: some View { let terminal = UtilityAreaTerminal( @@ -165,7 +193,7 @@ struct TerminalGroupViewPreviews: PreviewProvider { let utilityAreaViewModel = UtilityAreaViewModel() utilityAreaViewModel.terminalGroups = [ - UtilityAreaTerminalGroup(name: "Grupo de Preview", terminals: [terminal, terminal2]) + UtilityAreaTerminalGroup(name: "Preview Group", terminals: [terminal, terminal2]) ] utilityAreaViewModel.selectedTerminals = [terminal.id] @@ -182,6 +210,7 @@ struct TerminalGroupViewPreviews: PreviewProvider { } } +// Wrapper view to render the preview group. private struct TerminalGroupViewPreviewWrapper: View { @EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel @FocusState private var focusedTerminalID: UUID? diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift index c73688eaa..fcdab70a5 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalRowView.swift @@ -1,36 +1,47 @@ // -// UtilityAreaTerminalRowView.swift +// UtilityAreaTerminalSidebar.swift // CodeEdit // -// Created by Gustavo Soré on 28/06/25. +// Created by Khan Winter on 8/19/24. // import SwiftUI import UniformTypeIdentifiers +/// A single row representing a terminal within a terminal group. +/// Includes icon, title, selection handling, hover actions, and delete button. struct UtilityAreaTerminalRowView: View { + /// The terminal instance represented by this row. let terminal: UtilityAreaTerminal + + /// Focus binding used for keyboard interactions or editing. @FocusState.Binding var focusedTerminalID: UUID? + /// View model that manages the terminal groups and selection state. @EnvironmentObject var utilityAreaViewModel: UtilityAreaViewModel + + /// Tracks whether the mouse is currently hovering this row. @State private var isHovering = false + /// Computed property to check if the terminal is currently selected. var isSelected: Bool { utilityAreaViewModel.selectedTerminals.contains(terminal.id) } var body: some View { - HStack(spacing: 8) { + // Terminal icon Image(systemName: "terminal") .font(.system(size: 14, weight: .medium)) .foregroundStyle(isSelected ? Color.white : Color.secondary) .frame(width: 20, height: 20) + // Terminal title terminalTitleView() Spacer() + // Close button shown only on hover if isHovering { Button { utilityAreaViewModel.removeTerminals([terminal.id]) @@ -46,11 +57,14 @@ struct UtilityAreaTerminalRowView: View { .padding(.horizontal, 6) .padding(.vertical, 2) .background( + // Background changes on selection or drag-over RoundedRectangle(cornerRadius: 6) - .fill(isSelected ? Color.blue : - utilityAreaViewModel.dragOverTerminalID == terminal.id ? Color.blue.opacity(0.15) : .clear) + .fill( + isSelected ? Color.blue : + utilityAreaViewModel.dragOverTerminalID == terminal.id ? Color.blue.opacity(0.15) : .clear + ) ) - .contentShape(Rectangle()) + .contentShape(Rectangle()) // Increases tappable area .onHover { hovering in isHovering = hovering } @@ -62,6 +76,7 @@ struct UtilityAreaTerminalRowView: View { .animation(.easeInOut(duration: 0.15), value: isHovering) } + /// Returns a view displaying the terminal's title with styling depending on selection state. @ViewBuilder private func terminalTitleView() -> some View { Text(terminal.title.isEmpty ? "Terminal" : terminal.title) @@ -73,10 +88,14 @@ struct UtilityAreaTerminalRowView: View { } } +// MARK: - Preview + +/// Preview provider for `UtilityAreaTerminalRowView` with sample data. #Preview { UtilityAreaTerminalTabPreviewWrapper() } +/// Wrapper view for rendering the terminal row in Xcode Preview with mock data and environment. private struct UtilityAreaTerminalTabPreviewWrapper: View { @StateObject private var viewModel = UtilityAreaViewModel() @StateObject private var tabViewModel = UtilityAreaTabViewModel() diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift index cb023f47b..1fb0db799 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalSidebar.swift @@ -1,35 +1,46 @@ -// UtilityAreaTerminalSidebar.swift -// Com ScrollView + VStack e suporte a drag & drop com preview e grupos colapsáveis +// +// UtilityAreaTerminalSidebar.swift +// CodeEdit +// +// Created by Khan Winter on 8/19/24. +// import SwiftUI import UniformTypeIdentifiers +// MARK: - UTType Extension + +/// Custom type identifier used for drag-and-drop functionality involving terminal items. extension UTType { static let terminal = UTType(exportedAs: "dev.codeedit.terminal") } +// MARK: - TerminalDragInfo + +/// Represents the information used when dragging a terminal in the sidebar. struct TerminalDragInfo: Codable { let terminalID: UUID } -struct InsertionIndicator: View { - var body: some View { - Rectangle() - .fill(Color.accentColor) - .frame(height: 2) - .padding(.horizontal, 6) - .transition(.opacity) - } -} +// MARK: - UtilityAreaTerminalSidebar +/// A SwiftUI view that displays the list of available terminals in the utility area. +/// Supports single terminal rows, terminal groups, drag and drop functionality, +/// context menus for creating terminals, and a custom toolbar. struct UtilityAreaTerminalSidebar: View { + /// The current workspace document environment object. @EnvironmentObject private var workspace: WorkspaceDocument + + /// The view model that manages terminal groups and terminals in the utility area. @EnvironmentObject private var utilityAreaViewModel: UtilityAreaViewModel + + /// Focus state for determining which terminal (if any) is currently focused. @FocusState private var focusedTerminalID: UUID? var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { + // Iterate over terminal groups to display each accordingly ForEach(Array(utilityAreaViewModel.terminalGroups.enumerated()), id: \.element.id) { index, group in if group.terminals.count == 1 { let terminal = group.terminals[0] @@ -42,17 +53,15 @@ struct UtilityAreaTerminalSidebar: View { let dragInfo = TerminalDragInfo(terminalID: terminal.id) let provider = NSItemProvider() - do { - let data = try JSONEncoder().encode(dragInfo) - provider.registerDataRepresentation( - forTypeIdentifier: UTType.terminal.identifier, - visibility: .all - ) { completion in - completion(data, nil) - return nil - } - } catch { - print("❌ Erro ao codificar dragInfo: \(error)") + guard let data = try? JSONEncoder().encode(dragInfo) else { + return provider + } + provider.registerDataRepresentation( + forTypeIdentifier: UTType.terminal.identifier, + visibility: .all + ) { completion in + completion(data, nil) + return nil } return provider } @@ -67,6 +76,7 @@ struct UtilityAreaTerminalSidebar: View { .transition(.opacity.combined(with: .move(edge: .top))) .animation(.easeInOut(duration: 0.2), value: group.isCollapsed) } else { + // Display a terminal group with collapsible behavior UtilityAreaTerminalGroupView( index: index, isGroupSelected: true @@ -78,12 +88,11 @@ struct UtilityAreaTerminalSidebar: View { } .onDrop( of: [UTType.terminal.identifier], - delegate: NewGroupDropDelegate( - viewModel: utilityAreaViewModel - ) + delegate: NewGroupDropDelegate(viewModel: utilityAreaViewModel) ) .background(Color(NSColor.controlBackgroundColor)) .contextMenu { + // Context menu for creating new terminals Button("New Terminal") { utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } @@ -101,11 +110,14 @@ struct UtilityAreaTerminalSidebar: View { } .paneToolbar { PaneToolbarSection { + // Toolbar button for adding a new terminal Button { utilityAreaViewModel.addTerminal(rootURL: workspace.fileURL) } label: { Image(systemName: "plus") } + + // Toolbar button for removing selected terminals Button { utilityAreaViewModel.removeTerminals(utilityAreaViewModel.selectedTerminals) } label: { @@ -128,8 +140,12 @@ struct UtilityAreaTerminalSidebar: View { } } -struct UtilityAreaTerminalSidebar_Previews: PreviewProvider { +// MARK: - Preview + +/// Preview provider for `UtilityAreaTerminalSidebar`. +struct UtilityAreaTerminalSidebarPreviews: PreviewProvider { static var previews: some View { + // Sample terminal for preview let terminal = UtilityAreaTerminal( id: UUID(), url: URL(string: "https://example.com")!, @@ -137,14 +153,15 @@ struct UtilityAreaTerminalSidebar_Previews: PreviewProvider { shell: .zsh ) + // Mock view model with one group let utilityAreaViewModel = UtilityAreaViewModel() utilityAreaViewModel.terminalGroups = [ - UtilityAreaTerminalGroup(name: "Grupo", terminals: [terminal]) + UtilityAreaTerminalGroup(name: "Group", terminals: [terminal]) ] utilityAreaViewModel.selectedTerminals = [terminal.id] + // Mock tab view model and workspace let tabViewModel = UtilityAreaTabViewModel() - let workspace = WorkspaceDocument() workspace.setValue(URL(string: "file:///mock/path")!, forKey: "fileURL") diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index e81adccda..10be19a4e 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -247,92 +247,3 @@ class UtilityAreaViewModel: ObservableObject { terminalGroups.append(.init(name: "\(terminalGroups.count) Terminals", terminals: terminals)) } } - -struct TerminalDropDelegate: DropDelegate { - let groupID: UUID - let viewModel: UtilityAreaViewModel - let destinationTerminalID: UUID? - - func validateDrop(info: DropInfo) -> Bool { - info.hasItemsConforming(to: [UTType.terminal.identifier]) - } - - func dropEntered(info: DropInfo) { - guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return } - - item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in - guard let data = data, - let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data) else { return } - - DispatchQueue.main.async { - withAnimation { - viewModel.draggedTerminalID = dragInfo.terminalID - viewModel.dragOverTerminalID = destinationTerminalID - } - } - } - } - - func dropUpdated(info: DropInfo) -> DropProposal? { - DispatchQueue.main.async { - withAnimation(.easeInOut(duration: 0.2)) { - viewModel.dragOverTerminalID = destinationTerminalID - } - } - - return .init(operation: .move) - } - - func performDrop(info: DropInfo) -> Bool { - guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false } - - item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in - guard let data = data, - let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), - let terminal = viewModel.terminalGroups.flatMap({ - $0.terminals - }).first( where: { - $0.id == dragInfo.terminalID - }) else { return } - - DispatchQueue.main.async { - withAnimation(.easeInOut(duration: 0.2)) { - viewModel.finalizeMoveTerminal(terminal, toGroup: groupID, before: destinationTerminalID) - viewModel.dragOverTerminalID = nil - viewModel.draggedTerminalID = nil - } - } - } - return true - } -} - -struct NewGroupDropDelegate: DropDelegate { - let viewModel: UtilityAreaViewModel - - func validateDrop(info: DropInfo) -> Bool { - info.hasItemsConforming(to: [UTType.terminal.identifier]) - } - - func performDrop(info: DropInfo) -> Bool { - guard let item = info.itemProviders(for: [UTType.terminal.identifier]).first else { return false } - - item.loadDataRepresentation(forTypeIdentifier: UTType.terminal.identifier) { data, _ in - guard let data = data, - let dragInfo = try? JSONDecoder().decode(TerminalDragInfo.self, from: data), - let terminal = viewModel.terminalGroups.flatMap({ $0.terminals }).first(where: { $0.id == dragInfo.terminalID }) else { - return - } - - DispatchQueue.main.async { - withAnimation(.easeInOut(duration: 0.2)) { - viewModel.finalizeMoveTerminal(terminal, toGroup: UUID(), before: nil) - viewModel.createGroup(with: [terminal]) - viewModel.dragOverTerminalID = nil - viewModel.draggedTerminalID = nil - } - } - } - return true - } -} From 5096db50b4674d5808012cdeea3a678536ff1bdd Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 06:54:42 -0300 Subject: [PATCH 21/25] Improving Utility Area Terminal --- .../Models/UtilityAreaTerminalGroup.swift | 24 +++++++++++++++++++ .../ViewModels/UtilityAreaViewModel.swift | 19 --------------- 2 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminalGroup.swift diff --git a/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminalGroup.swift b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminalGroup.swift new file mode 100644 index 000000000..7d2af7c7e --- /dev/null +++ b/CodeEdit/Features/UtilityArea/Models/UtilityAreaTerminalGroup.swift @@ -0,0 +1,24 @@ +// +// UtilityAreaTerminalGroup.swift +// CodeEdit +// +// Created by Gustavo Soré on 30/06/25. +// + +import Foundation + +struct UtilityAreaTerminalGroup: Identifiable, Hashable { + var id = UUID() + var name: String = "Grupo" + var terminals: [UtilityAreaTerminal] = [] + var isCollapsed: Bool = false + var userName: Bool = false + + static func == (lhs: UtilityAreaTerminalGroup, rhs: UtilityAreaTerminalGroup) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 10be19a4e..1c764f3d4 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -4,22 +4,6 @@ import SwiftUI import UniformTypeIdentifiers -struct UtilityAreaTerminalGroup: Identifiable, Hashable { - var id = UUID() - var name: String = "Grupo" - var terminals: [UtilityAreaTerminal] = [] - var isCollapsed: Bool = false - var userName: Bool = false - - static func == (lhs: UtilityAreaTerminalGroup, rhs: UtilityAreaTerminalGroup) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - /// # UtilityAreaViewModel /// A model class to host and manage data for the Utility area. class UtilityAreaViewModel: ObservableObject { @@ -80,8 +64,6 @@ class UtilityAreaViewModel: ObservableObject { func finalizeMoveTerminal(_ terminal: UtilityAreaTerminal, toGroup groupID: UUID, before destinationID: UUID?) { - print("finalizeMoveTerminal") - let alreadyInGroup = terminalGroups.contains { group in group.id == groupID && group.terminals.count == 1 && @@ -99,7 +81,6 @@ class UtilityAreaViewModel: ObservableObject { // Adiciona ao grupo destino guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { - print("⚠️ Grupo não encontrado para o drop.") return } From a0b85675b313305413bea121a9da53218b30951a Mon Sep 17 00:00:00 2001 From: Sore Date: Mon, 30 Jun 2025 07:01:28 -0300 Subject: [PATCH 22/25] Improving Utility Area Terminal --- .../ViewModels/UtilityAreaViewModel.swift | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index 1c764f3d4..fa8f41e7b 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -1,39 +1,78 @@ -// UtilityAreaViewModel.swift -// Atualizado para suportar drag-and-drop com reordenação visual e final com ScrollView + VStack +// +// UtilityAreaViewModel.swift +// CodeEdit +// +// Created by Lukas Pistrol on 20.03.22. +// import SwiftUI import UniformTypeIdentifiers -/// # UtilityAreaViewModel -/// A model class to host and manage data for the Utility area. +/// View model responsible for managing terminal groups, individual terminals, +/// selection state, drag-and-drop operations, and utility panel configuration. class UtilityAreaViewModel: ObservableObject { + // MARK: - UI State + + /// Currently selected tab in the utility area. @Published var selectedTab: UtilityAreaTab? = .terminal + /// Flat list of all terminals, derived from `terminalGroups`. @Published var terminals: [UtilityAreaTerminal] = [] + + /// List of terminal groups. + /// Automatically updates the flat `terminals` array when changed. @Published var terminalGroups: [UtilityAreaTerminalGroup] = [] { didSet { self.terminals = terminalGroups.flatMap { $0.terminals } } } + /// Set of selected terminal IDs. @Published var selectedTerminals: Set = [] - @Published var dragOverTerminalID: UUID? = nil - @Published var draggedTerminalID: UUID? = nil + + /// ID of the terminal currently hovered as a drop target. + @Published var dragOverTerminalID: UUID? + + /// ID of the terminal being dragged. + @Published var draggedTerminalID: UUID? + + /// Whether the utility area is currently collapsed. @Published var isCollapsed: Bool = false + + /// Whether the panel collapse/expand action should animate. @Published var animateCollapse: Bool = true + + /// Whether the utility area is maximized. @Published var isMaximized: Bool = false + + /// Current height of the utility area panel. @Published var currentHeight: Double = 0 - @Published var editingTerminalID: UUID? = nil + + /// ID of the terminal currently being edited (e.g. for inline title editing). + @Published var editingTerminalID: UUID? + + /// Available tabs in the utility area. @Published var tabItems: [UtilityAreaTab] = UtilityAreaTab.allCases + + /// View model for the current tab (e.g. terminal tab). @Published var tabViewModel = UtilityAreaTabViewModel() - @Published var editingGroupID: UUID? = nil + + /// ID of the group currently being edited (e.g. renaming). + @Published var editingGroupID: UUID? + + /// Focus state for managing terminal keyboard focus. @FocusState private var focusedTerminalID: UUID? - // MARK: - Drag Support + // MARK: - Drag-and-Drop Support + + /// Previews a terminal move by temporarily updating the groups' array structure. func previewMoveTerminal(_ terminalID: UUID, toGroup groupID: UUID, before destinationID: UUID?) { - guard let currentGroupIndex = terminalGroups.firstIndex(where: { $0.terminals.contains(where: { $0.id == terminalID }) }), - let currentTerminalIndex = terminalGroups[currentGroupIndex].terminals.firstIndex(where: { $0.id == terminalID }) else { + guard let currentGroupIndex = terminalGroups.firstIndex(where: { + $0.terminals.contains(where: { $0.id == terminalID }) + }), + let currentTerminalIndex = terminalGroups[currentGroupIndex] + .terminals.firstIndex(where: { $0.id == terminalID }) else { return } @@ -50,20 +89,18 @@ class UtilityAreaViewModel: ObservableObject { if let targetIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) { var group = terminalGroups[targetIndex] - if let destID = destinationID, let destIndex = group.terminals.firstIndex(where: { $0.id == destID }) { group.terminals.insert(terminal, at: destIndex) } else { group.terminals.append(terminal) } - terminalGroups[targetIndex] = group } } + /// Finalizes a terminal move across or within groups, updating the actual structure. func finalizeMoveTerminal(_ terminal: UtilityAreaTerminal, toGroup groupID: UUID, before destinationID: UUID?) { - let alreadyInGroup = terminalGroups.contains { group in group.id == groupID && group.terminals.count == 1 && @@ -72,17 +109,16 @@ class UtilityAreaViewModel: ObservableObject { guard !alreadyInGroup else { return } + // Remove terminal from all groups for index in terminalGroups.indices { terminalGroups[index].terminals.removeAll { $0.id == terminal.id } } - // Remove grupos vazios após a remoção + // Remove empty groups terminalGroups.removeAll { $0.terminals.isEmpty } - // Adiciona ao grupo destino - guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { - return - } + // Insert into new group + guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { return } if let destinationID, let destinationIndex = terminalGroups[groupIndex].terminals.firstIndex(where: { $0.id == destinationID }) { @@ -91,18 +127,18 @@ class UtilityAreaViewModel: ObservableObject { terminalGroups[groupIndex].terminals.append(terminal) } - // Atualiza seleção + // Update selection if !selectedTerminals.contains(terminal.id) { selectedTerminals = [terminal.id] } - for index in terminalGroups.indices { - if !terminalGroups[index].userName { - terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" - } + // Auto-name group if it wasn't named by user + for index in terminalGroups.indices where !terminalGroups[index].userName { + terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" } } + /// Removes a terminal from all groups by ID and returns it. private func removeTerminal(withID id: UUID) -> UtilityAreaTerminal? { for index in terminalGroups.indices { if let terminalIndex = terminalGroups[index].terminals.firstIndex(where: { $0.id == id }) { @@ -112,20 +148,23 @@ class UtilityAreaViewModel: ObservableObject { return nil } - // MARK: - State Restoration + // MARK: - Panel State Restoration + /// Restores panel state from the workspace object (collapsed, height, maximized). func restoreFromState(_ workspace: WorkspaceDocument) { isCollapsed = workspace.getFromWorkspaceState(.utilityAreaCollapsed) as? Bool ?? false currentHeight = workspace.getFromWorkspaceState(.utilityAreaHeight) as? Double ?? 300.0 isMaximized = workspace.getFromWorkspaceState(.utilityAreaMaximized) as? Bool ?? false } + /// Persists current panel state into the workspace object. func saveRestorationState(_ workspace: WorkspaceDocument) { workspace.addToWorkspaceState(key: .utilityAreaCollapsed, value: isCollapsed) workspace.addToWorkspaceState(key: .utilityAreaHeight, value: currentHeight) workspace.addToWorkspaceState(key: .utilityAreaMaximized, value: isMaximized) } + /// Toggles panel collapse with optional animation. func togglePanel(animation: Bool = true) { self.animateCollapse = animation self.isMaximized = false @@ -134,15 +173,13 @@ class UtilityAreaViewModel: ObservableObject { // MARK: - Terminal Management + /// Removes terminals by their IDs and updates groups and selection. func removeTerminals(_ ids: Set) { for index in terminalGroups.indices { terminalGroups[index].terminals.removeAll { ids.contains($0.id) } } - - // Remove grupos vazios terminalGroups.removeAll { $0.terminals.isEmpty } - // Atualiza seleção selectedTerminals.subtract(ids) if selectedTerminals.isEmpty, let last = terminalGroups.last?.terminals.last { @@ -150,6 +187,7 @@ class UtilityAreaViewModel: ObservableObject { } } + /// Updates a terminal's title, or resets it if `nil`. func updateTerminal(_ id: UUID, title: String?) { for index in terminalGroups.indices { if let terminalIndex = terminalGroups[index].terminals.firstIndex(where: { $0.id == id }) { @@ -163,15 +201,14 @@ class UtilityAreaViewModel: ObservableObject { } } + /// Initializes a default terminal if none exist. func initializeTerminals(workspaceURL: URL) { guard terminalGroups.flatMap({ $0.terminals }).isEmpty else { return } addTerminal(rootURL: workspaceURL) } + /// Adds a new terminal, optionally to a specific group and with a specific shell. func addTerminal(to groupID: UUID? = nil, shell: Shell? = nil, rootURL: URL?) { - - print("Did add temrinal") - let newTerminal = UtilityAreaTerminal( id: UUID(), url: rootURL ?? URL(filePath: "~/"), @@ -189,6 +226,7 @@ class UtilityAreaViewModel: ObservableObject { selectedTerminals = [newTerminal.id] } + /// Replaces a terminal with a new instance, useful for restarting. func replaceTerminal(_ replacing: UUID) { for index in terminalGroups.indices { if let idx = terminalGroups[index].terminals.firstIndex(where: { $0.id == replacing }) { @@ -211,10 +249,12 @@ class UtilityAreaViewModel: ObservableObject { } } + /// Reorders terminals in the flat `terminals` list (UI only). func reorderTerminals(from source: IndexSet, to destination: Int) { terminals.move(fromOffsets: source, toOffset: destination) } + /// Moves a terminal to a specific group and index. func moveTerminal(_ terminal: UtilityAreaTerminal, toGroup targetGroupID: UUID, at index: Int) { for index in terminalGroups.indices { terminalGroups[index].terminals.removeAll { $0.id == terminal.id } @@ -224,6 +264,7 @@ class UtilityAreaViewModel: ObservableObject { } } + /// Creates a new terminal group with the given terminals. func createGroup(with terminals: [UtilityAreaTerminal]) { terminalGroups.append(.init(name: "\(terminalGroups.count) Terminals", terminals: terminals)) } From df632b59bdb9c2a1cecc5950d8cdc656153c42fa Mon Sep 17 00:00:00 2001 From: Sore Date: Wed, 2 Jul 2025 23:39:41 -0300 Subject: [PATCH 23/25] Set terminal minimal width --- .../UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index c0ed320f4..00c21491c 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -139,7 +139,7 @@ struct UtilityAreaTerminalView: View { } ) .frame(height: max(0, constrainedHeight - 1)) - .frame(minWidth: 200, maxWidth: .infinity) + .frame(minWidth: 400, maxWidth: .infinity) .id(terminal.id) .padding(.horizontal, 8) From 4de212814bf6d325cb46fbc417d27332d38aa614 Mon Sep 17 00:00:00 2001 From: Sore Date: Wed, 2 Jul 2025 23:41:16 -0300 Subject: [PATCH 24/25] Fix lint issues --- .../TerminalUtility/UtilityAreaTerminalView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift index 00c21491c..ea3c42a0b 100644 --- a/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift +++ b/CodeEdit/Features/UtilityArea/TerminalUtility/UtilityAreaTerminalView.swift @@ -92,7 +92,9 @@ struct UtilityAreaTerminalView: View { UtilityAreaTabView(model: utilityAreaViewModel.tabViewModel) { tabState in ZStack { if let selectedTerminal = getSelectedTerminal(), - let group = utilityAreaViewModel.terminalGroups.first(where: { $0.terminals.contains(selectedTerminal) }) { + let group = utilityAreaViewModel.terminalGroups.first(where: { + $0.terminals.contains(selectedTerminal) + }) { GeometryReader { geometry in let containerHeight = geometry.size.height @@ -229,8 +231,6 @@ struct UtilityAreaTerminalSidebarWrapper: View { @ObservedObject var viewModel: UtilityAreaViewModel var body: some View { - - return UtilityAreaTerminalSidebar() } } From c4f541c4039b680275c0a320439d0aa8dff2cb6d Mon Sep 17 00:00:00 2001 From: Sore Date: Fri, 4 Jul 2025 00:37:00 -0300 Subject: [PATCH 25/25] Fix terminal groups name --- .../ViewModels/UtilityAreaViewModel.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift index fa8f41e7b..809c345da 100644 --- a/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift +++ b/CodeEdit/Features/UtilityArea/ViewModels/UtilityAreaViewModel.swift @@ -118,7 +118,10 @@ class UtilityAreaViewModel: ObservableObject { terminalGroups.removeAll { $0.terminals.isEmpty } // Insert into new group - guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { return } + guard let groupIndex = terminalGroups.firstIndex(where: { $0.id == groupID }) else { + renameGroups() + return + } if let destinationID, let destinationIndex = terminalGroups[groupIndex].terminals.firstIndex(where: { $0.id == destinationID }) { @@ -132,10 +135,7 @@ class UtilityAreaViewModel: ObservableObject { selectedTerminals = [terminal.id] } - // Auto-name group if it wasn't named by user - for index in terminalGroups.indices where !terminalGroups[index].userName { - terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" - } + renameGroups() } /// Removes a terminal from all groups by ID and returns it. @@ -185,6 +185,7 @@ class UtilityAreaViewModel: ObservableObject { let last = terminalGroups.last?.terminals.last { selectedTerminals = [last.id] } + renameGroups() } /// Updates a terminal's title, or resets it if `nil`. @@ -218,12 +219,12 @@ class UtilityAreaViewModel: ObservableObject { if let groupID, let index = terminalGroups.firstIndex(where: { $0.id == groupID }) { terminalGroups[index].terminals.append(newTerminal) - terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" } else { terminalGroups.append(.init(name: "2 Terminals", terminals: [newTerminal])) } selectedTerminals = [newTerminal.id] + renameGroups() } /// Replaces a terminal with a new instance, useful for restarting. @@ -268,4 +269,13 @@ class UtilityAreaViewModel: ObservableObject { func createGroup(with terminals: [UtilityAreaTerminal]) { terminalGroups.append(.init(name: "\(terminalGroups.count) Terminals", terminals: terminals)) } + + // MARK: - PRIVATE METHODS + + /// Rename all terminal groups that not has user name specifed. + private func renameGroups() { + for index in terminalGroups.indices where !terminalGroups[index].userName { + terminalGroups[index].name = "\(terminalGroups[index].terminals.count) Terminals" + } + } }