diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index d527bc1..fba1545 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -10,6 +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 */; }; 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 */; }; @@ -86,6 +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 = ""; }; 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 = ""; }; @@ -232,6 +234,7 @@ isa = PBXGroup; children = ( EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */, + EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */, ); path = ViewModifiers; sourceTree = ""; @@ -734,6 +737,7 @@ ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */, EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, + EC0D14282BF7BF20009BFE5F /* ContextMenuableViewModifier.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 db49795..43c1a36 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -15,6 +15,9 @@ 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() init(object: ToolObject) { self.object = object @@ -45,6 +48,9 @@ public class Tool: NSObject, ObservableObject { func unselectPen(_ pen: Pen) { pen.isSelected = false + withAnimation { + selectedPen = nil + } } func addPen(_ pen: Pen) { @@ -56,4 +62,15 @@ public class Tool: NSObject, ObservableObject { object.pens.add(_pen) } } + + func removePen(_ pen: Pen) { + guard let index = pens.firstIndex(where: { $0 === pen }) else { return } + let deletedPen = withAnimation { + pens.remove(at: index) + } + if let _pen = deletedPen.object { + _pen.tool = nil + object.pens.remove(_pen) + } + } } diff --git a/Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift b/Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift new file mode 100644 index 0000000..ae270c8 --- /dev/null +++ b/Memola/Components/ViewModifiers/ContextMenuableViewModifier.swift @@ -0,0 +1,29 @@ +// +// ContextMenuableViewModifier.swift +// Memola +// +// Created by Dscyre Scotti on 5/17/24. +// + +import SwiftUI +import Foundation + +struct ContextMenuableViewModifier: ViewModifier { + let condition: Bool + let menuItems: () -> MenuContent + + @ViewBuilder + func body(content: Content) -> some View { + if condition { + content.contextMenu(menuItems: menuItems) + } else { + content + } + } +} + +public extension View { + func contextMenu(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent) -> some View { + modifier(ContextMenuableViewModifier(condition: condition, menuItems: menuItems)) + } +} diff --git a/Memola/Features/Memo/PenTool/PenDropDelegate.swift b/Memola/Features/Memo/PenTool/PenDropDelegate.swift index 27bc4e2..dbc479b 100644 --- a/Memola/Features/Memo/PenTool/PenDropDelegate.swift +++ b/Memola/Features/Memo/PenTool/PenDropDelegate.swift @@ -27,6 +27,7 @@ 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) diff --git a/Memola/Features/Memo/PenTool/PenToolView.swift b/Memola/Features/Memo/PenTool/PenToolView.swift index 353ba54..0172a48 100644 --- a/Memola/Features/Memo/PenTool/PenToolView.swift +++ b/Memola/Features/Memo/PenTool/PenToolView.swift @@ -17,17 +17,42 @@ struct PenToolView: View { var body: some View { VStack(alignment: .trailing, spacing: 0) { ScrollView(.vertical, showsIndicators: false) { - LazyVStack(spacing: 0) { - ForEach(tool.pens) { pen in - penView(pen) + 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) + } else { + penView(pen) + } + } } + .padding(.vertical, 5) + .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) + } + } + .padding(.vertical, 5) + .padding(.leading, 40) } - .padding(.vertical, 5) - .padding(.leading, 40) } VStack(spacing: 0) { Divider() - newPenButton + if tool.isReordering { + reorderCancelButton + } else { + newPenButton + } } .frame(width: width * factor - 20) } @@ -58,43 +83,40 @@ struct PenToolView: View { .frame(width: width * factor, height: height * factor) .padding(.vertical, 5) .padding(.leading, 10) - .clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) .contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) - .onDrag(if: pen.strokeStyle != .eraser) { + .onTapGesture { + if tool.selectedPen === pen { + tool.unselectPen(pen) + } else { + 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") + } + } + .onDrag(if: pen.strokeStyle != .eraser && tool.isReordering) { tool.draggedPen = pen return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider() } preview: { - ZStack { - if let tip = pen.style.icon.tip { - Image(tip) - .resizable() - .renderingMode(.template) - .foregroundStyle(Color.rgba(from: pen.color)) - } - Image(pen.style.icon.base) - .resizable() - } - .frame(width: width * factor, height: height * factor) - .padding([.vertical, .leading], 10) - .contentShape(.dragPreview, .rect(cornerRadius: 10)) + penPreview(pen) + .contentShape(.dragPreview, .rect(cornerRadius: 10)) } .onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool)) - .onTapGesture { - if tool.selectedPen === pen { - withAnimation { - tool.unselectPen(pen) - } - } else { - withAnimation { - tool.selectPen(pen) - } - } - } .offset(x: tool.selectedPen === pen ? 0 : 28) } var newPenButton: some View { - Button(action: { + Button { let pen = PenObject.createObject(\.viewContext, penStyle: .marker) pen.color = [Color.red, Color.blue, Color.green, Color.black, Color.orange].randomElement()!.components pen.isSelected = true @@ -102,12 +124,40 @@ struct PenToolView: View { pen.orderIndex = Int16(tool.pens.count) let _pen = Pen(object: pen) tool.addPen(_pen) - }) { - Image(systemName: "plus") - .font(.title3) + } label: { + Image(systemName: "pencil.tip.crop.circle.badge.plus") + .font(.title2) .contentShape(.circle) } .hoverEffect(.lift) .padding(10) } + + var reorderCancelButton: some View { + Button { + tool.isReordering = false + } label: { + Image(systemName: "xmark.circle") + .font(.title2) + .contentShape(.circle) + } + .hoverEffect(.lift) + .padding(10) + } + + func penPreview(_ pen: Pen) -> some View { + ZStack { + if let tip = pen.style.icon.tip { + Image(tip) + .resizable() + .renderingMode(.template) + .foregroundStyle(Color.rgba(from: pen.color)) + } + Image(pen.style.icon.base) + .resizable() + } + .frame(width: width * factor, height: height * factor) + .padding(.vertical, 5) + .padding(.leading, 10) + } }