From 5a9b9d60c618a806adf1fb0c96da3157ebf27c85 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 18 May 2024 11:12:31 +0700 Subject: [PATCH] feat: add duplicate and remove button --- Memola.xcodeproj/project.pbxproj | 16 +-- Memola/Canvas/Tool/Core/Tool.swift | 29 +++++- ...er.swift => ContextMenuViewModifier.swift} | 6 +- ...odifier.swift => OnDragViewModifier.swift} | 6 +- .../Memo/PenTool/PenDropDelegate.swift | 2 +- .../Features/Memo/PenTool/PenToolView.swift | 99 +++++++------------ 6 files changed, 75 insertions(+), 83 deletions(-) rename Memola/Components/ViewModifiers/{ContextMenuableViewModifier.swift => ContextMenuViewModifier.swift} (72%) rename Memola/Components/ViewModifiers/{DraggableViewModifier.swift => OnDragViewModifier.swift} (75%) diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index fba1545..12d425f 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; }; EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; }; EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; - EC0D14282BF7BF20009BFE5F /* ContextMenuableViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */; }; + EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; }; EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; }; EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; }; EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; }; @@ -18,7 +18,7 @@ EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; }; EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; }; - EC50500D2BF6674400B4D86E /* DraggableViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */; }; + EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; @@ -87,7 +87,7 @@ EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = ""; }; EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; - EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuableViewModifier.swift; sourceTree = ""; }; + EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = ""; }; EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; @@ -95,7 +95,7 @@ EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = ""; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = ""; }; - EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableViewModifier.swift; sourceTree = ""; }; + EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = ""; }; EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; @@ -233,8 +233,8 @@ EC50500B2BF6673300B4D86E /* ViewModifiers */ = { isa = PBXGroup; children = ( - EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */, - EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */, + EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */, + EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -721,7 +721,7 @@ ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, - EC50500D2BF6674400B4D86E /* DraggableViewModifier.swift in Sources */, + EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */, ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, @@ -737,7 +737,7 @@ ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */, EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, - EC0D14282BF7BF20009BFE5F /* ContextMenuableViewModifier.swift in Sources */, + EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index c36ffca..609530a 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -16,9 +16,6 @@ public class Tool: NSObject, ObservableObject { @Published var pens: [Pen] = [] @Published var selectedPen: Pen? @Published var draggedPen: Pen? - @Published var isReordering: Bool = false - @Published var isShaking: Bool = false - @Published var shakingId: UUID = UUID() let scrollPublisher = PassthroughSubject() @@ -47,6 +44,9 @@ public class Tool: NSObject, ObservableObject { selectedPen = pen } selectedPen?.isSelected = true + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } func unselectPen(_ pen: Pen) { @@ -54,6 +54,23 @@ public class Tool: NSObject, ObservableObject { withAnimation { selectedPen = nil } + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } + + func duplicatePen(_ pen: Pen, of originalPen: Pen) { + guard let index = pens.firstIndex(where: { originalPen === $0 }) else { return } + withAnimation { + pens.insert(pen, at: index + 1) + } + selectPen(pen) + withPersistence(\.viewContext) { [pens] context in + for (index, pen) in pens.enumerated() { + pen.object?.orderIndex = Int16(index) + } + try context.saveIfNeeded() + } } func addPen(_ pen: Pen) { @@ -65,6 +82,9 @@ public class Tool: NSObject, ObservableObject { object.pens.add(_pen) } scrollPublisher.send(pen.id) + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } func removePen(_ pen: Pen) { @@ -76,5 +96,8 @@ public class Tool: NSObject, ObservableObject { _pen.tool = nil object.pens.remove(_pen) } + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } } diff --git a/Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift b/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift similarity index 72% rename from Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift rename to Memola/Components/ViewModifiers/ContextMenuViewModifier.swift index ae270c8..9c7a335 100644 --- a/Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift +++ b/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift @@ -1,5 +1,5 @@ // -// ContextMenuableViewModifier.swift +// ContextMenuViewModifier.swift // Memola // // Created by Dscyre Scotti on 5/17/24. @@ -8,7 +8,7 @@ import SwiftUI import Foundation -struct ContextMenuableViewModifier: ViewModifier { +struct ContextMenuViewModifier: ViewModifier { let condition: Bool let menuItems: () -> MenuContent @@ -24,6 +24,6 @@ struct ContextMenuableViewModifier: ViewModifier { public extension View { func contextMenu(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent) -> some View { - modifier(ContextMenuableViewModifier(condition: condition, menuItems: menuItems)) + modifier(ContextMenuViewModifier(condition: condition, menuItems: menuItems)) } } diff --git a/Memola/Components/ViewModifiers/DraggableViewModifier.swift b/Memola/Components/ViewModifiers/OnDragViewModifier.swift similarity index 75% rename from Memola/Components/ViewModifiers/DraggableViewModifier.swift rename to Memola/Components/ViewModifiers/OnDragViewModifier.swift index 689834e..b60d540 100644 --- a/Memola/Components/ViewModifiers/DraggableViewModifier.swift +++ b/Memola/Components/ViewModifiers/OnDragViewModifier.swift @@ -1,5 +1,5 @@ // -// DraggableViewModifier.swift +// OnDragViewModifier.swift // Memola // // Created by Dscyre Scotti on 5/16/24. @@ -8,7 +8,7 @@ import SwiftUI import Foundation -struct DraggableViewModifier: ViewModifier { +struct OnDragViewModifier: ViewModifier { let condition: Bool let data: () -> NSItemProvider let preview: () -> Preview @@ -25,6 +25,6 @@ struct DraggableViewModifier: ViewModifier { public extension View { func onDrag(if condition: Bool, data: @escaping () -> NSItemProvider, @ViewBuilder preview: @escaping () -> Preview) -> some View { - modifier(DraggableViewModifier(condition: condition, data: data, preview: preview)) + modifier(OnDragViewModifier(condition: condition, data: data, preview: preview)) } } diff --git a/Memola/Features/Memo/PenTool/PenDropDelegate.swift b/Memola/Features/Memo/PenTool/PenDropDelegate.swift index dbc479b..d784d1d 100644 --- a/Memola/Features/Memo/PenTool/PenDropDelegate.swift +++ b/Memola/Features/Memo/PenTool/PenDropDelegate.swift @@ -27,11 +27,11 @@ struct PenDropDelegate: DropDelegate { tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) tool.objectWillChange.send() } - tool.shakingId = UUID() withPersistence(\.viewContext) { context in for (index, pen) in tool.pens.enumerated() { pen.object?.orderIndex = Int16(index) } + try context.saveIfNeeded() } } } diff --git a/Memola/Features/Memo/PenTool/PenToolView.swift b/Memola/Features/Memo/PenTool/PenToolView.swift index c2c0f85..717bb28 100644 --- a/Memola/Features/Memo/PenTool/PenToolView.swift +++ b/Memola/Features/Memo/PenTool/PenToolView.swift @@ -17,37 +17,14 @@ struct PenToolView: View { var body: some View { ScrollViewReader { proxy in ScrollView(.vertical, showsIndicators: false) { - if tool.isReordering { - LazyVStack(spacing: 0) { - ForEach(tool.pens) { pen in - if pen.strokeStyle == .marker { - penView(pen) - .offset(y: tool.isShaking ? 1.5 : -1.5) - .id(pen.id) - } else { - penView(pen) - .id(pen.id) - } - } + LazyVStack(spacing: 0) { + ForEach(tool.pens) { pen in + penView(pen) + .id(pen.id) } - .padding(.vertical, 10) - .padding(.leading, 40) - .onAppear { - withAnimation(.easeInOut.repeatForever().speed(5)) { - tool.isShaking.toggle() - } - } - .id(tool.shakingId) - } else { - LazyVStack(spacing: 0) { - ForEach(tool.pens) { pen in - penView(pen) - .id(pen.id) - } - } - .padding(.vertical, 10) - .padding(.leading, 40) } + .padding(.vertical, 10) + .padding(.leading, 40) } .onReceive(tool.scrollPublisher) { id in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -68,14 +45,8 @@ struct PenToolView: View { } .clipShape(.rect(cornerRadii: .init(bottomTrailing: 20, topTrailing: 20))) .overlay(alignment: .bottomLeading) { - Group { - if tool.isReordering { - doneButton - } else { - newPenButton - } - } - .offset(x: 60, y: 10) + newPenButton + .offset(x: 60, y: 10) } } @@ -102,20 +73,34 @@ struct PenToolView: View { tool.selectPen(pen) } } - .disabled(tool.isReordering) - .contextMenu(if: pen.strokeStyle != .eraser && !tool.isReordering) { - Button { - tool.isReordering = true - } label: { - Label("Rearrange", systemImage: "arrow.up.arrow.down.circle") - } - Button(role: .destructive) { - tool.removePen(pen) - } label: { - Label("Delete", systemImage: "trash") + .contextMenu(if: pen.strokeStyle != .eraser) { + ControlGroup { + Button { + let originalPen = pen + let pen = PenObject.createObject(\.viewContext, penStyle: originalPen.style) + pen.color = originalPen.color + pen.isSelected = true + pen.tool = tool.object + let _pen = Pen(object: pen) + tool.duplicatePen(_pen, of: originalPen) + } label: { + Label( + title: { Text("Duplicate") }, + icon: { Image(systemName: "plus.square.on.square") } + ) + } + Button(role: .destructive) { + tool.removePen(pen) + } label: { + Label( + title: { Text("Remove") }, + icon: { Image(systemName: "trash") } + ) + } } + .controlGroupStyle(.menu) } - .onDrag(if: pen.strokeStyle != .eraser && tool.isReordering) { + .onDrag(if: pen.strokeStyle != .eraser) { tool.draggedPen = pen return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider() } preview: { @@ -149,22 +134,6 @@ struct PenToolView: View { .hoverEffect(.lift) } - var doneButton: some View { - Button { - tool.isReordering = false - } label: { - Image(systemName: "xmark.circle") - .font(.title2) - .padding(1) - .contentShape(.circle) - .background { - Circle() - .fill(.white) - } - } - .hoverEffect(.lift) - } - func penPreview(_ pen: Pen) -> some View { ZStack { if let tip = pen.style.icon.tip {