mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-24 10:21:23 +01:00
feat: add reordering and deleting
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
|
||||
EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuableViewModifier.swift; sourceTree = "<group>"; };
|
||||
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
|
||||
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
|
||||
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
|
||||
@@ -232,6 +234,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */,
|
||||
EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */,
|
||||
);
|
||||
path = ViewModifiers;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// ContextMenuableViewModifier.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 5/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct ContextMenuableViewModifier<MenuContent: View>: 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<MenuContent: View>(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent) -> some View {
|
||||
modifier(ContextMenuableViewModifier(condition: condition, menuItems: menuItems))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user