diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 8ce3dd4..9b61856 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -21,17 +21,21 @@ EC1815082C2D980B00541369 /* Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815072C2D980B00541369 /* Sort.swift */; }; EC18150A2C2DA09E00541369 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815092C2DA09E00541369 /* Filter.swift */; }; EC18150D2C2DAC3700541369 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC18150C2C2DAC3700541369 /* Placeholder.swift */; }; - EC18150F2C2DB13200541369 /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC18150E2C2DB13200541369 /* Date++.swift */; }; EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; + EC2002D52C416033002EBD5F /* FileCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002D42C416033002EBD5F /* FileCommands.swift */; }; + EC2002D72C4160EF002EBD5F /* EditCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002D62C4160EF002EBD5F /* EditCommands.swift */; }; + EC2002D92C4161ED002EBD5F /* ViewCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002D82C4161ED002EBD5F /* ViewCommands.swift */; }; + EC2002DD2C4163E8002EBD5F /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002DC2C4163E8002EBD5F /* AppCommands.swift */; }; + EC2002E12C416470002EBD5F /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002E02C416470002EBD5F /* Shortcut.swift */; }; + EC2002E52C416551002EBD5F /* Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002E42C416551002EBD5F /* Shortcuts.swift */; }; + EC2002E92C4167C5002EBD5F /* ShortcutKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002E82C4167C5002EBD5F /* ShortcutKey.swift */; }; + EC2002ED2C417B68002EBD5F /* AppScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002EC2C417B68002EBD5F /* AppScene.swift */; }; + EC2002F02C417BF1002EBD5F /* ActiveSceneKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2002EF2C417BF1002EBD5F /* ActiveSceneKey.swift */; }; EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; }; EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; }; EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF52C0F600D005DB0AF /* Box.swift */; }; EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF72C0F601A005DB0AF /* Node.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 */; }; EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; }; - EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; }; EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC37FB112C1B2DD90008D976 /* ToolSelection.swift */; }; EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC42F7842C25267000E86E96 /* ElementGroup.swift */; }; EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; @@ -39,9 +43,15 @@ EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; }; EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */; }; EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */; }; + EC6E3BD72C43C6A400DD20F3 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6E3BD62C43C6A400DD20F3 /* Application.swift */; }; + EC6E3BD92C43C6C000DD20F3 /* OnDismissSearchViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6E3BD82C43C6C000DD20F3 /* OnDismissSearchViewModifier.swift */; }; + EC6E3BDB2C43C78700DD20F3 /* NavigationSplitViewVisibility++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6E3BDA2C43C78700DD20F3 /* NavigationSplitViewVisibility++.swift */; }; + EC6E3BDE2C43D5A500DD20F3 /* SidebarVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC6E3BDC2C43D5A500DD20F3 /* SidebarVisibility.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 */; }; + EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC86C5812C4010CC00C07D21 /* PhotoDock.swift */; }; + EC8C9DCE2C39882500A8F3C4 /* NSSyncScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */; }; EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */; }; EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */; }; EC8F54B02C2AF5E9001C7C74 /* LineGridContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AF2C2AF5E9001C7C74 /* LineGridContext.swift */; }; @@ -61,7 +71,6 @@ ECA7389C2BE601AF00A4542E /* PointGridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389B2BE601AF00A4542E /* PointGridVertex.swift */; }; ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389D2BE601CB00A4542E /* QuadVertex.swift */; }; ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */; }; - ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A22BE6020A00A4542E /* CGFloat++.swift */; }; ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A52BE6023F00A4542E /* GridUniforms.swift */; }; ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A72BE6025900A4542E /* GraphicUniforms.swift */; }; ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738A92BE6026D00A4542E /* Uniforms.swift */; }; @@ -88,22 +97,11 @@ ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */; }; ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */; }; ECA738E42BE6110800A4542E /* Drawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E32BE6110800A4542E /* Drawable.swift */; }; - ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E52BE611FD00A4542E /* CGRect++.swift */; }; - ECA738E82BE6120F00A4542E /* Color++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E72BE6120F00A4542E /* Color++.swift */; }; - ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E92BE6122E00A4542E /* CGPoint++.swift */; }; - ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */; }; - ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */; }; - ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738EF2BE6127700A4542E /* CGSize++.swift */; }; - ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F12BE6128F00A4542E /* Collection++.swift */; }; - ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; - ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; }; ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; }; ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; }; - ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */; }; ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; }; - ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A42C1EB4CC00B2699A /* Data++.swift */; }; ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; }; ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; }; ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; }; @@ -113,11 +111,30 @@ ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; }; ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */; }; ECDDD40D2C366B3B00DF9D5E /* PreviewRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */; }; - ECDDD40F2C368B2700DF9D5E /* MTLTexture++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */; }; ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; }; ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; }; ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; }; ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; + ECF7B2D02C39169C004D2C57 /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2BD2C39169C004D2C57 /* Array++.swift */; }; + ECF7B2D12C39169C004D2C57 /* CGAffineTransform++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2BE2C39169C004D2C57 /* CGAffineTransform++.swift */; }; + ECF7B2D22C39169C004D2C57 /* CGFloat++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2BF2C39169C004D2C57 /* CGFloat++.swift */; }; + ECF7B2D32C39169C004D2C57 /* CGPoint++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C02C39169C004D2C57 /* CGPoint++.swift */; }; + ECF7B2D42C39169C004D2C57 /* CGRect++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C12C39169C004D2C57 /* CGRect++.swift */; }; + ECF7B2D52C39169C004D2C57 /* CGSize++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C22C39169C004D2C57 /* CGSize++.swift */; }; + ECF7B2D62C39169C004D2C57 /* Collection++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C32C39169C004D2C57 /* Collection++.swift */; }; + ECF7B2D72C39169C004D2C57 /* Color++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C42C39169C004D2C57 /* Color++.swift */; }; + ECF7B2D82C39169C004D2C57 /* Data++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C52C39169C004D2C57 /* Data++.swift */; }; + ECF7B2D92C39169C004D2C57 /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C62C39169C004D2C57 /* Date++.swift */; }; + ECF7B2DA2C39169C004D2C57 /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C72C39169C004D2C57 /* Float++.swift */; }; + ECF7B2DB2C39169C004D2C57 /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C82C39169C004D2C57 /* MTLDevice++.swift */; }; + ECF7B2DC2C39169C004D2C57 /* MTLTexture++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2C92C39169C004D2C57 /* MTLTexture++.swift */; }; + ECF7B2DD2C39169C004D2C57 /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2CA2C39169C004D2C57 /* NSManagedObject++.swift */; }; + ECF7B2DE2C39169C004D2C57 /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2CB2C39169C004D2C57 /* NSManagedObjectContext++.swift */; }; + ECF7B2DF2C39169C004D2C57 /* simd_float4x4++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2CC2C39169C004D2C57 /* simd_float4x4++.swift */; }; + ECF7B2E02C39169C004D2C57 /* Image++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2CD2C39169C004D2C57 /* Image++.swift */; }; + ECF7B2E12C39169C004D2C57 /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2CE2C39169C004D2C57 /* View++.swift */; }; + ECF7B2E42C39174D004D2C57 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2E32C39174D004D2C57 /* Platform.swift */; }; + ECF7B2E72C39544E004D2C57 /* NSCenterClipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF7B2E62C39544E004D2C57 /* NSCenterClipView.swift */; }; ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; }; ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; }; ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15232BEF223300455818 /* GraphicContextObject.swift */; }; @@ -141,17 +158,21 @@ EC1815072C2D980B00541369 /* Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sort.swift; sourceTree = ""; }; EC1815092C2DA09E00541369 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; EC18150C2C2DAC3700541369 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; - EC18150E2C2DB13200541369 /* Date++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date++.swift"; sourceTree = ""; }; EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; + EC2002D42C416033002EBD5F /* FileCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCommands.swift; sourceTree = ""; }; + EC2002D62C4160EF002EBD5F /* EditCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCommands.swift; sourceTree = ""; }; + EC2002D82C4161ED002EBD5F /* ViewCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCommands.swift; sourceTree = ""; }; + EC2002DC2C4163E8002EBD5F /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = ""; }; + EC2002E02C416470002EBD5F /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; + EC2002E42C416551002EBD5F /* Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; + EC2002E82C4167C5002EBD5F /* ShortcutKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutKey.swift; sourceTree = ""; }; + EC2002EC2C417B68002EBD5F /* AppScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScene.swift; sourceTree = ""; }; + EC2002EF2C417BF1002EBD5F /* ActiveSceneKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSceneKey.swift; sourceTree = ""; }; EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = ""; }; EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; EC2BEBF52C0F600D005DB0AF /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; EC2BEBF72C0F601A005DB0AF /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.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 = ""; }; EC3565592BF060D900A4E0BF /* Quad.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Quad.metal; sourceTree = ""; }; - EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = ""; }; EC37FB112C1B2DD90008D976 /* ToolSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolSelection.swift; sourceTree = ""; }; EC42F7842C25267000E86E96 /* ElementGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementGroup.swift; sourceTree = ""; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; @@ -160,10 +181,16 @@ EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoBackgroundRenderPass.swift; sourceTree = ""; }; EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingAverage.swift; sourceTree = ""; }; + EC6E3BD62C43C6A400DD20F3 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + EC6E3BD82C43C6C000DD20F3 /* OnDismissSearchViewModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnDismissSearchViewModifier.swift; sourceTree = ""; }; + EC6E3BDA2C43C78700DD20F3 /* NavigationSplitViewVisibility++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NavigationSplitViewVisibility++.swift"; sourceTree = ""; }; + EC6E3BDC2C43D5A500DD20F3 /* SidebarVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarVisibility.swift; 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 = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + EC86C5812C4010CC00C07D21 /* PhotoDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoDock.swift; sourceTree = ""; }; + EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSyncScrollView.swift; sourceTree = ""; }; EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridMode.swift; sourceTree = ""; }; EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGridVertex.swift; sourceTree = ""; }; EC8F54AF2C2AF5E9001C7C74 /* LineGridContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGridContext.swift; sourceTree = ""; }; @@ -183,7 +210,6 @@ ECA7389B2BE601AF00A4542E /* PointGridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointGridVertex.swift; sourceTree = ""; }; ECA7389D2BE601CB00A4542E /* QuadVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadVertex.swift; sourceTree = ""; }; ECA7389F2BE601E400A4542E /* ViewPortVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPortVertex.swift; sourceTree = ""; }; - ECA738A22BE6020A00A4542E /* CGFloat++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = ""; }; ECA738A52BE6023F00A4542E /* GridUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridUniforms.swift; sourceTree = ""; }; ECA738A72BE6025900A4542E /* GraphicUniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUniforms.swift; sourceTree = ""; }; ECA738A92BE6026D00A4542E /* Uniforms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uniforms.swift; sourceTree = ""; }; @@ -210,22 +236,11 @@ ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserRenderPass.swift; sourceTree = ""; }; ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicRenderPass.swift; sourceTree = ""; }; ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = ""; }; - ECA738E52BE611FD00A4542E /* CGRect++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = ""; }; - ECA738E72BE6120F00A4542E /* Color++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = ""; }; - ECA738E92BE6122E00A4542E /* CGPoint++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = ""; }; - ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = ""; }; - ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = ""; }; - ECA738EF2BE6127700A4542E /* CGSize++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = ""; }; - ECA738F12BE6128F00A4542E /* Collection++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = ""; }; - ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; - ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = ""; }; ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; - ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = ""; }; ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = ""; }; - ECC995A42C1EB4CC00B2699A /* Data++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data++.swift"; sourceTree = ""; }; ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = ""; }; ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = ""; }; @@ -235,11 +250,31 @@ ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = ""; }; ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToolbar.swift; sourceTree = ""; }; ECDDD40C2C366B3B00DF9D5E /* PreviewRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRenderPass.swift; sourceTree = ""; }; - ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLTexture++.swift"; sourceTree = ""; }; ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = ""; }; ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = ""; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; + ECF7B2BD2C39169C004D2C57 /* Array++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; + ECF7B2BE2C39169C004D2C57 /* CGAffineTransform++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGAffineTransform++.swift"; sourceTree = ""; }; + ECF7B2BF2C39169C004D2C57 /* CGFloat++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat++.swift"; sourceTree = ""; }; + ECF7B2C02C39169C004D2C57 /* CGPoint++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint++.swift"; sourceTree = ""; }; + ECF7B2C12C39169C004D2C57 /* CGRect++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect++.swift"; sourceTree = ""; }; + ECF7B2C22C39169C004D2C57 /* CGSize++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGSize++.swift"; sourceTree = ""; }; + ECF7B2C32C39169C004D2C57 /* Collection++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection++.swift"; sourceTree = ""; }; + ECF7B2C42C39169C004D2C57 /* Color++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Color++.swift"; sourceTree = ""; }; + ECF7B2C52C39169C004D2C57 /* Data++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data++.swift"; sourceTree = ""; }; + ECF7B2C62C39169C004D2C57 /* Date++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date++.swift"; sourceTree = ""; }; + ECF7B2C72C39169C004D2C57 /* Float++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = ""; }; + ECF7B2C82C39169C004D2C57 /* MTLDevice++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; + ECF7B2C92C39169C004D2C57 /* MTLTexture++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MTLTexture++.swift"; sourceTree = ""; }; + ECF7B2CA2C39169C004D2C57 /* NSManagedObject++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; + ECF7B2CB2C39169C004D2C57 /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; + ECF7B2CC2C39169C004D2C57 /* simd_float4x4++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "simd_float4x4++.swift"; sourceTree = ""; }; + ECF7B2CD2C39169C004D2C57 /* Image++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image++.swift"; sourceTree = ""; }; + ECF7B2CE2C39169C004D2C57 /* View++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; + ECF7B2E32C39174D004D2C57 /* Platform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; + ECF7B2E62C39544E004D2C57 /* NSCenterClipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSCenterClipView.swift; sourceTree = ""; }; + ECF7B2E82C395A8E004D2C57 /* Memola.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Memola.entitlements; sourceTree = ""; }; ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = ""; }; ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = ""; }; ECFA15232BEF223300455818 /* GraphicContextObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicContextObject.swift; sourceTree = ""; }; @@ -317,6 +352,7 @@ EC1437B42BE748E60022C903 /* Views */ = { isa = PBXGroup; children = ( + EC8C9DCC2C3987FD00A8F3C4 /* AppKit */, ECA738AC2BE60CC600A4542E /* DrawingView.swift */, ); path = Views; @@ -356,6 +392,53 @@ path = Toolbar; sourceTree = ""; }; + EC2002D32C416002002EBD5F /* Commands */ = { + isa = PBXGroup; + children = ( + EC2002DC2C4163E8002EBD5F /* AppCommands.swift */, + EC2002D62C4160EF002EBD5F /* EditCommands.swift */, + EC2002D42C416033002EBD5F /* FileCommands.swift */, + EC2002D82C4161ED002EBD5F /* ViewCommands.swift */, + ); + path = Commands; + sourceTree = ""; + }; + EC2002DE2C41645A002EBD5F /* Shortcut */ = { + isa = PBXGroup; + children = ( + EC2002D32C416002002EBD5F /* Commands */, + EC2002DF2C416466002EBD5F /* Core */, + EC2002E72C4167B1002EBD5F /* EnvironmentValues */, + ); + path = Shortcut; + sourceTree = ""; + }; + EC2002DF2C416466002EBD5F /* Core */ = { + isa = PBXGroup; + children = ( + EC2002E02C416470002EBD5F /* Shortcut.swift */, + EC2002E42C416551002EBD5F /* Shortcuts.swift */, + ); + path = Core; + sourceTree = ""; + }; + EC2002E72C4167B1002EBD5F /* EnvironmentValues */ = { + isa = PBXGroup; + children = ( + EC2002E82C4167C5002EBD5F /* ShortcutKey.swift */, + ); + path = EnvironmentValues; + sourceTree = ""; + }; + EC2002EE2C417BBF002EBD5F /* AppScene */ = { + isa = PBXGroup; + children = ( + EC2002EC2C417B68002EBD5F /* AppScene.swift */, + EC2002EF2C417BF1002EBD5F /* ActiveSceneKey.swift */, + ); + path = AppScene; + sourceTree = ""; + }; EC2BEBF22C0F5FE1005DB0AF /* RTree */ = { isa = PBXGroup; children = ( @@ -412,6 +495,7 @@ EC50500B2BF6673300B4D86E /* ViewModifiers */ = { isa = PBXGroup; children = ( + EC6E3BD82C43C6C000DD20F3 /* OnDismissSearchViewModifier.swift */, EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */, EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */, ); @@ -434,6 +518,14 @@ path = Algorithms; sourceTree = ""; }; + EC6E3BDD2C43D5A500DD20F3 /* SidebarVisibility */ = { + isa = PBXGroup; + children = ( + EC6E3BDC2C43D5A500DD20F3 /* SidebarVisibility.swift */, + ); + path = SidebarVisibility; + sourceTree = ""; + }; EC7F6BDF2BE5E6E300A34A7B = { isa = PBXGroup; children = ( @@ -454,14 +546,16 @@ isa = PBXGroup; children = ( ECA738762BE5EE4E00A4542E /* App */, + EC2002DE2C41645A002EBD5F /* Shortcut */, ECA7387E2BE5FE4200A4542E /* Canvas */, - EC5050102BF670EE00B4D86E /* Config */, EC50500A2BF6672000B4D86E /* Components */, - ECA738A12BE601F700A4542E /* Extensions */, + EC5050102BF670EE00B4D86E /* Config */, ECA738772BE5EEE800A4542E /* Features */, + ECF7B2E82C395A8E004D2C57 /* Memola.entitlements */, ECA738FA2BE61B1700A4542E /* Persistence */, EC7F6BF12BE5E6E400A34A7B /* Preview Content */, ECA738802BE5FE6000A4542E /* Resources */, + ECF7B2E52C391DFA004D2C57 /* Utilies */, ); path = Memola; sourceTree = ""; @@ -474,6 +568,23 @@ path = "Preview Content"; sourceTree = ""; }; + EC86C5802C4010BE00C07D21 /* PhotoDock */ = { + isa = PBXGroup; + children = ( + EC86C5812C4010CC00C07D21 /* PhotoDock.swift */, + ); + path = PhotoDock; + sourceTree = ""; + }; + EC8C9DCC2C3987FD00A8F3C4 /* AppKit */ = { + isa = PBXGroup; + children = ( + ECF7B2E62C39544E004D2C57 /* NSCenterClipView.swift */, + EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */, + ); + path = AppKit; + sourceTree = ""; + }; EC8F54AA2C2ACD9D001C7C74 /* Grid */ = { isa = PBXGroup; children = ( @@ -486,6 +597,7 @@ isa = PBXGroup; children = ( EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */, + EC6E3BD62C43C6A400DD20F3 /* Application.swift */, ); path = App; sourceTree = ""; @@ -512,6 +624,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + EC86C5802C4010BE00C07D21 /* PhotoDock */, ECDAC0792C318DAF0000ED77 /* ElementToolbar */, ECBE52942C1D58F5006BDB3D /* PhotoPreview */, EC1B783B2BFA0AAC005A34E2 /* Toolbar */, @@ -613,31 +726,6 @@ path = Vertices; sourceTree = ""; }; - ECA738A12BE601F700A4542E /* Extensions */ = { - isa = PBXGroup; - children = ( - ECA738F32BE612A000A4542E /* Array++.swift */, - ECA738EB2BE6124E00A4542E /* CGAffineTransform++.swift */, - ECA738A22BE6020A00A4542E /* CGFloat++.swift */, - ECA738E92BE6122E00A4542E /* CGPoint++.swift */, - ECA738E52BE611FD00A4542E /* CGRect++.swift */, - ECA738EF2BE6127700A4542E /* CGSize++.swift */, - ECA738F12BE6128F00A4542E /* Collection++.swift */, - ECA738E72BE6120F00A4542E /* Color++.swift */, - ECA738F52BE612B700A4542E /* MTLDevice++.swift */, - ECA738ED2BE6125D00A4542E /* simd_float4x4++.swift */, - EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */, - EC3565532BEFC6AD00A4E0BF /* View++.swift */, - EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */, - EC35655B2BF0712A00A4E0BF /* Float++.swift */, - ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */, - ECC995A42C1EB4CC00B2699A /* Data++.swift */, - EC18150E2C2DB13200541369 /* Date++.swift */, - ECDDD40E2C368B2700DF9D5E /* MTLTexture++.swift */, - ); - path = Extensions; - sourceTree = ""; - }; ECA738A42BE6022F00A4542E /* Uniforms */ = { isa = PBXGroup; children = ( @@ -871,6 +959,51 @@ path = Core; sourceTree = ""; }; + ECF7B2CF2C39169C004D2C57 /* Extensions */ = { + isa = PBXGroup; + children = ( + EC6E3BDA2C43C78700DD20F3 /* NavigationSplitViewVisibility++.swift */, + ECF7B2BD2C39169C004D2C57 /* Array++.swift */, + ECF7B2BE2C39169C004D2C57 /* CGAffineTransform++.swift */, + ECF7B2BF2C39169C004D2C57 /* CGFloat++.swift */, + ECF7B2C02C39169C004D2C57 /* CGPoint++.swift */, + ECF7B2C12C39169C004D2C57 /* CGRect++.swift */, + ECF7B2C22C39169C004D2C57 /* CGSize++.swift */, + ECF7B2C32C39169C004D2C57 /* Collection++.swift */, + ECF7B2C42C39169C004D2C57 /* Color++.swift */, + ECF7B2C52C39169C004D2C57 /* Data++.swift */, + ECF7B2C62C39169C004D2C57 /* Date++.swift */, + ECF7B2C72C39169C004D2C57 /* Float++.swift */, + ECF7B2C82C39169C004D2C57 /* MTLDevice++.swift */, + ECF7B2C92C39169C004D2C57 /* MTLTexture++.swift */, + ECF7B2CA2C39169C004D2C57 /* NSManagedObject++.swift */, + ECF7B2CB2C39169C004D2C57 /* NSManagedObjectContext++.swift */, + ECF7B2CC2C39169C004D2C57 /* simd_float4x4++.swift */, + ECF7B2CD2C39169C004D2C57 /* Image++.swift */, + ECF7B2CE2C39169C004D2C57 /* View++.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + ECF7B2E22C39172D004D2C57 /* Platform */ = { + isa = PBXGroup; + children = ( + ECF7B2E32C39174D004D2C57 /* Platform.swift */, + ); + path = Platform; + sourceTree = ""; + }; + ECF7B2E52C391DFA004D2C57 /* Utilies */ = { + isa = PBXGroup; + children = ( + EC6E3BDD2C43D5A500DD20F3 /* SidebarVisibility */, + EC2002EE2C417BBF002EBD5F /* AppScene */, + ECF7B2CF2C39169C004D2C57 /* Extensions */, + ECF7B2E22C39172D004D2C57 /* Platform */, + ); + path = Utilies; + sourceTree = ""; + }; ECFA151E2BEF21BE00455818 /* Objects */ = { isa = PBXGroup; children = ( @@ -981,73 +1114,84 @@ EC8F54B02C2AF5E9001C7C74 /* LineGridContext.swift in Sources */, EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */, ECA738912BE600F500A4542E /* Cache.metal in Sources */, + ECF7B2D82C39169C004D2C57 /* Data++.swift in Sources */, ECA7389C2BE601AF00A4542E /* PointGridVertex.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, EC01511E2C305CA9008A115E /* DashboardView.swift in Sources */, EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */, - ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */, + EC2002F02C417BF1002EBD5F /* ActiveSceneKey.swift in Sources */, + EC2002E12C416470002EBD5F /* Shortcut.swift in Sources */, EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */, ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */, - ECA738E82BE6120F00A4542E /* Color++.swift in Sources */, ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */, + EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */, ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, - EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */, ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, + ECF7B2D12C39169C004D2C57 /* CGAffineTransform++.swift in Sources */, + EC2002E52C416551002EBD5F /* Shortcuts.swift in Sources */, + EC2002DD2C4163E8002EBD5F /* AppCommands.swift in Sources */, EC1815082C2D980B00541369 /* Sort.swift in Sources */, + EC6E3BD92C43C6C000DD20F3 /* OnDismissSearchViewModifier.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, ECDDD40D2C366B3B00DF9D5E /* PreviewRenderPass.swift in Sources */, + ECF7B2E12C39169C004D2C57 /* View++.swift in Sources */, ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, + ECF7B2DA2C39169C004D2C57 /* Float++.swift in Sources */, ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */, + EC6E3BDE2C43D5A500DD20F3 /* SidebarVisibility.swift in Sources */, ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */, ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */, EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */, + EC6E3BDB2C43C78700DD20F3 /* NavigationSplitViewVisibility++.swift in Sources */, ECA738C62BE60E9D00A4542E /* EraserPenStyle.swift in Sources */, - ECA738EA2BE6122E00A4542E /* CGPoint++.swift in Sources */, ECA738A62BE6023F00A4542E /* GridUniforms.swift in Sources */, ECA738D72BE60FC100A4542E /* SolidPointStrokeGenerator.swift in Sources */, - EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */, - ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */, EC2BEBF62C0F600D005DB0AF /* Box.swift in Sources */, ECA738832BE5FEFE00A4542E /* RenderPass.swift in Sources */, ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */, + ECF7B2DC2C39169C004D2C57 /* MTLTexture++.swift in Sources */, ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, + ECF7B2DE2C39169C004D2C57 /* NSManagedObjectContext++.swift in Sources */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, + EC6E3BD72C43C6A400DD20F3 /* Application.swift in Sources */, + EC8C9DCE2C39882500A8F3C4 /* NSSyncScrollView.swift in Sources */, + ECF7B2D72C39169C004D2C57 /* Color++.swift in Sources */, EC01512C2C306BEF008A115E /* MemoCard.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, + ECF7B2D52C39169C004D2C57 /* CGSize++.swift in Sources */, ECA739082BE623F300A4542E /* PenDock.swift in Sources */, ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */, - ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */, ECA7388C2BE6009600A4542E /* Textures.swift in Sources */, ECFC51272BF8885700D0D051 /* ColorPicker.swift in Sources */, ECA738B82BE60DDC00A4542E /* HistoryEvent.swift in Sources */, EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */, ECA738952BE6012D00A4542E /* ViewPort.metal in Sources */, ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */, + ECF7B2DD2C39169C004D2C57 /* NSManagedObject++.swift in Sources */, ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */, ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */, EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */, - ECA738F02BE6127700A4542E /* CGSize++.swift in Sources */, + EC2002ED2C417B68002EBD5F /* AppScene.swift in Sources */, ECFA15242BEF223300455818 /* GraphicContextObject.swift in Sources */, - EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */, - ECA738EC2BE6124E00A4542E /* CGAffineTransform++.swift in Sources */, - EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */, ECA738E22BE610D000A4542E /* GraphicRenderPass.swift in Sources */, ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */, ECA738DC2BE6108D00A4542E /* StrokeRenderPass.swift in Sources */, - ECA738F42BE612A000A4542E /* Array++.swift in Sources */, + ECF7B2E72C39544E004D2C57 /* NSCenterClipView.swift in Sources */, + ECF7B2D22C39169C004D2C57 /* CGFloat++.swift in Sources */, EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */, EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */, - EC18150F2C2DB13200541369 /* Date++.swift in Sources */, ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */, + ECF7B2D42C39169C004D2C57 /* CGRect++.swift in Sources */, + ECF7B2D62C39169C004D2C57 /* Collection++.swift in Sources */, EC0151232C306089008A115E /* Sidebar.swift in Sources */, ECD12A932C1B062000B96E12 /* Photo.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */, ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, - ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, + EC2002E92C4167C5002EBD5F /* ShortcutKey.swift in Sources */, EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */, EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */, @@ -1059,30 +1203,36 @@ EC01512A2C306935008A115E /* MemoGrid.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, EC0151262C3067B9008A115E /* TrashView.swift in Sources */, + ECF7B2D92C39169C004D2C57 /* Date++.swift in Sources */, ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */, ECFA15282BEF225000455818 /* QuadObject.swift in Sources */, ECA738932BE6011100A4542E /* Stroke.metal in Sources */, ECA738B62BE60DCD00A4542E /* History.swift in Sources */, ECA738D22BE60F7B00A4542E /* PenStroke.swift in Sources */, EC01512E2C30727F008A115E /* MemoPreview.swift in Sources */, - ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */, EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */, - ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */, + ECF7B2E42C39174D004D2C57 /* Platform.swift in Sources */, ECA738C12BE60E5300A4542E /* PenStyle.swift in Sources */, + EC2002D72C4160EF002EBD5F /* EditCommands.swift in Sources */, + ECF7B2DF2C39169C004D2C57 /* simd_float4x4++.swift in Sources */, + ECF7B2D02C39169C004D2C57 /* Array++.swift in Sources */, ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */, ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */, EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */, + EC2002D92C4161ED002EBD5F /* ViewCommands.swift in Sources */, EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */, + EC2002D52C416033002EBD5F /* FileCommands.swift in Sources */, EC2BEBF82C0F601A005DB0AF /* Node.swift in Sources */, - ECDDD40F2C368B2700DF9D5E /* MTLTexture++.swift in Sources */, - ECC995A52C1EB4CC00B2699A /* Data++.swift in Sources */, ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */, ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */, EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */, EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */, + ECF7B2DB2C39169C004D2C57 /* MTLDevice++.swift in Sources */, ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */, ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, + ECF7B2E02C39169C004D2C57 /* Image++.swift in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */, + ECF7B2D32C39169C004D2C57 /* CGPoint++.swift in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, EC18150D2C2DAC3700541369 /* Placeholder.swift in Sources */, ); @@ -1215,6 +1365,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Memola/Memola.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\""; @@ -1231,12 +1383,13 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTLLINKER_FLAGS = ""; MTL_COMPILER_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1250,6 +1403,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Memola/Memola.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\""; @@ -1266,12 +1421,13 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; MTLLINKER_FLAGS = ""; MTL_COMPILER_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.example.Memola; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Memola/App/Application.swift b/Memola/App/Application.swift new file mode 100644 index 0000000..8f473e9 --- /dev/null +++ b/Memola/App/Application.swift @@ -0,0 +1,62 @@ +// +// Application.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import Combine +import SwiftUI + +final class Application: NSObject, ObservableObject { + @Published var memoObject: MemoObject? + @Published private(set) var sidebarVisibility: SidebarVisibility = .shown +} + +extension Application { + func openMemo(_ memoObject: MemoObject?) { + self.memoObject = memoObject + } + + func closeMemo() { + self.memoObject = nil + } +} + +extension Application { + func activateSearchBar() { + #if os(macOS) + guard let toolbar = NSApp.keyWindow?.toolbar else { return } + if let search = toolbar.items.first(where: { $0.itemIdentifier.rawValue == "com.apple.SwiftUI.search" }) as? NSSearchToolbarItem { + search.beginSearchInteraction() + } + #else + #warning("TODO: implement for ipad") + #endif + } + + func toggleSidebar() { + #if os(macOS) + NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil) + #else + #warning("TODO: implement for ipad") + #endif + } + + func changeSidebarVisibility(_ visibility: SidebarVisibility) { + self.sidebarVisibility = visibility + } +} + +#if os(macOS) +extension Application: NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + NSWindow.allowsAutomaticWindowTabbing = false + UserDefaults.standard.register(defaults: ["NSQuitAlwaysKeepsWindows": false]) + } +} +#else +extension Application: UIApplicationDelegate { + func applicationDidFinishLaunching(_ application: UIApplication) { } +} +#endif diff --git a/Memola/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index c5aca74..5889ecd 100644 --- a/Memola/App/MemolaApp.swift +++ b/Memola/App/MemolaApp.swift @@ -9,11 +9,17 @@ import SwiftUI @main struct MemolaApp: App { + #if os(macOS) + @NSApplicationDelegateAdaptor(Application.self) private var application + #else + @UIApplicationDelegateAdaptor(Application.self) private var application + #endif + var body: some Scene { WindowGroup { DashboardView() .persistence(\.viewContext) - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in + .onReceive(NotificationCenter.default.publisher(for: Platform.Application.willTerminateNotification)) { _ in withPersistenceSync(\.viewContext) { context in try context.saveIfNeeded() } @@ -21,6 +27,22 @@ struct MemolaApp: App { try context.saveIfNeeded() } } + #if os(macOS) + .frame(minWidth: 1000, minHeight: 600) + #endif + .environmentObject(application) + } + #if os(macOS) + .defaultPosition(.center) + .windowResizability(.contentSize) + .defaultSize(width: 1200, height: 800) + .windowToolbarStyle(.unifiedCompact) + #endif + .commands { + AppCommands() + FileCommands() + EditCommands() + ViewCommands(application: application) } } } diff --git a/Memola/Canvas/Contexts/LineGridContext.swift b/Memola/Canvas/Contexts/LineGridContext.swift index 4dc9b09..68ecfd0 100644 --- a/Memola/Canvas/Contexts/LineGridContext.swift +++ b/Memola/Canvas/Contexts/LineGridContext.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class LineGridContext { +final class LineGridContext { var vertices: [LineGridVertex] = [] var vertexCount: Int = 0 var vertexBuffer: MTLBuffer? diff --git a/Memola/Canvas/Contexts/PointGridContext.swift b/Memola/Canvas/Contexts/PointGridContext.swift index 0f94681..2ab9281 100644 --- a/Memola/Canvas/Contexts/PointGridContext.swift +++ b/Memola/Canvas/Contexts/PointGridContext.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class PointGridContext { +final class PointGridContext { var vertices: [PointGridVertex] = [] var vertexCount: Int = 0 var vertexBuffer: MTLBuffer? diff --git a/Memola/Canvas/Contexts/ViewPortContext.swift b/Memola/Canvas/Contexts/ViewPortContext.swift index f74e6b5..c560a68 100644 --- a/Memola/Canvas/Contexts/ViewPortContext.swift +++ b/Memola/Canvas/Contexts/ViewPortContext.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class ViewPortContext { +final class ViewPortContext { var vertices: [ViewPortVertex] = [] let vertexCount: Int = 4 var vertexBuffer: MTLBuffer? diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 1798f51..ddb9f3b 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -32,7 +32,6 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { @Published var state: State = .initial @Published var zoomScale: CGFloat = .zero - @Published var locksCanvas: Bool = false @Published var gridMode: GridMode = .point @@ -85,7 +84,11 @@ extension Canvas { func save(for memoObject: MemoObject, completion: @escaping () -> Void) { state = .closing let previewImage = renderer?.drawPreview(on: self) + #if os(macOS) + memoObject.preview = previewImage?.tiffRepresentation + #else memoObject.preview = previewImage?.jpegData(compressionQuality: 0.8) + #endif withPersistenceSync(\.viewContext) { context in try context.saveIfNeeded() } @@ -105,7 +108,13 @@ extension Canvas { func updateTransform(on drawingView: DrawingView) { let bounds = CGRect(origin: .zero, size: size) let renderView = drawingView.renderView + #if os(macOS) + let drawingViewBounds = drawingView.bounds + var targetRect = drawingView.convert(drawingViewBounds, to: renderView) + targetRect.origin.y = renderView.bounds.height - targetRect.maxY + #else let targetRect = drawingView.convert(drawingView.bounds, to: renderView) + #endif let transform1 = bounds.transform(to: targetRect) let transform2 = renderView.bounds.transform(to: CGRect(x: -1.0, y: -1.0, width: 2.0, height: 2.0)) let transform3 = CGAffineTransform.identity.translatedBy(x: 0, y: 1).scaledBy(x: 1, y: -1).translatedBy(x: 0, y: 1) @@ -135,20 +144,30 @@ extension Canvas { self.previewTransform = simd_float4x4(transform) } - func updateClipBounds(_ scrollView: UIScrollView, on drawingView: DrawingView) { + func updateClipBounds(_ scrollView: Platform.ScrollView, on drawingView: DrawingView) { + #if os(macOS) + let ratio = drawingView.ratio + var bounds = scrollView.convert(scrollView.bounds, to: drawingView) + bounds.origin.y = drawingView.bounds.height - (bounds.origin.y + bounds.height) + clipBounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio)) + #else let ratio = drawingView.ratio let bounds = scrollView.convert(scrollView.bounds, to: drawingView) clipBounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio)) + #endif } } // MARK: - Zoom Scale extension Canvas { func setZoomScale(_ zoomScale: CGFloat) { - DispatchQueue.main.async { [weak self] in - guard let self else { return } + #if os(macOS) + self.zoomScale = min(max(zoomScale, minimumZoomScale), maximumZoomScale) + #else + DispatchQueue.main.async { [unowned self] in self.zoomScale = min(max(zoomScale, minimumZoomScale), maximumZoomScale) } + #endif } } @@ -162,6 +181,19 @@ extension Canvas { try context.saveIfNeeded() } } + + func toggleGridMode() { + let _gridMode: GridMode + switch gridMode { + case .none: + _gridMode = .point + case .point: + _gridMode = .line + case .line: + _gridMode = .none + } + setGridMode(_gridMode) + } } // MARK: - Stroke diff --git a/Memola/Canvas/Core/PipelineStates.swift b/Memola/Canvas/Core/PipelineStates.swift index b4e02f3..ac22028 100644 --- a/Memola/Canvas/Core/PipelineStates.swift +++ b/Memola/Canvas/Core/PipelineStates.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -struct PipelineStates { +enum PipelineStates { static func createPointGridPipelineState(from renderer: Renderer, pixelFormat: MTLPixelFormat? = nil) -> MTLRenderPipelineState? { let device = renderer.device let library = renderer.library diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift index e5292b9..3409ade 100644 --- a/Memola/Canvas/Core/Renderer.swift +++ b/Memola/Canvas/Core/Renderer.swift @@ -110,9 +110,9 @@ final class Renderer { viewPortRenderPass.draw(into: commandBuffer, on: canvas, with: self) } - func drawPreview(on canvas: Canvas) -> UIImage? { + func drawPreview(on canvas: Canvas) -> Platform.Image? { guard let commandBuffer = commandQueue.makeCommandBuffer() else { - NSLog("[Memola] - Unable to create command buffer") + NSLog("[Memola] - Unable to create command buffer for preview") return nil } strokeRenderPass.eraserRenderPass = eraserRenderPass @@ -124,6 +124,10 @@ final class Renderer { guard let cgImage = previewRenderPass.previewTexture?.getImage() else { return nil } + #if os(macOS) + return NSImage(cgImage: cgImage, size: .init(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height))).flipped(flipVertically: true) + #else return UIImage(cgImage: cgImage, scale: 1.0, orientation: .downMirrored) + #endif } } diff --git a/Memola/Canvas/Elements/Core/ElementGroup.swift b/Memola/Canvas/Elements/Core/ElementGroup.swift index 71c8668..d61365b 100644 --- a/Memola/Canvas/Elements/Core/ElementGroup.swift +++ b/Memola/Canvas/Elements/Core/ElementGroup.swift @@ -7,7 +7,7 @@ import Foundation -class ElementGroup { +final class ElementGroup { var elements: [Element] = [] var type: ElementGroupType diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Algorithms/MovingAverage.swift b/Memola/Canvas/Elements/Geometries/Stroke/Algorithms/MovingAverage.swift index ff4d6f1..242194a 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Algorithms/MovingAverage.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Algorithms/MovingAverage.swift @@ -7,7 +7,7 @@ import Foundation -class MovingAverage { +final class MovingAverage { private var sum: CGPoint private var points: [CGPoint] private var windowSize: Int diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift index 3f1aa47..f633904 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift @@ -101,7 +101,9 @@ final class PenStroke: Stroke, @unchecked Sendable { fetchRequest.predicate = NSPredicate(format: "ANY strokes == %@", stroke) do { - let erasers = try Persistence.shared.backgroundContext.fetch(fetchRequest) + let erasers = try withPersistenceContext(\.backgroundContext) { context in + try context.fetch(fetchRequest) + } return erasers } catch { NSLog("[Memola] - \(error.localizedDescription)") diff --git a/Memola/Canvas/Elements/Photo/Photo.swift b/Memola/Canvas/Elements/Photo/Photo.swift index cf2fdaa..ed6a77c 100644 --- a/Memola/Canvas/Elements/Photo/Photo.swift +++ b/Memola/Canvas/Elements/Photo/Photo.swift @@ -12,7 +12,7 @@ final class Photo: @unchecked Sendable, Equatable { var id: UUID = UUID() var size: CGSize var origin: CGPoint - var image: UIImage? + var image: Platform.Image? var url: URL? var bounds: [CGFloat] var createdAt: Date diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index a23b8eb..9cbf6c8 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -8,7 +8,7 @@ import Combine import Foundation -class History: ObservableObject { +final class History: ObservableObject { var memo: MemoObject? init(memo: MemoObject?) { diff --git a/Memola/Canvas/RTree/Node.swift b/Memola/Canvas/RTree/Node.swift index 2321961..f024183 100644 --- a/Memola/Canvas/RTree/Node.swift +++ b/Memola/Canvas/RTree/Node.swift @@ -7,7 +7,7 @@ import Foundation -class Node where T: Equatable & Comparable { +final class Node where T: Equatable & Comparable { var box: Box var value: T? var isLeaf: Bool diff --git a/Memola/Canvas/RTree/RTree.swift b/Memola/Canvas/RTree/RTree.swift index 168520f..3c69ec8 100644 --- a/Memola/Canvas/RTree/RTree.swift +++ b/Memola/Canvas/RTree/RTree.swift @@ -7,7 +7,7 @@ import Foundation -class RTree where T: Equatable & Comparable { +final class RTree where T: Equatable & Comparable { private var root: Node private let maxEntries: Int private let minEntries: Int diff --git a/Memola/Canvas/RenderPasses/CacheRenderPass.swift b/Memola/Canvas/RenderPasses/CacheRenderPass.swift index 75d4e5b..92aaf9f 100644 --- a/Memola/Canvas/RenderPasses/CacheRenderPass.swift +++ b/Memola/Canvas/RenderPasses/CacheRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class CacheRenderPass: RenderPass { +final class CacheRenderPass: RenderPass { var label: String = "Cache Render Pass" var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/RenderPasses/EraserRenderPass.swift b/Memola/Canvas/RenderPasses/EraserRenderPass.swift index 229a452..f06d781 100644 --- a/Memola/Canvas/RenderPasses/EraserRenderPass.swift +++ b/Memola/Canvas/RenderPasses/EraserRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class EraserRenderPass: RenderPass { +final class EraserRenderPass: RenderPass { var label: String = "Eraser Render Pass" var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index c13d903..697bb0d 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class GraphicRenderPass: RenderPass { +final class GraphicRenderPass: RenderPass { var label: String { "Graphic Render Pass" } var descriptor: MTLRenderPassDescriptor? var graphicTexture: MTLTexture? diff --git a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift index 86f1eb5..eaf51f3 100644 --- a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class PhotoBackgroundRenderPass: RenderPass { +final class PhotoBackgroundRenderPass: RenderPass { var label: String = "Photo Background Render Pass" var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift index 6761a0e..3a08df1 100644 --- a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class PhotoRenderPass: RenderPass { +final class PhotoRenderPass: RenderPass { var label: String = "Photo Render Pass" var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index f319616..cdbb77a 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class StrokeRenderPass: RenderPass { +final class StrokeRenderPass: RenderPass { var label: String = "Stroke Render Pass" var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift index d939bb1..cafbb60 100644 --- a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift +++ b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -class ViewPortRenderPass: RenderPass { +final class ViewPortRenderPass: RenderPass { var label: String { "View Port Render Pass"} var descriptor: MTLRenderPassDescriptor? diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 38d9b9e..12b1e2d 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -10,7 +10,7 @@ import SwiftUI import CoreData import Foundation -public class Tool: NSObject, ObservableObject { +final class Tool: NSObject, ObservableObject { let object: ToolObject @Published var pens: [Pen] = [] @@ -35,6 +35,7 @@ public class Tool: NSObject, ObservableObject { } func selectTool(_ selection: ToolSelection) { + guard self.selection != selection else { return } self.selection = selection withPersistence(\.viewContext) { [weak object] context in object?.selection = selection.rawValue @@ -127,7 +128,7 @@ public class Tool: NSObject, ObservableObject { } } - func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) { + func selectPhoto(_ image: Platform.Image, for canvasID: NSManagedObjectID) { guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: canvasID) withAnimation { @@ -136,7 +137,7 @@ public class Tool: NSObject, ObservableObject { } } - private func resizePhoto(of image: UIImage) -> (UIImage, CGSize)? { + private func resizePhoto(of image: Platform.Image) -> (Platform.Image, CGSize)? { let targetSize = CGSize(width: 512, height: 512) let size = image.size let widthRatio = targetSize.width / size.width @@ -147,6 +148,14 @@ public class Tool: NSObject, ObservableObject { ) let rect = CGRect(origin: .zero, size: targetSize) + #if os(macOS) + let newImage = NSImage(size: rect.size, flipped: false) { destRect in + NSGraphicsContext.current?.imageInterpolation = .high + image.draw(in: destRect, from: NSZeroRect, operation: .copy, fraction: 1) + return true + } + return (newImage, dimension) + #else UIGraphicsBeginImageContextWithOptions(targetSize, true, 1.0) image.draw(in: rect) let newImage = UIGraphicsGetImageFromCurrentImageContext() @@ -155,10 +164,15 @@ public class Tool: NSObject, ObservableObject { guard let newImage else { return nil } return (newImage, dimension) + #endif } - private func bookmarkPhoto(of image: UIImage, and previewImage: UIImage, in dimension: CGSize, with canvasID: NSManagedObjectID) -> PhotoItem? { + private func bookmarkPhoto(of image: Platform.Image, and previewImage: Platform.Image, in dimension: CGSize, with canvasID: NSManagedObjectID) -> PhotoItem? { + #if os(macOS) + guard let data = image.tiffRepresentation else { return nil } + #else guard let data = image.jpegData(compressionQuality: 1) else { return nil } + #endif let fileManager = FileManager.default guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 3875932..5c045d5 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -10,21 +10,21 @@ import SwiftUI import MetalKit import Foundation -class CanvasViewController: UIViewController { - let drawingView: DrawingView - let scrollView: UIScrollView = UIScrollView() - var renderView: MTKView { +final class CanvasViewController: Platform.ViewController { + private let drawingView: DrawingView + private let scrollView: Platform.ScrollView = Platform.ScrollView() + private var renderView: MTKView { drawingView.renderView } - var photoInsertGesture: UITapGestureRecognizer? + private var photoInsertGesture: Platform.TapGestureRecognizer? - let tool: Tool - let canvas: Canvas - let history: History - let renderer: Renderer + private let tool: Tool + private let canvas: Canvas + private let history: History + private let renderer: Renderer - var cancellables: Set = [] + private var cancellables: Set = [] init(tool: Tool, canvas: Canvas, history: History) { self.tool = tool @@ -47,6 +47,29 @@ class CanvasViewController: UIViewController { configureListeners() } + #if os(macOS) + + override func viewWillAppear() { + super.viewWillAppear() + resizeDocumentView() + updateDocumentBounds() + loadMemo() + } + + override func viewDidLayout() { + super.viewDidLayout() + drawingView.disableUserInteraction() + drawingView.updateDrawableSize(with: view.frame.size) + renderer.resize(on: renderView, to: renderView.drawableSize) + renderView.draw() + drawingView.enableUserInteraction() + } + + override func viewDidDisappear() { + super.viewDidDisappear() + history.resetRedo() + } + #else override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) resizeDocumentView() @@ -67,11 +90,18 @@ class CanvasViewController: UIViewController { super.viewDidDisappear(animated) history.resetRedo() } + #endif } extension CanvasViewController { - func configureViews() { + private func configureViews() { + #if os(macOS) + view.wantsLayer = true + view.layer?.backgroundColor = NSColor.white.cgColor + #else view.backgroundColor = .white + #endif + renderView.autoResizeDrawable = false renderView.enableSetNeedsDisplay = true renderView.translatesAutoresizingMaskIntoConstraints = false @@ -84,14 +114,24 @@ extension CanvasViewController { renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) + #if os(macOS) + scrollView.maxMagnification = canvas.maximumZoomScale + scrollView.minMagnification = canvas.minimumZoomScale + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = true + scrollView.allowsMagnification = true + scrollView.drawsBackground = false + scrollView.scrollerKnobStyle = .dark + #else scrollView.maximumZoomScale = canvas.maximumZoomScale scrollView.minimumZoomScale = canvas.minimumZoomScale scrollView.contentInsetAdjustmentBehavior = .never scrollView.isScrollEnabled = true scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = true - scrollView.delegate = self scrollView.backgroundColor = .clear + #endif + scrollView.delegate = self scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) @@ -102,14 +142,23 @@ extension CanvasViewController { scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) + #if os(macOS) + scrollView.contentView = NSCenterClipView() + scrollView.contentView.drawsBackground = false + scrollView.documentView = drawingView + #else scrollView.addSubview(drawingView) drawingView.backgroundColor = .clear drawingView.isUserInteractionEnabled = false + #endif } - func resizeDocumentView(to newSize: CGSize? = nil) { + private func resizeDocumentView(to newSize: CGSize? = nil) { + #if os(macOS) + scrollView.layoutSubtreeIfNeeded() + #else scrollView.layoutIfNeeded() - + #endif let size = canvas.size let widthScale = (newSize?.width ?? view.frame.width) / size.width let heightScale = (newSize?.height ?? view.frame.height) / size.height @@ -120,28 +169,48 @@ extension CanvasViewController { let newFrame = CGRect(x: 0, y: 0, width: width, height: height) drawingView.frame = newFrame + #if os(macOS) + DispatchQueue.main.async { [unowned canvas] in + canvas.setZoomScale(canvas.defaultZoomScale) + } + scrollView.contentView.setBoundsSize(newFrame.size) + let center = NSPoint(x: newFrame.midX, y: newFrame.midY) + scrollView.setMagnification(canvas.defaultZoomScale, centeredAt: center) + #else + canvas.setZoomScale(canvas.defaultZoomScale) scrollView.setZoomScale(canvas.defaultZoomScale, animated: true) centerDocumentView(to: newSize) + #endif + #if os(iOS) let offsetX = (newFrame.width * canvas.defaultZoomScale - view.frame.width) / 2 let offsetY = (newFrame.height * canvas.defaultZoomScale - view.frame.height) / 2 let point = CGPoint(x: offsetX, y: offsetY) scrollView.setContentOffset(point, animated: true) - + #endif drawingView.updateDrawableSize(with: view.frame.size) } - func centerDocumentView(to newSize: CGSize? = nil) { + #if os(iOS) + private func centerDocumentView(to newSize: CGSize? = nil) { let documentViewSize = drawingView.frame.size let scrollViewSize = newSize ?? view.frame.size let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0 let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0 self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding) } + #endif - func updateDocumentBounds() { + private func updateDocumentBounds() { + #if os(macOS) + let ratio = drawingView.ratio + var bounds = scrollView.convert(scrollView.bounds, to: drawingView) + bounds.origin.y = drawingView.bounds.height - (bounds.origin.y + bounds.height) + bounds = CGRect(origin: bounds.origin.muliply(by: ratio), size: bounds.size.multiply(by: ratio)) + #else var bounds = scrollView.bounds.muliply(by: drawingView.ratio / scrollView.zoomScale) + #endif let xDelta = bounds.minX * 0.0 let yDelta = bounds.minY * 0.0 bounds.origin.x -= xDelta @@ -156,7 +225,31 @@ extension CanvasViewController { } extension CanvasViewController { - func configureListeners() { + private func configureListeners() { + #if os(macOS) + NotificationCenter.default.publisher(for: NSScrollView.didEndLiveMagnifyNotification, object: scrollView) + .sink { [weak self] _ in + self?.updateDocumentBounds() + self?.magnificationEnded() + } + .store(in: &cancellables) + NotificationCenter.default.publisher(for: NSScrollView.willStartLiveMagnifyNotification, object: scrollView) + .sink { [weak self] _ in + self?.magnificationStarted() + } + .store(in: &cancellables) + NotificationCenter.default.publisher(for: NSScrollView.willStartLiveScrollNotification, object: scrollView) + .sink { [weak self] _ in + self?.draggingStarted() + } + .store(in: &cancellables) + NotificationCenter.default.publisher(for: NSScrollView.didEndLiveScrollNotification, object: scrollView) + .sink { [weak self] _ in + self?.updateDocumentBounds() + self?.draggingEnded() + } + .store(in: &cancellables) + #endif canvas.$state .sink { [weak self] state in self?.canvasStateChanged(state) @@ -167,11 +260,6 @@ extension CanvasViewController { self?.zoomChanged(zoomScale) } .store(in: &cancellables) - canvas.$locksCanvas - .sink { [weak self] state in - self?.lockModeChanged(state) - } - .store(in: &cancellables) canvas.$gridMode .delay(for: .milliseconds(100), scheduler: DispatchQueue.main) .sink { [weak self] mode in @@ -204,12 +292,12 @@ extension CanvasViewController { } extension CanvasViewController { - func loadMemo() { + private func loadMemo() { tool.load() canvas.load() } - func canvasStateChanged(_ state: Canvas.State) { + private func canvasStateChanged(_ state: Canvas.State) { guard state == .loaded else { return } renderView.delegate = self renderer.resize(on: renderView, to: renderView.drawableSize) @@ -230,25 +318,46 @@ extension CanvasViewController: MTKViewDelegate { } extension CanvasViewController { - func configureGestures() { - let photoInsertGesture = UITapGestureRecognizer(target: self, action: #selector(recognizeTapGesture)) + private func configureGestures() { + let photoInsertGesture = Platform.TapGestureRecognizer(target: self, action: #selector(recognizeTapGesture)) + #if os(macOS) + photoInsertGesture.numberOfClicksRequired = 1 + #else photoInsertGesture.numberOfTapsRequired = 1 + #endif self.photoInsertGesture = photoInsertGesture scrollView.addGestureRecognizer(photoInsertGesture) } - @objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) { + @objc private func recognizeTapGesture(_ gesture: Platform.TapGestureRecognizer) { guard let photoItem = tool.selectedPhotoItem else { return } withAnimation { tool.selectedPhotoItem = nil } + #if os(macOS) + let pointInLeftBottomOrigin = gesture.location(in: drawingView) + let point = CGPoint(x: pointInLeftBottomOrigin.x, y: drawingView.bounds.height - pointInLeftBottomOrigin.y) + #else let point = gesture.location(in: drawingView) + #endif let photo = canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem) history.addUndo(.photo(photo)) drawingView.draw() } } +#if os(macOS) +extension CanvasViewController: NSSyncScrollViewDelegate { + func scrollViewDidZoom(_ scrollView: NSSyncScrollView) { + canvas.setZoomScale(scrollView.magnification) + renderView.draw() + } + + func scrollViewDidScroll(_ scrollView: NSSyncScrollView) { + renderView.draw() + } +} +#else extension CanvasViewController: UIScrollViewDelegate { func viewForZooming(in scrollView: UIScrollView) -> UIView? { drawingView @@ -294,9 +403,10 @@ extension CanvasViewController: UIScrollViewDelegate { draggingEnded() } } +#endif extension CanvasViewController { - func magnificationStarted() { + private func magnificationStarted() { guard !renderer.updatesViewPort else { return } drawingView.touchCancelled() canvas.updateClipBounds(scrollView, on: drawingView) @@ -304,21 +414,21 @@ extension CanvasViewController { renderer.setUpdatesViewPort(true) } - func magnificationEnded() { + private func magnificationEnded() { renderer.setUpdatesViewPort(false) renderer.setRedrawsGraphicRender() renderView.draw() drawingView.enableUserInteraction() } - func draggingStarted() { + private func draggingStarted() { guard !renderer.updatesViewPort else { return } canvas.updateClipBounds(scrollView, on: drawingView) drawingView.disableUserInteraction() renderer.setUpdatesViewPort(true) } - func draggingEnded() { + private func draggingEnded() { renderer.setUpdatesViewPort(false) renderer.setRedrawsGraphicRender() renderView.draw() @@ -327,13 +437,13 @@ extension CanvasViewController { } extension CanvasViewController { - func penChanged(to pen: Pen?) { + private func penChanged(to pen: Pen?) { if let pen, let device = drawingView.renderView.device { pen.style.loadTexture(on: device) } } - func toolSelectionChanged(to selection: ToolSelection) { + private func toolSelectionChanged(to selection: ToolSelection) { let enablesScrolling: Bool let enablesDrawing: Bool let enablesPhotoInsertion: Bool @@ -352,23 +462,36 @@ extension CanvasViewController { enablesDrawing = false enablesPhotoInsertion = true } + #if os(macOS) + #warning("TODO: implement for macos") + #else scrollView.isScrollEnabled = enablesScrolling drawingView.isUserInteractionEnabled = enablesDrawing photoInsertGesture?.isEnabled = enablesPhotoInsertion enablesDrawing ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction() + #endif } } extension CanvasViewController { - func zoomChanged(_ zoomScale: CGFloat) { + private func zoomChanged(_ zoomScale: CGFloat) { + #if os(macOS) + let rect = scrollView.documentVisibleRect + scrollView.setMagnification(zoomScale, centeredAt: CGPoint(x: rect.midX, y: rect.midY)) + #else scrollView.setZoomScale(zoomScale, animated: true) + #endif } - func lockModeChanged(_ state: Bool) { + private func lockModeChanged(_ state: Bool) { + #if os(macOS) + #warning("TODO: implement for macos") + #else scrollView.pinchGestureRecognizer?.isEnabled = !state + #endif } - func gridModeChanged(_ mode: GridMode) { + private func gridModeChanged(_ mode: GridMode) { drawingView.disableUserInteraction() renderer.setRedrawsGraphicRender() renderView.draw() @@ -377,7 +500,7 @@ extension CanvasViewController { } extension CanvasViewController { - func historyUndid() { + private func historyUndid() { guard let event = history.undo() else { return } drawingView.disableUserInteraction() canvas.graphicContext.undoGraphic(for: event) @@ -386,7 +509,7 @@ extension CanvasViewController { drawingView.enableUserInteraction() } - func historyRedid() { + private func historyRedid() { guard let event = history.redo() else { return } drawingView.disableUserInteraction() canvas.graphicContext.redoGraphic(for: event) diff --git a/Memola/Canvas/View/Bridge/Views/AppKit/NSCenterClipView.swift b/Memola/Canvas/View/Bridge/Views/AppKit/NSCenterClipView.swift new file mode 100644 index 0000000..94d785a --- /dev/null +++ b/Memola/Canvas/View/Bridge/Views/AppKit/NSCenterClipView.swift @@ -0,0 +1,25 @@ +// +// NSCenterClipView.swift +// Memola +// +// Created by Dscyre Scotti on 7/6/24. +// + +#if canImport(AppKit) +import AppKit + +final class NSCenterClipView: NSClipView { + override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect { + var rect = super.constrainBoundsRect(proposedBounds) + if let containerView = self.documentView { + if (rect.size.width > containerView.frame.size.width) { + rect.origin.x = (containerView.frame.width - rect.width) / 2 + } + if(rect.size.height > containerView.frame.size.height) { + rect.origin.y = (containerView.frame.height - rect.height) / 2 + } + } + return rect + } +} +#endif diff --git a/Memola/Canvas/View/Bridge/Views/AppKit/NSSyncScrollView.swift b/Memola/Canvas/View/Bridge/Views/AppKit/NSSyncScrollView.swift new file mode 100644 index 0000000..4ca1a55 --- /dev/null +++ b/Memola/Canvas/View/Bridge/Views/AppKit/NSSyncScrollView.swift @@ -0,0 +1,29 @@ +// +// NSSyncScrollView.swift +// Memola +// +// Created by Dscyre Scotti on 7/6/24. +// + +#if canImport(AppKit) +import AppKit + +protocol NSSyncScrollViewDelegate: AnyObject { + func scrollViewDidZoom(_ scrollView: NSSyncScrollView) + func scrollViewDidScroll(_ scrollView: NSSyncScrollView) +} + +final class NSSyncScrollView: NSScrollView { + weak var delegate: NSSyncScrollViewDelegate? + + override func magnify(with event: NSEvent) { + super.magnify(with: event) + delegate?.scrollViewDidZoom(self) + } + + override func scrollWheel(with event: NSEvent) { + super.scrollWheel(with: event) + delegate?.scrollViewDidScroll(self) + } +} +#endif diff --git a/Memola/Canvas/View/Bridge/Views/DrawingView.swift b/Memola/Canvas/View/Bridge/Views/DrawingView.swift index 2e98577..71833c2 100644 --- a/Memola/Canvas/View/Bridge/Views/DrawingView.swift +++ b/Memola/Canvas/View/Bridge/Views/DrawingView.swift @@ -5,14 +5,14 @@ // Created by Dscyre Scotti on 5/4/24. // -import UIKit +import SwiftUI import MetalKit import Foundation -class DrawingView: UIView { - let tool: Tool - let canvas: Canvas - let history: History +final class DrawingView: Platform.View { + private let tool: Tool + private let canvas: Canvas + private let history: History let renderView: MTKView var ratio: CGFloat { canvas.size.width / bounds.width } @@ -36,7 +36,29 @@ class DrawingView: UIView { func updateDrawableSize(with size: CGSize) { renderView.drawableSize = size.multiply(by: 2) } + + #if os(macOS) + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + let pointInLeftBottomOrigin = convert(event.locationInWindow, from: nil) + let point = CGPoint(x: pointInLeftBottomOrigin.x, y: bounds.height - pointInLeftBottomOrigin.y) + touchBegan(at: point) + } + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + let pointInLeftBottomOrigin = convert(event.locationInWindow, from: nil) + let point = CGPoint(x: pointInLeftBottomOrigin.x, y: bounds.height - pointInLeftBottomOrigin.y) + touchMoved(to: point) + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + let pointInLeftBottomOrigin = convert(event.locationInWindow, from: nil) + let point = CGPoint(x: pointInLeftBottomOrigin.x, y: bounds.height - pointInLeftBottomOrigin.y) + touchEnded(at: point) + } + #else override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { !canvas.hasValidStroke } @@ -78,6 +100,7 @@ class DrawingView: UIView { let point = touch.preciseLocation(in: self) touchEnded(at: point) } + #endif func touchBegan(at point: CGPoint) { guard !disablesUserInteraction else { return } diff --git a/Memola/Canvas/View/Canvas/CanvasView.swift b/Memola/Canvas/View/Canvas/CanvasView.swift index d697358..ba1f52e 100644 --- a/Memola/Canvas/View/Canvas/CanvasView.swift +++ b/Memola/Canvas/View/Canvas/CanvasView.swift @@ -7,14 +7,28 @@ import SwiftUI -struct CanvasView: UIViewControllerRepresentable { - @ObservedObject var tool: Tool - @ObservedObject var canvas: Canvas - @ObservedObject var history: History +struct CanvasView: Platform.ViewControllerRepresentable { + @ObservedObject private var tool: Tool + @ObservedObject private var canvas: Canvas + @ObservedObject private var history: History + init(tool: Tool, canvas: Canvas, history: History) { + self.tool = tool + self.canvas = canvas + self.history = history + } + + #if os(macOS) + func makeNSViewController(context: Context) -> CanvasViewController { + CanvasViewController(tool: tool, canvas: canvas, history: history) + } + + func updateNSViewController(_ nsViewController: CanvasViewController, context: Context) { } + #else func makeUIViewController(context: Context) -> CanvasViewController { CanvasViewController(tool: tool, canvas: canvas, history: history) } func updateUIViewController(_ uiViewController: CanvasViewController, context: Context) { } + #endif } diff --git a/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift b/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift index ee1a3f2..3b0e4d0 100644 --- a/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift +++ b/Memola/Components/ViewModifiers/ContextMenuViewModifier.swift @@ -8,10 +8,16 @@ import SwiftUI import Foundation -struct ContextMenuViewModifier: ViewModifier { - let condition: Bool - let menuItems: () -> MenuContent - let preview: () -> Preview +private struct ContextMenuViewModifier: ViewModifier { + private let condition: Bool + private let menuItems: () -> MenuContent + private let preview: () -> Preview + + init(condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent, @ViewBuilder preview: @escaping () -> Preview) { + self.condition = condition + self.menuItems = menuItems + self.preview = preview + } @ViewBuilder func body(content: Content) -> some View { diff --git a/Memola/Components/ViewModifiers/OnDismissSearchViewModifier.swift b/Memola/Components/ViewModifiers/OnDismissSearchViewModifier.swift new file mode 100644 index 0000000..6f5f973 --- /dev/null +++ b/Memola/Components/ViewModifiers/OnDismissSearchViewModifier.swift @@ -0,0 +1,33 @@ +// +// OnDismissSearchViewModifier.swift +// Memola +// +// Created by Dscyre Scotti on 7/14/24. +// + +import SwiftUI + +private struct OnDismissSearchViewModifier: ViewModifier { + @Environment(\.dismissSearch) private var dismissSearch + + @Binding private var isActive: Bool + + init(isActive: Binding) { + self._isActive = isActive + } + + func body(content: Content) -> some View { + content + .onChange(of: isActive) { oldValue, newValue in + if !newValue { + dismissSearch() + } + } + } +} + +extension View { + func onDismissSearch(isActive: Binding) -> some View { + modifier(OnDismissSearchViewModifier(isActive: isActive)) + } +} diff --git a/Memola/Components/ViewModifiers/OnDragViewModifier.swift b/Memola/Components/ViewModifiers/OnDragViewModifier.swift index b60d540..1305779 100644 --- a/Memola/Components/ViewModifiers/OnDragViewModifier.swift +++ b/Memola/Components/ViewModifiers/OnDragViewModifier.swift @@ -8,10 +8,16 @@ import SwiftUI import Foundation -struct OnDragViewModifier: ViewModifier { - let condition: Bool - let data: () -> NSItemProvider - let preview: () -> Preview +private struct OnDragViewModifier: ViewModifier { + private let condition: Bool + private let data: () -> NSItemProvider + private let preview: () -> Preview + + init(condition: Bool, data: @escaping () -> NSItemProvider, @ViewBuilder preview: @escaping () -> Preview) { + self.condition = condition + self.data = data + self.preview = preview + } @ViewBuilder func body(content: Content) -> some View { diff --git a/Memola/Components/Views/CameraView/CameraView.swift b/Memola/Components/Views/CameraView/CameraView.swift index 4be3e8b..25abccd 100644 --- a/Memola/Components/Views/CameraView/CameraView.swift +++ b/Memola/Components/Views/CameraView/CameraView.swift @@ -7,13 +7,19 @@ import SwiftUI +#if os(iOS) struct CameraView: UIViewControllerRepresentable { - @Binding var image: UIImage? + @Binding private var image: UIImage? - @ObservedObject var canvas: Canvas + @ObservedObject private var canvas: Canvas @Environment(\.dismiss) private var dismiss + init(image: Binding, canvas: Canvas) { + self._image = image + self.canvas = canvas + } + func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera @@ -44,3 +50,4 @@ struct CameraView: UIViewControllerRepresentable { } } } +#endif diff --git a/Memola/Components/Views/ColorPicker/ColorPicker.swift b/Memola/Components/Views/ColorPicker/ColorPicker.swift index fe29a9e..5346e61 100644 --- a/Memola/Components/Views/ColorPicker/ColorPicker.swift +++ b/Memola/Components/Views/ColorPicker/ColorPicker.swift @@ -9,14 +9,18 @@ import SwiftUI import Foundation struct ColorPicker: View { - @State var hue: Double = 1 - @State var saturation: Double = 0 - @State var brightness: Double = 1 - @State var alpha: Double = 1 + @State private var hue: Double = 1 + @State private var saturation: Double = 0 + @State private var brightness: Double = 1 + @State private var alpha: Double = 1 - @Binding var color: Color + @Binding private var color: Color - let size: CGFloat = 20 + private let size: CGFloat = 20 + + init(color: Binding) { + self._color = color + } var body: some View { VStack(spacing: 10) { @@ -43,7 +47,7 @@ struct ColorPicker: View { } @ViewBuilder - var colorPicker: some View { + private var colorPicker: some View { GeometryReader { proxy in ZStack { Color(hue: hue, saturation: 1, brightness: 1) @@ -92,7 +96,7 @@ struct ColorPicker: View { } @ViewBuilder - var hueSlider: some View { + private var hueSlider: some View { GeometryReader { proxy in ZStack(alignment: .leading) { LinearGradient( @@ -138,7 +142,7 @@ struct ColorPicker: View { } @ViewBuilder - var alphaSlider: some View { + private var alphaSlider: some View { GeometryReader { proxy in let color = Color(hue: hue, saturation: saturation, brightness: brightness) ZStack(alignment: .leading) { @@ -190,7 +194,7 @@ struct ColorPicker: View { .frame(height: size) } - func updateColor() { + private func updateColor() { color = Color(hue: hue, saturation: saturation, brightness: brightness).opacity(0.7 * alpha + 0.3) } } diff --git a/Memola/Components/Views/Placeholder/Placeholder.swift b/Memola/Components/Views/Placeholder/Placeholder.swift index c2d2828..e74cb23 100644 --- a/Memola/Components/Views/Placeholder/Placeholder.swift +++ b/Memola/Components/Views/Placeholder/Placeholder.swift @@ -8,9 +8,13 @@ import SwiftUI struct Placeholder: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - let info: Info + private let info: Info + + init(info: Info) { + self.info = info + } var body: some View { VStack(spacing: 15) { diff --git a/Memola/Extensions/UIImage++.swift b/Memola/Extensions/UIImage++.swift deleted file mode 100644 index 983ad0f..0000000 --- a/Memola/Extensions/UIImage++.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UIImage++.swift -// Memola -// -// Created by Dscyre Scotti on 6/15/24. -// - -import UIKit -import Foundation - -extension UIImage { - func imageWithUpOrientation() -> UIImage? { - switch imageOrientation { - case .up: - return self - default: - UIGraphicsBeginImageContextWithOptions(size, false, scale) - draw(in: CGRect(origin: .zero, size: size)) - let result = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return result - } - } -} diff --git a/Memola/Features/Dashboard/Dashboard/DashboardView.swift b/Memola/Features/Dashboard/Dashboard/DashboardView.swift index c6aa549..48cc22c 100644 --- a/Memola/Features/Dashboard/Dashboard/DashboardView.swift +++ b/Memola/Features/Dashboard/Dashboard/DashboardView.swift @@ -8,25 +8,68 @@ import SwiftUI struct DashboardView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @State var memo: MemoObject? - @State var sidebarItem: SidebarItem? = .memos + @EnvironmentObject private var application: Application + + @State private var sidebarItem: SidebarItem? = .memos + @AppStorage("memola.app.scene.side-bar.column-visibility") private var columnVisibility: NavigationSplitViewVisibility = .all + + @Namespace private var namespace var body: some View { - NavigationSplitView { + #if os(macOS) + ZStack { + if let memo = application.memoObject { + NavigationStack { + MemoView(memo: memo) + .onDisappear { + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + context.refreshAllObjects() + } + } + .navigationTitle("") + } + .transition(.opacity) + .matchedGeometryEffect(id: "pop-up", in: namespace) + } else { + NavigationSplitView(columnVisibility: $columnVisibility) { + Sidebar(sidebarItem: $sidebarItem, horizontalSizeClass: horizontalSizeClass) + } detail: { + switch sidebarItem { + case .memos: + MemosView() + case .trash: + TrashView(sidebarItem: $sidebarItem) + default: + MemosView() + } + } + .transition(.opacity) + .matchedGeometryEffect(id: "pop-up", in: namespace) + } + } + .animation(.easeIn, value: application.memoObject) + .toolbar(application.memoObject == nil ? .visible : .hidden, for: .windowToolbar) + .toolbarBackground(application.memoObject == nil ? .clear : Color(nsColor: .windowBackgroundColor), for: .windowToolbar) + .onChange(of: columnVisibility) { oldValue, newValue in + application.changeSidebarVisibility(newValue == .all ? .shown : .hidden) + } + #else + NavigationSplitView(columnVisibility: $columnVisibility) { Sidebar(sidebarItem: $sidebarItem, horizontalSizeClass: horizontalSizeClass) } detail: { switch sidebarItem { case .memos: - MemosView(memo: $memo) + MemosView() case .trash: - TrashView(memo: $memo, sidebarItem: $sidebarItem) + TrashView(sidebarItem: $sidebarItem) default: - MemosView(memo: $memo) + MemosView() } } - .fullScreenCover(item: $memo) { memo in + .fullScreenCover(item: $application.memoObject) { memo in MemoView(memo: memo) .onDisappear { withPersistence(\.viewContext) { context in @@ -35,5 +78,6 @@ struct DashboardView: View { } } } + #endif } } diff --git a/Memola/Features/Dashboard/Details/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift index 53e71ae..d40aa0e 100644 --- a/Memola/Features/Dashboard/Details/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -8,31 +8,32 @@ import SwiftUI struct MemosView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.shortcut) private var shortcut + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @FetchRequest var memoObjects: FetchedResults + @EnvironmentObject private var application: Application - @State var query: String = "" - @State var currentDate: Date = .now + @FetchRequest private var memoObjects: FetchedResults - @Binding var memo: MemoObject? + @State private var query: String = "" + @State private var currentDate: Date = .now + @State private var isActiveSearch: Bool = false - @AppStorage("memola.memo-objects.memos.sort") var sort: Sort = .recent - @AppStorage("memola.memo-objects.memos.filter") var filter: Filter = .none + @AppStorage("memola.memo-objects.memos.sort") private var sort: Sort = .recent + @AppStorage("memola.memo-objects.memos.filter") private var filter: Filter = .none - let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() + private let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() - var placeholder: Placeholder.Info { + private var placeholder: Placeholder.Info { query.isEmpty ? .memoEmpty : .memoNotFound } - init(memo: Binding) { - _memo = memo + init() { let standard = UserDefaults.standard var descriptors: [SortDescriptor] = [] var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = NO")] - let sort = Sort(rawValue: standard.value(forKey: "memola.memo-objects.sort") as? String ?? "") ?? .recent - let filter = Filter(rawValue: standard.value(forKey: "memola.memo-objects.filter") as? String ?? "") ?? .none + let sort = Sort(rawValue: standard.value(forKey: "memola.memo-objects.memos.sort") as? String ?? "") ?? .recent + let filter = Filter(rawValue: standard.value(forKey: "memola.memo-objects.memos.filter") as? String ?? "") ?? .none if filter == .favorites { predicates.append(NSPredicate(format: "isFavorite = YES")) } @@ -45,10 +46,45 @@ struct MemosView: View { MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in memoCard(memoObject, cellWidth) } + .onDismissSearch(isActive: $isActiveSearch) + .focusedSceneValue(\.activeSceneKey, .memos) .navigationTitle(horizontalSizeClass == .compact ? "Memos" : "") + #if os(iOS) .navigationBarTitleDisplayMode(.inline) - .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + #endif + .searchable(text: $query, isPresented: $isActiveSearch, placement: .toolbar, prompt: Text("Search")) + .onSubmit(of: .search) { + isActiveSearch = false + } .toolbar { + #if os(macOS) + ToolbarItem(placement: .navigation) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) + } + ToolbarItemGroup(placement: .primaryAction) { + HStack(spacing: 5) { + Button { + createMemo(title: "Untitled") + } label: { + Image(systemName: "square.and.pencil") + } + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + } + } + #else if horizontalSizeClass == .regular { ToolbarItem(placement: .topBarLeading) { Text("Memola") @@ -73,14 +109,12 @@ struct MemosView: View { .tag(sort) } } - .pickerStyle(.automatic) Picker("", selection: $filter) { ForEach(Filter.all) { filter in Text(filter.name) .tag(filter) } } - .pickerStyle(.automatic) } } label: { Image(systemName: "ellipsis.circle") @@ -104,13 +138,14 @@ struct MemosView: View { .tag(filter) } } - .pickerStyle(.automatic) } label: { Image(systemName: "line.3.horizontal.decrease.circle") } + .hoverEffect(.lift) } } } + #endif } .onChange(of: sort) { oldValue, newValue in memoObjects.sortDescriptors = newValue.memoSortDescriptors @@ -124,13 +159,12 @@ struct MemosView: View { .onReceive(timer) { date in currentDate = date } - .onAppear { - memoObjects.sortDescriptors = sort.memoSortDescriptors - updatePredicate() + .onReceive(shortcut.publisher) { shortcut in + handleShortcut(for: shortcut) } } - func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { + private func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in card .contextMenu { @@ -138,11 +172,13 @@ struct MemosView: View { openMemo(for: memoObject) } label: { Label("Open", systemImage: "doc.text") + .labelStyle(.titleAndIcon) } Button(role: .destructive) { markAsTrash(for: memoObject) } label: { Label("Delete", systemImage: "trash") + .labelStyle(.titleAndIcon) } } .overlay(alignment: .topTrailing) { @@ -152,7 +188,11 @@ struct MemosView: View { .animation(.easeInOut, value: memoObject.isFavorite) .frame(width: 20, height: 20) .padding(5) + #if os(macOS) + .background(.gray) + #else .background(.gray.tertiary) + #endif .cornerRadius(5) .contentShape(Rectangle()) .onTapGesture { @@ -170,7 +210,7 @@ struct MemosView: View { } } - func createMemo(title: String) { + private func createMemo(title: String) { let memoObject = MemoObject(\.viewContext) memoObject.title = title memoObject.createdAt = .now @@ -220,11 +260,11 @@ struct MemosView: View { } } - func openMemo(for memo: MemoObject) { - self.memo = memo + private func openMemo(for memo: MemoObject) { + application.openMemo(memo) } - func updatePredicate() { + private func updatePredicate() { var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = NO")] if !query.isEmpty { predicates.append(NSPredicate(format: "title contains[c] %@", query)) @@ -235,18 +275,29 @@ struct MemosView: View { memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } - func toggleFavorite(for memo: MemoObject) { + private func toggleFavorite(for memo: MemoObject) { memo.isFavorite.toggle() withPersistence(\.viewContext) { context in try context.saveIfNeeded() } } - func markAsTrash(for memo: MemoObject) { + private func markAsTrash(for memo: MemoObject) { memo.isTrash = true memo.deletedAt = .now withPersistence(\.viewContext) { context in try context.saveIfNeeded() } } + + private func handleShortcut(for shortcut: Shortcuts) { + switch shortcut { + case .newMemo: + if application.memoObject == nil { + createMemo(title: "Untitled") + } + default: + break + } + } } diff --git a/Memola/Features/Dashboard/Details/Shared/MemoCard.swift b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift index 9f75ee5..dfde35f 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoCard.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift @@ -8,12 +8,12 @@ import SwiftUI struct MemoCard: View { - let memoObject: MemoObject - let cellWidth: CGFloat - let modifyPreview: ((MemoPreview) -> Preview)? - let details: () -> Detail + private let memoObject: MemoObject + private let cellWidth: CGFloat + private let modifyPreview: ((MemoPreview) -> Preview)? + private let details: () -> Detail - init(memoObject: MemoObject, cellWidth: CGFloat, @ViewBuilder modifyPreview: @escaping (MemoPreview) -> Preview, @ViewBuilder details: @escaping () -> Detail) { + init(memoObject: MemoObject, cellWidth: CGFloat, modifyPreview: ((MemoPreview) -> Preview)?, @ViewBuilder details: @escaping () -> Detail) { self.memoObject = memoObject self.cellWidth = cellWidth self.modifyPreview = modifyPreview diff --git a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift index 319fc14..e3b5315 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift @@ -8,12 +8,18 @@ import SwiftUI struct MemoGrid: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - let memoObjects: FetchedResults - let placeholder: Placeholder.Info - @ViewBuilder let card: (MemoObject, CGFloat) -> Card + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + private let memoObjects: FetchedResults + private let placeholder: Placeholder.Info + @ViewBuilder private let card: (MemoObject, CGFloat) -> Card - var maxCellWidth: CGFloat { + init(memoObjects: FetchedResults, placeholder: Placeholder.Info, @ViewBuilder card: @escaping (MemoObject, CGFloat) -> Card) { + self.memoObjects = memoObjects + self.placeholder = placeholder + self.card = card + } + + private var maxCellWidth: CGFloat { if horizontalSizeClass == .compact { return 180 } @@ -42,6 +48,10 @@ struct MemoGrid: View { } } } - .background(Color(uiColor: .secondarySystemBackground)) + #if os(macOS) + .background(Color(color: .windowBackgroundColor)) + #else + .background(Color(color: .secondarySystemBackground)) + #endif } } diff --git a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift index c5fc94c..6609f59 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift @@ -8,11 +8,17 @@ import SwiftUI struct MemoPreview: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - let preview: Data? - let cellWidth: CGFloat - var cellHeight: CGFloat { + private let preview: Data? + private let cellWidth: CGFloat + + init(preview: Data?, cellWidth: CGFloat) { + self.preview = preview + self.cellWidth = cellWidth + } + + private var cellHeight: CGFloat { if horizontalSizeClass == .compact { return 120 } @@ -21,8 +27,8 @@ struct MemoPreview: View { var body: some View { Group { - if let preview, let previewImage = UIImage(data: preview) { - Image(uiImage: previewImage) + if let preview, let previewImage = Platform.Image(data: preview) { + Image(image: previewImage) .resizable() .aspectRatio(contentMode: .fit) } else { diff --git a/Memola/Features/Dashboard/Details/Trash/TrashView.swift b/Memola/Features/Dashboard/Details/Trash/TrashView.swift index 815e068..a3f9f8d 100644 --- a/Memola/Features/Dashboard/Details/Trash/TrashView.swift +++ b/Memola/Features/Dashboard/Details/Trash/TrashView.swift @@ -8,23 +8,25 @@ import SwiftUI struct TrashView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.shortcut) private var shortcut + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @FetchRequest var memoObjects: FetchedResults + @EnvironmentObject private var application: Application - @State var query: String = "" - @State var restoredMemo: MemoObject? - @State var deletedMemo: MemoObject? + @FetchRequest private var memoObjects: FetchedResults - @Binding var memo: MemoObject? - @Binding var sidebarItem: SidebarItem? + @State private var query: String = "" + @State private var restoredMemo: MemoObject? + @State private var deletedMemo: MemoObject? + @State private var isActiveSearch: Bool = false - var placeholder: Placeholder.Info { + @Binding private var sidebarItem: SidebarItem? + + private var placeholder: Placeholder.Info { query.isEmpty ? .trashEmpty : .trashNotFound } - init(memo: Binding, sidebarItem: Binding) { - _memo = memo + init(sidebarItem: Binding) { _sidebarItem = sidebarItem let descriptors = [SortDescriptor(\MemoObject.deletedAt, order: .reverse)] let predicate = NSPredicate(format: "isTrash = YES") @@ -45,10 +47,24 @@ struct TrashView: View { MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject, cellWidth in memoCard(memoObject, cellWidth) } + .onDismissSearch(isActive: $isActiveSearch) + .focusedSceneValue(\.activeSceneKey, .trash) .navigationTitle(horizontalSizeClass == .compact ? "Trash" : "") + #if os(iOS) .navigationBarTitleDisplayMode(.inline) - .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + #endif + .searchable(text: $query, isPresented: $isActiveSearch, placement: .toolbar, prompt: Text("Search")) + .onSubmit(of: .search) { + isActiveSearch = false + } .toolbar { + #if os(macOS) + ToolbarItem(placement: .navigation) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) + } + #else if horizontalSizeClass == .regular { ToolbarItem(placement: .topBarLeading) { Text("Memola") @@ -56,6 +72,7 @@ struct TrashView: View { .fontWeight(.bold) } } + #endif } .onChange(of: query) { oldValue, newValue in updatePredicate() @@ -87,7 +104,7 @@ struct TrashView: View { } } - func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { + private func memoCard(_ memoObject: MemoObject, _ cellWidth: CGFloat) -> some View { MemoCard(memoObject: memoObject, cellWidth: cellWidth) { card in card .contextMenu { @@ -95,11 +112,13 @@ struct TrashView: View { restoreMemo(for: memoObject) } label: { Label("Restore", systemImage: "square.and.arrow.down") + .labelStyle(.titleAndIcon) } Button(role: .destructive) { deletedMemo = memoObject } label: { Label("Delete Permanently", systemImage: "trash") + .labelStyle(.titleAndIcon) } } } details: { @@ -114,7 +133,7 @@ struct TrashView: View { } } - func updatePredicate() { + private func updatePredicate() { var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = YES")] if !query.isEmpty { predicates.append(NSPredicate(format: "title contains[c] %@", query)) @@ -122,7 +141,7 @@ struct TrashView: View { memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } - func restoreMemo(for memo: MemoObject?) { + private func restoreMemo(for memo: MemoObject?) { guard let memo else { return } memo.isTrash = false memo.deletedAt = nil @@ -131,13 +150,15 @@ struct TrashView: View { } } - func restoreAndOpenMemo(for memo: MemoObject?) { + private func restoreAndOpenMemo(for memo: MemoObject?) { restoreMemo(for: memo) self.sidebarItem = .memos - self.memo = memo + if let memo { + application.openMemo(memo) + } } - func deleteMemo(for memo: MemoObject?) { + private func deleteMemo(for memo: MemoObject?) { guard let memo else { return } withPersistenceSync(\.viewContext) { context in context.delete(memo) diff --git a/Memola/Features/Dashboard/Sidebar/Sidebar.swift b/Memola/Features/Dashboard/Sidebar/Sidebar.swift index 06eb780..3d1794f 100644 --- a/Memola/Features/Dashboard/Sidebar/Sidebar.swift +++ b/Memola/Features/Dashboard/Sidebar/Sidebar.swift @@ -8,10 +8,15 @@ import SwiftUI struct Sidebar: View { - let sidebarItems: [SidebarItem] = [.memos, .trash] - @Binding var sidebarItem: SidebarItem? + private let sidebarItems: [SidebarItem] = [.memos, .trash] + @Binding private var sidebarItem: SidebarItem? - let horizontalSizeClass: UserInterfaceSizeClass? + private let horizontalSizeClass: UserInterfaceSizeClass? + + init(sidebarItem: Binding, horizontalSizeClass: UserInterfaceSizeClass?) { + self._sidebarItem = sidebarItem + self.horizontalSizeClass = horizontalSizeClass + } var body: some View { List(selection: $sidebarItem) { @@ -38,14 +43,20 @@ struct Sidebar: View { .listStyle(.sidebar) .navigationTitle(horizontalSizeClass == .compact ? "Memola" : "") .scrollContentBackground(.hidden) - .background(Color(uiColor: .secondarySystemBackground)) + #if os(macOS) + .background(Color(color: .windowBackgroundColor)) + #else + .background(Color(color: .secondarySystemBackground)) + #endif .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) + #if os(iOS) .navigationBarTitleDisplayMode(horizontalSizeClass == .compact ? .automatic : .inline) + #endif } } extension Sidebar { - struct SidebarItemButtonStyle: ButtonStyle { + fileprivate struct SidebarItemButtonStyle: ButtonStyle { let state: State func makeBody(configuration: Configuration) -> some View { diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index d3c62a3..b8f3387 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -10,29 +10,29 @@ import PhotosUI import AVFoundation struct ElementToolbar: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - let size: CGFloat - @ObservedObject var tool: Tool - @ObservedObject var canvas: Canvas + @Environment(\.colorScheme) private var colorScheme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @State var opensCamera: Bool = false - @State var isCameraAccessDenied: Bool = false - @State var photosPickerItem: PhotosPickerItem? + private let size: CGFloat = 40 + @ObservedObject private var tool: Tool + @ObservedObject private var canvas: Canvas - @Namespace var namespace + init(tool: Tool, canvas: Canvas) { + self.tool = tool + self.canvas = canvas + } var body: some View { Group { + #if os(macOS) + regularToolbar + #else if horizontalSizeClass == .regular { regularToolbar } else { ZStack(alignment: .bottom) { if tool.selection == .photo { - photoOption - .background { - RoundedRectangle(cornerRadius: 8) - .fill(.regularMaterial) - } + PhotoDock(tool: tool, canvas: canvas) .padding(.bottom, 10) .frame(maxWidth: .infinity) .transition(.move(edge: .bottom).combined(with: .blurReplace)) @@ -40,46 +40,13 @@ struct ElementToolbar: View { compactToolbar } } - .padding(.bottom, 10) - } - } - .fullScreenCover(isPresented: $opensCamera) { - let image: Binding = Binding { - tool.selectedPhotoItem?.image - } set: { image in - guard let image else { return } - tool.selectPhoto(image, for: canvas.canvasID) - } - CameraView(image: image, canvas: canvas) - .ignoresSafeArea() - } - .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { - Button { - if let url = URL(string: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { - UIApplication.shared.open(url) - } - } label: { - Text("Open Settings") - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") - } - .onChange(of: photosPickerItem) { oldValue, newValue in - if newValue != nil { - Task { - tool.isLoadingPhoto = true - let data = try? await newValue?.loadTransferable(type: Data.self) - if let data, let image = UIImage(data: data) { - tool.selectPhoto(image, for: canvas.canvasID) - } - photosPickerItem = nil - } } + #endif } + .foregroundStyle(Color.accentColor) } - var regularToolbar: some View { + private var regularToolbar: some View { HStack(spacing: 0) { Button { withAnimation { @@ -90,15 +57,19 @@ struct ElementToolbar: View { .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .hand ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif .background { if tool.selection == .hand { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } } Button { @@ -110,15 +81,19 @@ struct ElementToolbar: View { .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .pen ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif .background { if tool.selection == .pen { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } } HStack(spacing: 0) { @@ -130,32 +105,20 @@ struct ElementToolbar: View { Image(systemName: "photo") .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .photo ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif .background { if tool.selection == .photo { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } - if tool.selection != .photo { - Color.clear - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - } - } - if tool.selection == .photo { - photoOption - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - .transition(.blurReplace.animation(.easeIn(duration: 0.1))) - } - } - .background { - if tool.selection == .photo { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white.tertiary) - .transition(.move(edge: .leading).combined(with: .opacity).animation(.easeIn(duration: 0.1))) } } } @@ -166,7 +129,7 @@ struct ElementToolbar: View { .transition(.move(edge: .top).combined(with: .blurReplace)) } - var compactToolbar: some View { + private var compactToolbar: some View { HStack(spacing: 0) { Button { withAnimation { @@ -179,7 +142,9 @@ struct ElementToolbar: View { .frame(width: size, height: size) .clipShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #endif Button { withAnimation { tool.selectTool(.photo) @@ -190,7 +155,9 @@ struct ElementToolbar: View { .frame(width: size, height: size) .clipShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #endif } .background { RoundedRectangle(cornerRadius: 8) @@ -201,44 +168,5 @@ struct ElementToolbar: View { .transition(.move(edge: .bottom).combined(with: .blurReplace)) } - var photoOption: some View { - HStack(spacing: 0) { - Button { - openCamera() - } label: { - Image(systemName: "camera.fill") - .contentShape(.circle) - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { - Image(systemName: "photo.fill.on.rectangle.fill") - .contentShape(.circle) - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - } - } - - func openCamera() { - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { status in - withAnimation { - if status { - opensCamera = true - } else { - isCameraAccessDenied = true - } - } - } - case .authorized: - opensCamera = true - default: - isCameraAccessDenied = true - } - } + } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 722e431..2030334 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -9,17 +9,17 @@ import SwiftUI import CoreData struct MemoView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @StateObject var tool: Tool - @StateObject var canvas: Canvas - @StateObject var history: History + @StateObject private var tool: Tool + @StateObject private var canvas: Canvas + @StateObject private var history: History - @State var memo: MemoObject - @State var title: String - @FocusState var textFieldState: Bool + @State private var title: String + @FocusState private var textFieldState: Bool - let size: CGFloat = 32 + private let memo: MemoObject + private let size: CGFloat = 40 init(memo: MemoObject) { self.memo = memo @@ -31,14 +31,18 @@ struct MemoView: View { var body: some View { Group { + #if os(macOS) + canvasView + #else if horizontalSizeClass == .regular { canvasView } else { compactCanvasView } + #endif } .overlay(alignment: .top) { - Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) + Toolbar(memo: memo, tool: tool, canvas: canvas, history: history) } .disabled(textFieldState || tool.isLoadingPhoto) .disabled(canvas.state == .loading || canvas.state == .closing) @@ -57,21 +61,28 @@ struct MemoView: View { loadingIndicator("Loading photo...") } } + .focusedSceneObject(tool) + .focusedSceneObject(canvas) + .focusedSceneObject(history) + .focusedSceneValue(\.activeSceneKey, .memo) } - var canvasView: some View { + private var canvasView: some View { CanvasView(tool: tool, canvas: canvas, history: history) .ignoresSafeArea() - .overlay(alignment: .bottomTrailing) { + .overlay(alignment: .trailing) { switch tool.selection { case .pen: - PenDock(tool: tool, canvas: canvas, size: size) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + PenDock(tool: tool, canvas: canvas) case .photo: - if let photoItem = tool.selectedPhotoItem { - PhotoPreview(photoItem: photoItem, tool: tool) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + ZStack(alignment: .bottomTrailing) { + PhotoDock(tool: tool, canvas: canvas) + if let photoItem = tool.selectedPhotoItem { + PhotoPreview(photoItem: photoItem, tool: tool) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } } + .frame(maxWidth: .infinity, alignment: .trailing) default: EmptyView() } @@ -81,13 +92,13 @@ struct MemoView: View { } } - var compactCanvasView: some View { + private var compactCanvasView: some View { CanvasView(tool: tool, canvas: canvas, history: history) .ignoresSafeArea() .overlay(alignment: .bottom) { switch tool.selection { case .pen: - PenDock(tool: tool, canvas: canvas, size: size) + PenDock(tool: tool, canvas: canvas) .transition(.move(edge: .bottom).combined(with: .blurReplace)) case .photo: if let photoItem = tool.selectedPhotoItem { @@ -101,69 +112,86 @@ struct MemoView: View { } .overlay(alignment: .bottom) { if tool.selection != .pen { - ElementToolbar(size: size, tool: tool, canvas: canvas) + ElementToolbar(tool: tool, canvas: canvas) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } } - .overlay(alignment: .bottom) { - if tool.selection != .hand { - Button { - withAnimation { - tool.selectTool(.hand) - } - } label: { - Image(systemName: "chevron.compact.down") - .font(.headline) - .frame(width: 80) - .padding(5) - .background(.regularMaterial) - .clipShape(.capsule) - .contentShape(.capsule) - } - .offset(y: 5) - } - } } @ViewBuilder - var zoomControl: some View { + private var zoomControl: some View { let upperBound: CGFloat = 400 let lowerBound: CGFloat = 10 let zoomScale: CGFloat = (((canvas.zoomScale - canvas.minimumZoomScale) * (upperBound - lowerBound) / (canvas.maximumZoomScale - canvas.minimumZoomScale)) + lowerBound).rounded() let zoomScales: [Int] = [400, 200, 100, 75, 50, 25, 10] - if !canvas.locksCanvas { - Menu { - ForEach(zoomScales, id: \.self) { scale in - Button { - let zoomScale = ((CGFloat(scale) - lowerBound) * (canvas.maximumZoomScale - canvas.minimumZoomScale) / (upperBound - lowerBound)) + canvas.minimumZoomScale - canvas.zoomPublisher.send(zoomScale) - } label: { - Label { - Text(scale, format: .percent) - } icon: { - if CGFloat(scale) == zoomScale { - Image(systemName: "checkmark") - } + #if os(macOS) + Menu { + ForEach(zoomScales, id: \.self) { scale in + Button { + let zoomScale = ((CGFloat(scale) - lowerBound) * (canvas.maximumZoomScale - canvas.minimumZoomScale) / (upperBound - lowerBound)) + canvas.minimumZoomScale + canvas.zoomPublisher.send(zoomScale) + } label: { + Label { + Text(scale, format: .percent) + } icon: { + if CGFloat(scale) == zoomScale { + Image(systemName: "checkmark") } - .font(.headline) } + .font(.headline) } - } label: { - Text(zoomScale / 100, format: .percent) - .frame(width: 45) - .font(.subheadline) - .padding(.horizontal, size / 2.5) - .frame(height: size) - .background(.regularMaterial) - .clipShape(.rect(cornerRadius: 8)) - .padding(10) } - .hoverEffect(.lift) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } label: { + Text(zoomScale / 100, format: .percent) + .foregroundStyle(Color.accentColor) + .font(.subheadline) + .frame(height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } + .menuIndicator(.hidden) + .frame(width: 50, height: size) + .padding(.leading, 12) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + .menuStyle(.borderlessButton) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + .padding(10) + #else + Menu { + ForEach(zoomScales, id: \.self) { scale in + Button { + let zoomScale = ((CGFloat(scale) - lowerBound) * (canvas.maximumZoomScale - canvas.minimumZoomScale) / (upperBound - lowerBound)) + canvas.minimumZoomScale + canvas.zoomPublisher.send(zoomScale) + } label: { + Label { + Text(scale, format: .percent) + } icon: { + if CGFloat(scale) == zoomScale { + Image(systemName: "checkmark") + } + } + .font(.headline) + } + } + } label: { + Text(zoomScale / 100, format: .percent) + .frame(width: 45) + .font(.subheadline) + .padding(.horizontal, size / 2.5) + .frame(height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + .padding(10) + } + .hoverEffect(.lift) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + #endif } - func loadingIndicator(_ title: String) -> some View { + private func loadingIndicator(_ title: String) -> some View { ProgressView { Text(title) } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index f053c3a..f11f4d1 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -8,114 +8,143 @@ import SwiftUI struct PenDock: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject var tool: Tool - @ObservedObject var canvas: Canvas + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @ObservedObject private var tool: Tool + @ObservedObject private var canvas: Canvas - let size: CGFloat - var width: CGFloat { - horizontalSizeClass == .compact ? 30 : 90 + private let size: CGFloat = 40 + private let penPropertySize: CGFloat = 32 + private var width: CGFloat { + horizontalSizeClass == .compact ? size / 2 : size } - var height: CGFloat { - horizontalSizeClass == .compact ? 90 : 30 + private var height: CGFloat { + horizontalSizeClass == .compact ? size : size / 2 } - var factor: CGFloat = 0.9 - @State var refreshScrollId: UUID = UUID() - @State var opensColorPicker: Bool = false + @State private var refreshScrollId: UUID = UUID() + @State private var opensColorPicker: Bool = false + #if os(macOS) + @State private var showsThinknessPicker: Bool = false + #endif + + init(tool: Tool, canvas: Canvas) { + self.tool = tool + self.canvas = canvas + } var body: some View { - if horizontalSizeClass == .regular { - ZStack(alignment: .bottomTrailing) { - if !canvas.locksCanvas { - VStack(alignment: .trailing) { - penPropertyTool - penItemList - } - .fixedSize() - .frame(maxHeight: .infinity) - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) - } - lockButton - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) - } - } else { - ZStack(alignment: .bottomTrailing) { - if !canvas.locksCanvas { - GeometryReader { proxy in - HStack(alignment: .bottom, spacing: 10) { - newPenButton - .frame(height: height * factor - 18) - compactPenItemList - .fixedSize(horizontal: false, vertical: true) - compactPenPropertyTool - } - .padding(.horizontal, 10) - .clipped() - .background(alignment: .bottom) { - RoundedRectangle(cornerRadius: 8) - .fill(.regularMaterial) - .frame(height: height * factor - 18) - } - .padding(.horizontal, 10) - .padding(.bottom, 20) - .frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom) - .frame(maxWidth: .infinity) - } - .transition(.move(edge: .bottom).combined(with: .blurReplace)) - } - lockButton - .frame(maxWidth: .infinity, alignment: .bottomTrailing) - .padding(10) - .offset(y: canvas.locksCanvas ? 0 : -(height * factor - size + 30)) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + #if os(macOS) + GeometryReader { proxy in + VStack(alignment: .trailing, spacing: 5) { + penPropertyTool + penItemList + .frame(maxWidth: proxy.size.width * 0.4) } + .fixedSize() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) } + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + #else + if horizontalSizeClass == .regular { + GeometryReader { proxy in + VStack(alignment: .trailing, spacing: 5) { + penPropertyTool + penItemList + .frame(maxHeight: proxy.size.height * 0.4) + } + .fixedSize() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) + } + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } else { + GeometryReader { proxy in + HStack(alignment: .bottom, spacing: 10) { + newPenButton + .padding(.leading, 10) + .frame(height: height) + compactPenItemList + .fixedSize(horizontal: false, vertical: true) + HStack(spacing: 0) { + compactPenPropertyTool + Divider() + .padding(.vertical, 4) + .frame(height: size) + .foregroundStyle(Color.accentColor) + .padding(.leading, 8) + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "xmark") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + } + } + .clipped() + .background(alignment: .bottom) { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + .frame(height: height) + } + .padding([.horizontal, .bottom], 10) + .frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom) + .frame(maxWidth: .infinity) + } + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + #endif } @ViewBuilder - var penItemList: some View { - ScrollViewReader { proxy in - ScrollView(.vertical, showsIndicators: false) { - LazyVStack(spacing: 0) { - ForEach(tool.pens) { pen in - penItem(pen) - .id(pen.id) - .scrollTransition { content, phase in - content - .scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .trailing) - } + private var penItemList: some View { + VStack(alignment: .trailing, spacing: 0) { + ScrollViewReader { proxy in + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(spacing: 5) { + ForEach(tool.pens) { pen in + penItem(pen) + .id(pen.id) + .scrollTransition { content, phase in + content + .scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .trailing) + } + } } + .padding(.vertical, 5) + .id(refreshScrollId) } - .padding(.vertical, 10) - .id(refreshScrollId) - } - .onReceive(tool.scrollPublisher) { id in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - withAnimation { - proxy.scrollTo(id) + .onReceive(tool.scrollPublisher) { id in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + proxy.scrollTo(id) + } } } } + newPenButton + .padding(.vertical, 5) + .frame(width: width) } - .frame(maxHeight: ((height * factor + 10) * 6) + 20) - .fixedSize() + .padding(.vertical, 3) .background(alignment: .trailing) { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) - .frame(width: width * factor - 18) - } - .clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8))) - .overlay(alignment: .bottomLeading) { - newPenButton - .offset(x: 15, y: 10) + .frame(width: width) } } @ViewBuilder - var compactPenItemList: some View { + private var compactPenItemList: some View { ScrollViewReader { proxy in ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 0) { @@ -128,7 +157,7 @@ struct PenDock: View { } } } - .padding(.horizontal, 10) + .padding(.horizontal, 5) .id(refreshScrollId) } .onReceive(tool.scrollPublisher) { id in @@ -141,7 +170,7 @@ struct PenDock: View { } } - func penItem(_ pen: Pen) -> some View { + private func penItem(_ pen: Pen) -> some View { ZStack { penShadow(pen) if let tip = pen.style.icon.tip { @@ -153,15 +182,10 @@ struct PenDock: View { Image(pen.style.icon.base) .resizable() } - .frame(width: width * factor, height: height * factor) + .frame(width: width * 1.2, height: height * 0.9) .padding(.vertical, 5) - .contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) - .onTapGesture { - if tool.selectedPen !== pen { - tool.selectPen(pen) - } - } .padding(.leading, 10) + .contentShape(.rect) .contextMenu(if: pen.strokeStyle != .eraser) { ControlGroup { Button { @@ -201,7 +225,10 @@ struct PenDock: View { } preview: { penPreview(pen) .drawingGroup() + #if os(iOS) .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) + #else + #endif } .onDrag(if: pen.strokeStyle != .eraser) { tool.draggedPen = pen @@ -211,10 +238,16 @@ struct PenDock: View { .contentShape(.dragPreview, .rect(cornerRadius: 10)) } .onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() })) - .offset(x: tool.selectedPen === pen ? 0 : 25) + .offset(x: tool.selectedPen === pen ? 0 : 16) + .contentShape(.rect) + .onTapGesture { + if tool.selectedPen !== pen { + tool.selectPen(pen) + } + } } - func compactPenItem(_ pen: Pen) -> some View { + private func compactPenItem(_ pen: Pen) -> some View { ZStack { compactPenShadow(pen) if let tip = pen.style.compactIcon.tip { @@ -226,7 +259,7 @@ struct PenDock: View { Image(pen.style.compactIcon.base) .resizable() } - .frame(width: width * factor, height: height * factor) + .frame(width: width * 0.9, height: height * 1.2) .padding(.top, 5) .contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) .onTapGesture { @@ -234,7 +267,7 @@ struct PenDock: View { tool.selectPen(pen) } } - .padding(.horizontal, 10) + .padding(.horizontal, 6) .contextMenu(if: pen.strokeStyle != .eraser) { ControlGroup { Button { @@ -272,23 +305,26 @@ struct PenDock: View { } .controlGroupStyle(.menu) } preview: { - penPreview(pen) + compactPenPreview(pen) .drawingGroup() + #if os(iOS) .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) + #else + #endif } .onDrag(if: pen.strokeStyle != .eraser) { tool.draggedPen = pen return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider() } preview: { - penPreview(pen) + compactPenPreview(pen) .contentShape(.dragPreview, .rect(cornerRadius: 10)) } .onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() })) - .offset(y: tool.selectedPen === pen ? 0 : 25) + .offset(y: tool.selectedPen === pen ? 0 : 16) } @ViewBuilder - var penPropertyTool: some View { + private var penPropertyTool: some View { if let pen = tool.selectedPen { VStack(spacing: 5) { if pen.strokeStyle == .marker { @@ -296,37 +332,36 @@ struct PenDock: View { } penThicknessPicker(pen) } - .padding(10) - .frame(width: width * factor - 18) + .padding(.vertical, 5) + .frame(width: width) .background { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } - .transition(.move(edge: .trailing).combined(with: .blurReplace)) } else { Color.clear - .frame(width: width * factor - 18, height: 50) + .frame(width: width, height: 50) } } @ViewBuilder - var compactPenPropertyTool: some View { + private var compactPenPropertyTool: some View { if let pen = tool.selectedPen { - HStack(spacing: 10) { - compactPenThicknessPicker(pen) - .frame(width: width) + HStack(spacing: 8) { + penThicknessPicker(pen) + .frame(width: penPropertySize) .rotationEffect(.degrees(-90)) if pen.strokeStyle == .marker { penColorPicker(pen) - .frame(width: width) + .frame(width: penPropertySize) .transition(.move(edge: .trailing).combined(with: .opacity)) } } - .frame(height: height * factor - 18) + .frame(height: height) } } - func penColorPicker(_ pen: Pen) -> some View { + private func penColorPicker(_ pen: Pen) -> some View { Button { opensColorPicker = true } label: { @@ -347,7 +382,8 @@ struct PenDock: View { } .background(baseColor) .clipShape(.rect(cornerRadius: 8)) - .frame(height: horizontalSizeClass == .compact ? 30 : 25) + .contentShape(.rect(cornerRadius: 8)) + .frame(width: penPropertySize, height: penPropertySize) .overlay { RoundedRectangle(cornerRadius: 8) .stroke(Color.gray, lineWidth: 0.4) @@ -356,7 +392,9 @@ struct PenDock: View { .drawingGroup() } .buttonStyle(.plain) + #if os(iOS) .hoverEffect(.lift) + #endif .popover(isPresented: $opensColorPicker) { let color = Binding( get: { pen.color }, @@ -376,75 +414,56 @@ struct PenDock: View { } @ViewBuilder - func penThicknessPicker(_ pen: Pen) -> some View { + private func penThicknessPicker(_ pen: Pen) -> some View { let minimum: CGFloat = pen.style.thickness.min let maximum: CGFloat = pen.style.thickness.max let start: CGFloat = 4 let end: CGFloat = 10 - let selection = Binding( - get: { pen.thickness }, - set: { - pen.thickness = $0 - tool.objectWillChange.send() - } - ) - Picker("", selection: selection) { - ForEach(pen.style.thicknessSteps, id: \.self) { step in - let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) - Circle() - .fill(.black) - .frame(width: size, height: size) - .frame(width: size + 2, height: size + 2) - } - } - .hoverEffect(.lift) - .pickerStyle(.wheel) - .frame(width: width * factor - 18, height: 35) - .onChange(of: pen.thickness) { _, _ in - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() - } - } - } - - @ViewBuilder - func compactPenThicknessPicker(_ pen: Pen) -> some View { - let minimum: CGFloat = pen.style.thickness.min - let maximum: CGFloat = pen.style.thickness.max - let start: CGFloat = 4 - let end: CGFloat = 7 - let selection = Binding( + let selection = Binding( get: { pen.thickness }, set: { - pen.thickness = $0 + pen.thickness = $0 ?? .zero tool.objectWillChange.send() } ) - Picker("", selection: selection) { - ForEach(pen.style.thicknessSteps, id: \.self) { step in - let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) - Circle() - .fill(.black) - .frame(width: size, height: size) - .frame(width: size + 2, height: size + 2) + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(pen.style.thicknessSteps, id: \.self) { step in + let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) + Circle() + .foregroundStyle(.primary) + .frame(width: size, height: size) + .frame(width: penPropertySize, height: penPropertySize) + .contentShape(.rect) + .id(step) + } + } } - } - .hoverEffect(.lift) - .pickerStyle(.wheel) - .frame(width: 50, height: 30) - .onChange(of: pen.thickness) { _, _ in - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() + .frame(width: penPropertySize, height: penPropertySize) + .background(.gray.quaternary) + .clipShape(.rect(cornerRadius: 8)) + .scrollPosition(id: selection, anchor: .center) + .scrollTargetLayout() + .scrollTargetBehavior(.viewAligned) + .scrollIndicators(.hidden) + .onAppear { + proxy.scrollTo(selection.wrappedValue) + } + .onChange(of: pen.thickness) { _, _ in + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } } } - var newPenButton: some View { + private var newPenButton: some View { Button { createNewPen() } label: { Image(systemName: "plus.circle.fill") - .font(.title2) + .font(.headline) .padding(1) .contentShape(.circle) .background { @@ -453,10 +472,14 @@ struct PenDock: View { } } .foregroundStyle(.green) + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif } - func penPreview(_ pen: Pen) -> some View { + private func penPreview(_ pen: Pen) -> some View { ZStack { if let tip = pen.style.icon.tip { Image(tip) @@ -467,12 +490,28 @@ struct PenDock: View { Image(pen.style.icon.base) .resizable() } - .frame(width: width * factor, height: height * factor) + .frame(width: width * 1.2, height: height * 0.9) .padding(.vertical, 5) .padding(.leading, 10) } - func penShadow(_ pen: Pen) -> some View { + private func compactPenPreview(_ pen: Pen) -> some View { + ZStack { + if let tip = pen.style.compactIcon.tip { + Image(tip) + .resizable() + .renderingMode(.template) + .foregroundStyle(Color.rgba(from: pen.rgba)) + } + Image(pen.style.compactIcon.base) + .resizable() + } + .frame(width: width * 0.9, height: height * 1.2) + .padding(.top, 5) + .padding(.horizontal, 5) + } + + private func penShadow(_ pen: Pen) -> some View { ZStack { Group { if let tip = pen.style.icon.tip { @@ -496,7 +535,7 @@ struct PenDock: View { } } - func compactPenShadow(_ pen: Pen) -> some View { + private func compactPenShadow(_ pen: Pen) -> some View { ZStack { Group { if let tip = pen.style.compactIcon.tip { @@ -520,23 +559,7 @@ struct PenDock: View { } } - var lockButton: some View { - Button { - withAnimation { - canvas.locksCanvas.toggle() - } - } label: { - Image(systemName: canvas.locksCanvas ? "lock.fill" : "lock.open.fill") - .contentShape(.circle) - .frame(width: size, height: size) - .background(.regularMaterial) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - .contentTransition(.symbolEffect(.replace)) - } - - func createNewPen() { + private func createNewPen() { let pen = PenObject.createObject(\.viewContext, penStyle: .marker) var selectedPen = tool.selectedPen selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last) diff --git a/Memola/Features/Memo/PenDock/PenDropDelegate.swift b/Memola/Features/Memo/PenDock/PenDropDelegate.swift index 8557f34..e5dfc31 100644 --- a/Memola/Features/Memo/PenDock/PenDropDelegate.swift +++ b/Memola/Features/Memo/PenDock/PenDropDelegate.swift @@ -9,9 +9,15 @@ import SwiftUI import Foundation struct PenDropDelegate: DropDelegate { - let id: String - @ObservedObject var tool: Tool - let action: () -> Void + private let id: String + @ObservedObject private var tool: Tool + private let action: () -> Void + + init(id: String, tool: Tool, action: @escaping () -> Void) { + self.id = id + self.tool = tool + self.action = action + } func performDrop(info: DropInfo) -> Bool { tool.draggedPen = nil diff --git a/Memola/Features/Memo/PhotoDock/PhotoDock.swift b/Memola/Features/Memo/PhotoDock/PhotoDock.swift new file mode 100644 index 0000000..66f4116 --- /dev/null +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -0,0 +1,208 @@ +// +// PhotoDock.swift +// Memola +// +// Created by Dscyre Scotti on 7/11/24. +// + +import SwiftUI +import PhotosUI + +struct PhotoDock: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private let size: CGFloat = 40 + + @ObservedObject private var tool: Tool + @ObservedObject private var canvas: Canvas + + @State private var opensCamera: Bool = false + @State private var isCameraAccessDenied: Bool = false + @State private var photosPickerItem: PhotosPickerItem? + + init(tool: Tool, canvas: Canvas) { + self.tool = tool + self.canvas = canvas + } + + var body: some View { + Group { + #if os(macOS) + photoOption + #else + if horizontalSizeClass == .regular { + photoOption + } else { + compactPhotoOption + } + #endif + } + .foregroundStyle(Color.accentColor) + #if os(iOS) + .fullScreenCover(isPresented: $opensCamera) { + let image: Binding = Binding { + tool.selectedPhotoItem?.image + } set: { image in + guard let image else { return } + tool.selectPhoto(image, for: canvas.canvasID) + } + CameraView(image: image, canvas: canvas) + .ignoresSafeArea() + } + .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { + Button { + if let url = URL(string: Platform.Application.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { + Platform.Application.shared.open(url) + } + } label: { + Text("Open Settings") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") + } + #endif + .onChange(of: photosPickerItem) { oldValue, newValue in + if newValue != nil { + Task { + tool.isLoadingPhoto = true + let data = try? await newValue?.loadTransferable(type: Data.self) + if let data, let image = Platform.Image(data: data) { + tool.selectPhoto(image, for: canvas.canvasID) + } + photosPickerItem = nil + } + } + } + } + + private var photoOption: some View { + VStack(spacing: 0) { + #if os(iOS) + Button { + openCamera() + } label: { + Image(systemName: "camera.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + #endif + PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + Image(systemName: "photo.fill.on.rectangle.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + if horizontalSizeClass == .compact { + Divider() + .padding(.vertical, 4) + .frame(height: size) + .foregroundStyle(Color.accentColor) + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "xmark") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + } + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .padding(.trailing, 10) + .frame(maxHeight: .infinity) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + + private var compactPhotoOption: some View { + HStack(spacing: 0) { + #if os(iOS) + Button { + openCamera() + } label: { + Image(systemName: "camera.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + #endif + PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + Image(systemName: "photo.fill.on.rectangle.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + if horizontalSizeClass == .compact { + Divider() + .padding(.vertical, 4) + .frame(height: size) + .foregroundStyle(Color.accentColor) + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "xmark") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + } + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .padding(.bottom, 10) + .frame(maxWidth: .infinity) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + + private func openCamera() { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { status in + withAnimation { + if status { + opensCamera = true + } else { + isCameraAccessDenied = true + } + } + } + case .authorized: + opensCamera = true + default: + isCameraAccessDenied = true + } + } +} diff --git a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift index 79e2539..821d7fb 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift @@ -5,13 +5,13 @@ // Created by Dscyre Scotti on 6/16/24. // -import UIKit +import SwiftUI import Foundation struct PhotoItem: Identifiable, Equatable { var id: URL - let image: UIImage - let previewImage: UIImage + let image: Platform.Image + let previewImage: Platform.Image let dimension: CGSize let bookmark: Data diff --git a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift index beb923e..35e7a17 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift @@ -8,13 +8,18 @@ import SwiftUI struct PhotoPreview: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - let photoItem: PhotoItem - @ObservedObject var tool: Tool + private let photoItem: PhotoItem + @ObservedObject private var tool: Tool + + init(photoItem: PhotoItem, tool: Tool) { + self.photoItem = photoItem + self.tool = tool + } var body: some View { - Image(uiImage: photoItem.previewImage) + Image(image: photoItem.previewImage) .resizable() .scaledToFit() .frame(width: horizontalSizeClass == .compact ? 80 : nil, height: horizontalSizeClass == .compact ? nil : 100) @@ -40,7 +45,9 @@ struct PhotoPreview: View { } } .foregroundStyle(.red) + #if os(iOS) .hoverEffect(.lift) + #endif .offset(x: -12, y: -12) } .padding(10) diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 1f41fbd..89e98ce 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -9,22 +9,25 @@ import SwiftUI import Foundation struct Toolbar: View { - @Environment(\.dismiss) var dismiss - @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.dismiss) private var dismiss + @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @ObservedObject var tool: Tool - @ObservedObject var canvas: Canvas - @ObservedObject var history: History + #if os(macOS) + @EnvironmentObject private var application: Application + #endif - @State var title: String - @State var memo: MemoObject + @ObservedObject private var tool: Tool + @ObservedObject private var canvas: Canvas + @ObservedObject private var history: History - @FocusState var textFieldState: Bool + @State private var title: String - let size: CGFloat + @FocusState private var textFieldState: Bool - init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) { - self.size = size + private let size: CGFloat = 40 + private let memo: MemoObject + + init(memo: MemoObject, tool: Tool, canvas: Canvas, history: History) { self.memo = memo self.tool = tool self.canvas = canvas @@ -35,43 +38,49 @@ struct Toolbar: View { var body: some View { HStack(spacing: 5) { HStack(spacing: 5) { - if !canvas.locksCanvas { - closeButton - titleField - } + closeButton + titleField + .foregroundStyle(Color.primary) } .frame(maxWidth: .infinity, alignment: .leading) - if !canvas.locksCanvas, horizontalSizeClass == .regular { - ElementToolbar(size: size, tool: tool, canvas: canvas) + #if os(macOS) + ElementToolbar(tool: tool, canvas: canvas) + #else + if horizontalSizeClass == .regular { + ElementToolbar(tool: tool, canvas: canvas) } + #endif HStack(spacing: 5) { - if !canvas.locksCanvas { - gridModeControl - historyControl - } + gridModeControl + historyControl } .frame(maxWidth: .infinity, alignment: .trailing) } .font(.subheadline) .padding(10) + .foregroundStyle(Color.accentColor) } - var closeButton: some View { + private var closeButton: some View { Button { closeMemo() } label: { Image(systemName: "xmark") - .contentShape(.circle) .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } + .disabled(title.isEmpty) + #if os(iOS) .hoverEffect(.lift) - .disabled(textFieldState) + #else + .buttonStyle(.plain) + #endif .transition(.move(edge: .top).combined(with: .blurReplace)) } - var titleField: some View { + private var titleField: some View { TextField("", text: $title) .focused($textFieldState) .textFieldStyle(.plain) @@ -93,35 +102,60 @@ struct Toolbar: View { } } .transition(.move(edge: .top).combined(with: .blurReplace)) + .onSubmit(of: .text) { + textFieldState = false + } } - var historyControl: some View { - HStack { + private var historyControl: some View { + HStack(spacing: 0) { Button { history.historyPublisher.send(.undo) } label: { Image(systemName: "arrow.uturn.backward.circle") - .contentShape(.circle) + .frame(width: size, height: size) + .contentShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif .disabled(history.undoDisabled) Button { history.historyPublisher.send(.redo) } label: { Image(systemName: "arrow.uturn.forward.circle") - .contentShape(.circle) + .frame(width: size, height: size) + .contentShape(.rect(cornerRadius: 8)) } + #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif .disabled(history.redoDisabled) } - .frame(width: size * 2, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) - .disabled(textFieldState) .transition(.move(edge: .top).combined(with: .blurReplace)) } - var gridModeControl: some View { + private var gridModeControl: some View { + #if os(macOS) + Button { + canvas.toggleGridMode() + } label: { + Image(systemName: canvas.gridMode.icon) + .frame(width: size, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .buttonStyle(.plain) + .contentTransition(.symbolEffect(.replace)) + .transition(.move(edge: .top).combined(with: .blurReplace)) + #else Menu { ForEach(GridMode.all, id: \.self) { mode in Button { @@ -137,19 +171,24 @@ struct Toolbar: View { } } label: { Image(systemName: canvas.gridMode.icon) - .contentShape(.circle) .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } .hoverEffect(.lift) .contentTransition(.symbolEffect(.replace)) .transition(.move(edge: .top).combined(with: .blurReplace)) + #endif } - func closeMemo() { + private func closeMemo() { canvas.save(for: memo) { + #if os(macOS) + application.closeMemo() + #else dismiss() + #endif } } } diff --git a/Memola/Memola.entitlements b/Memola/Memola.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Memola/Memola.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index 5ebac21..e34a888 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -101,3 +101,8 @@ func withPersistenceSync(_ keypath: KeyPath } } } + +func withPersistenceContext(_ keypath: KeyPath, _ task: @escaping (NSManagedObjectContext) throws -> T) throws -> T { + let context = Persistence.shared[keyPath: keypath] + return try task(context) +} diff --git a/Memola/Persistence/Objects/PenObject.swift b/Memola/Persistence/Objects/PenObject.swift index 054edf4..9be5f86 100644 --- a/Memola/Persistence/Objects/PenObject.swift +++ b/Memola/Persistence/Objects/PenObject.swift @@ -9,7 +9,7 @@ import CoreData import Foundation @objc(PenObject) -class PenObject: NSManagedObject { +final class PenObject: NSManagedObject { @NSManaged var color: [CGFloat] @NSManaged var style: Int16 @NSManaged var thickness: CGFloat @@ -20,7 +20,7 @@ class PenObject: NSManagedObject { extension PenObject { static func createObject(_ keyPath: KeyPath, penStyle: any PenStyle) -> PenObject { - let object = PenObject(context: Persistence.shared[keyPath: keyPath]) + let object = PenObject(keyPath) object.color = penStyle.color object.style = penStyle.strokeStyle.rawValue object.isSelected = false diff --git a/Memola/Persistence/Objects/PhotoObject.swift b/Memola/Persistence/Objects/PhotoObject.swift index 10283e8..ac697c8 100644 --- a/Memola/Persistence/Objects/PhotoObject.swift +++ b/Memola/Persistence/Objects/PhotoObject.swift @@ -9,7 +9,7 @@ import CoreData import Foundation @objc(PhotoObject) -class PhotoObject: NSManagedObject { +final class PhotoObject: NSManagedObject { @NSManaged var width: CGFloat @NSManaged var originY: CGFloat @NSManaged var originX: CGFloat diff --git a/Memola/Persistence/Objects/ToolObject.swift b/Memola/Persistence/Objects/ToolObject.swift index 757d4c2..c5468d4 100644 --- a/Memola/Persistence/Objects/ToolObject.swift +++ b/Memola/Persistence/Objects/ToolObject.swift @@ -9,7 +9,7 @@ import CoreData import Foundation @objc(ToolObject) -class ToolObject: NSManagedObject { +final class ToolObject: NSManagedObject { @NSManaged var selection: Int16 @NSManaged var pens: NSMutableSet @NSManaged var memo: MemoObject? diff --git a/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json b/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..462f7c0 100644 --- a/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "colors" : [ { + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0x7E", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xCE", + "red" : "0x99" + } + }, "idiom" : "universal" } ], diff --git a/Memola/Shortcut/Commands/AppCommands.swift b/Memola/Shortcut/Commands/AppCommands.swift new file mode 100644 index 0000000..16c39fd --- /dev/null +++ b/Memola/Shortcut/Commands/AppCommands.swift @@ -0,0 +1,21 @@ +// +// AppCommands.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +struct AppCommands: Commands { + var body: some Commands { + CommandGroup(replacing: .appSettings) { + Button { + + } label: { + Text("Services...") + } + .keyboardShortcut(",", modifiers: .command) + } + } +} diff --git a/Memola/Shortcut/Commands/EditCommands.swift b/Memola/Shortcut/Commands/EditCommands.swift new file mode 100644 index 0000000..a11c1ba --- /dev/null +++ b/Memola/Shortcut/Commands/EditCommands.swift @@ -0,0 +1,61 @@ +// +// EditCommands.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +struct EditCommands: Commands { + @FocusedValue(\.activeSceneKey) private var appScene + + @FocusedObject var tool: Tool? + @FocusedObject var history: History? + + var body: some Commands { + CommandGroup(replacing: .undoRedo) { + if appScene == .memo { + if let history { + Button { + history.historyPublisher.send(.undo) + } label: { + Text("Undo") + } + .keyboardShortcut("z", modifiers: [.command]) + .disabled(history.undoDisabled) + Button { + history.historyPublisher.send(.redo) + } label: { + Text("Redo") + } + .keyboardShortcut("z", modifiers: [.command, .shift]) + .disabled(history.redoDisabled) + } + Divider() + if let tool { + Button { + tool.selectTool(.hand) + } label: { + Text("Hand Tool") + } + .keyboardShortcut("h", modifiers: [.option]) + Button { + tool.selectTool(.pen) + } label: { + Text("Pen Tool") + } + .keyboardShortcut("p", modifiers: [.option]) + Button { + tool.selectTool(.photo) + } label: { + Text("Photo Tool") + } + .keyboardShortcut("p", modifiers: [.option, .shift]) + } + } + } + CommandGroup(replacing: .pasteboard) { } + } +} + diff --git a/Memola/Shortcut/Commands/FileCommands.swift b/Memola/Shortcut/Commands/FileCommands.swift new file mode 100644 index 0000000..3a602ba --- /dev/null +++ b/Memola/Shortcut/Commands/FileCommands.swift @@ -0,0 +1,26 @@ +// +// FileCommands.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +struct FileCommands: Commands { + @Environment(\.shortcut) private var shortcut + @FocusedValue(\.activeSceneKey) private var appScene + + var body: some Commands { + CommandGroup(replacing: .newItem) { + if appScene == .memos { + Button { + shortcut.trigger(.newMemo) + } label: { + Text("New Memo") + } + .keyboardShortcut("n", modifiers: [.command]) + } + } + } +} diff --git a/Memola/Shortcut/Commands/ViewCommands.swift b/Memola/Shortcut/Commands/ViewCommands.swift new file mode 100644 index 0000000..12290e0 --- /dev/null +++ b/Memola/Shortcut/Commands/ViewCommands.swift @@ -0,0 +1,54 @@ +// +// ViewCommands.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +struct ViewCommands: Commands { + @ObservedObject private var application: Application + + @FocusedValue(\.activeSceneKey) private var appScene + + @FocusedObject var canvas: Canvas? + + init(application: Application) { + self.application = application + } + + var body: some Commands { + CommandGroup(replacing: .toolbar) { + switch appScene { + case .memos, .trash: + Button { + application.activateSearchBar() + } label: { + Text("Find Memo") + } + .keyboardShortcut("f", modifiers: [.command]) + Button { + application.toggleSidebar() + } label: { + switch application.sidebarVisibility { + case .shown: + Text("Hide Sidebar") + case .hidden: + Text("Show Sidebar") + } + } + .keyboardShortcut("o", modifiers: [.command]) + case .memo: + Button { + canvas?.toggleGridMode() + } label: { + Text("Change Grid Layout") + } + .keyboardShortcut("g", modifiers: [.option]) + default: + EmptyView() + } + } + } +} diff --git a/Memola/Shortcut/Core/Shortcut.swift b/Memola/Shortcut/Core/Shortcut.swift new file mode 100644 index 0000000..24c00ef --- /dev/null +++ b/Memola/Shortcut/Core/Shortcut.swift @@ -0,0 +1,25 @@ +// +// Shortcut.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import Combine +import Foundation + +final class Shortcut { + static let shared: Shortcut = .init() + + private let _publisher = PassthroughSubject() + + lazy var publisher: AnyPublisher = { + _publisher.eraseToAnyPublisher() + }() + + private init() { } + + func trigger(_ shortcut: Shortcuts) { + _publisher.send(shortcut) + } +} diff --git a/Memola/Shortcut/Core/Shortcuts.swift b/Memola/Shortcut/Core/Shortcuts.swift new file mode 100644 index 0000000..438600e --- /dev/null +++ b/Memola/Shortcut/Core/Shortcuts.swift @@ -0,0 +1,14 @@ +// +// Shortcuts.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import Foundation + +enum Shortcuts { + // MARK: - Memos + case newMemo + case findMemo +} diff --git a/Memola/Shortcut/EnvironmentValues/ShortcutKey.swift b/Memola/Shortcut/EnvironmentValues/ShortcutKey.swift new file mode 100644 index 0000000..4d1f1ab --- /dev/null +++ b/Memola/Shortcut/EnvironmentValues/ShortcutKey.swift @@ -0,0 +1,18 @@ +// +// ShortcutKey.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +private struct ShortcutKey: EnvironmentKey { + static var defaultValue: Shortcut = .shared +} + +extension EnvironmentValues { + var shortcut: Shortcut { + get { self[ShortcutKey.self] } + } +} diff --git a/Memola/Utilies/AppScene/ActiveSceneKey.swift b/Memola/Utilies/AppScene/ActiveSceneKey.swift new file mode 100644 index 0000000..b27d6d7 --- /dev/null +++ b/Memola/Utilies/AppScene/ActiveSceneKey.swift @@ -0,0 +1,19 @@ +// +// ActiveSceneKey.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import SwiftUI + +private struct ActiveSceneKey: FocusedValueKey { + typealias Value = AppScene +} + +extension FocusedValues { + var activeSceneKey: AppScene? { + get { self[ActiveSceneKey.self] } + set { self[ActiveSceneKey.self] = newValue } + } +} diff --git a/Memola/Utilies/AppScene/AppScene.swift b/Memola/Utilies/AppScene/AppScene.swift new file mode 100644 index 0000000..0c71200 --- /dev/null +++ b/Memola/Utilies/AppScene/AppScene.swift @@ -0,0 +1,14 @@ +// +// AppScene.swift +// Memola +// +// Created by Dscyre Scotti on 7/12/24. +// + +import Foundation + +enum AppScene { + case memos + case trash + case memo +} diff --git a/Memola/Extensions/Array++.swift b/Memola/Utilies/Extensions/Array++.swift similarity index 100% rename from Memola/Extensions/Array++.swift rename to Memola/Utilies/Extensions/Array++.swift diff --git a/Memola/Extensions/CGAffineTransform++.swift b/Memola/Utilies/Extensions/CGAffineTransform++.swift similarity index 100% rename from Memola/Extensions/CGAffineTransform++.swift rename to Memola/Utilies/Extensions/CGAffineTransform++.swift diff --git a/Memola/Extensions/CGFloat++.swift b/Memola/Utilies/Extensions/CGFloat++.swift similarity index 100% rename from Memola/Extensions/CGFloat++.swift rename to Memola/Utilies/Extensions/CGFloat++.swift diff --git a/Memola/Extensions/CGPoint++.swift b/Memola/Utilies/Extensions/CGPoint++.swift similarity index 100% rename from Memola/Extensions/CGPoint++.swift rename to Memola/Utilies/Extensions/CGPoint++.swift diff --git a/Memola/Extensions/CGRect++.swift b/Memola/Utilies/Extensions/CGRect++.swift similarity index 100% rename from Memola/Extensions/CGRect++.swift rename to Memola/Utilies/Extensions/CGRect++.swift diff --git a/Memola/Extensions/CGSize++.swift b/Memola/Utilies/Extensions/CGSize++.swift similarity index 100% rename from Memola/Extensions/CGSize++.swift rename to Memola/Utilies/Extensions/CGSize++.swift diff --git a/Memola/Extensions/Collection++.swift b/Memola/Utilies/Extensions/Collection++.swift similarity index 100% rename from Memola/Extensions/Collection++.swift rename to Memola/Utilies/Extensions/Collection++.swift diff --git a/Memola/Extensions/Color++.swift b/Memola/Utilies/Extensions/Color++.swift similarity index 50% rename from Memola/Extensions/Color++.swift rename to Memola/Utilies/Extensions/Color++.swift index 332db55..493769c 100644 --- a/Memola/Extensions/Color++.swift +++ b/Memola/Utilies/Extensions/Color++.swift @@ -8,8 +8,16 @@ import SwiftUI extension Color { + init(color: Platform.Color) { + #if os(macOS) + self = Color(nsColor: color) + #else + self = Color(uiColor: color) + #endif + } + var components: [CGFloat] { - let color = UIColor(self) + let color = Platform.Color(self) return color.components } @@ -20,6 +28,16 @@ extension Color { extension Color { var hsba: (hue: Double, saturation: Double, brightness: Double, alpha: Double) { + #if os(macOS) + #warning("TODO: need double check") + let nsColor = NSColor(self) + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + nsColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + return (hue, saturation, brightness, alpha) + #else let uiColor = UIColor(self) var hue: CGFloat = 0 var saturation: CGFloat = 0 @@ -27,13 +45,21 @@ extension Color { var alpha: CGFloat = 0 uiColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) return (hue, saturation, brightness, alpha) + #endif } } -extension UIColor { +extension Platform.Color { var components: [CGFloat] { + #if os(macOS) + #warning("TODO: need double check") + let nsColor: NSColor = self + let ciColor: CIColor = .init(color: nsColor) ?? CIColor(red: 0, green: 0, blue: 0) + return [ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha] + #else let uiColor: UIColor = self let ciColor: CIColor = .init(color: uiColor) return [ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha] + #endif } } diff --git a/Memola/Extensions/Data++.swift b/Memola/Utilies/Extensions/Data++.swift similarity index 100% rename from Memola/Extensions/Data++.swift rename to Memola/Utilies/Extensions/Data++.swift diff --git a/Memola/Extensions/Date++.swift b/Memola/Utilies/Extensions/Date++.swift similarity index 100% rename from Memola/Extensions/Date++.swift rename to Memola/Utilies/Extensions/Date++.swift diff --git a/Memola/Extensions/Float++.swift b/Memola/Utilies/Extensions/Float++.swift similarity index 100% rename from Memola/Extensions/Float++.swift rename to Memola/Utilies/Extensions/Float++.swift diff --git a/Memola/Utilies/Extensions/Image++.swift b/Memola/Utilies/Extensions/Image++.swift new file mode 100644 index 0000000..2c420c7 --- /dev/null +++ b/Memola/Utilies/Extensions/Image++.swift @@ -0,0 +1,89 @@ +// +// Image++.swift +// Memola +// +// Created by Dscyre Scotti on 6/15/24. +// + +import SwiftUI +import Foundation + +extension Image { + init(image: Platform.Image) { + #if os(macOS) + self = Image(nsImage: image) + #else + self = Image(uiImage: image) + #endif + } +} + +#if os(macOS) +extension NSImage { + func upsideDownMirrored() -> NSImage { + let degrees: CGFloat = 180 + let sinDegrees = abs(sin(degrees * CGFloat.pi / 180.0)) + let cosDegrees = abs(cos(degrees * CGFloat.pi / 180.0)) + let newSize = CGSize( + width: size.height * sinDegrees + size.width * cosDegrees, + height: size.width * sinDegrees + size.height * cosDegrees + ) + + let imageBounds = NSRect( + x: (newSize.width - size.width) / 2, + y: (newSize.height - size.height) / 2, + width: size.width, + height: size.height + ) + + let otherTransform = NSAffineTransform() + otherTransform.translateX(by: newSize.width / 2, yBy: newSize.height / 2) + otherTransform.rotate(byDegrees: degrees) + otherTransform.translateX(by: -newSize.width / 2, yBy: -newSize.height / 2) + + let rotatedImage = NSImage(size: newSize) + rotatedImage.lockFocus() + otherTransform.concat() + draw(in: imageBounds, from: CGRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0) + rotatedImage.unlockFocus() + + return rotatedImage + } + + func flipped(flipHorizontally: Bool = false, flipVertically: Bool = false) -> NSImage { + let flippedImage = NSImage(size: size) + + flippedImage.lockFocus() + + NSGraphicsContext.current?.imageInterpolation = .high + + let transform = NSAffineTransform() + transform.translateX(by: flipHorizontally ? size.width : 0, yBy: flipVertically ? size.height : 0) + transform.scaleX(by: flipHorizontally ? -1 : 1, yBy: flipVertically ? -1 : 1) + transform.concat() + + draw(at: .zero, from: NSRect(origin: .zero, size: size), operation: .sourceOver, fraction: 1) + + flippedImage.unlockFocus() + + return flippedImage + } +} +#endif + +#if os(iOS) +extension UIImage { + func imageWithUpOrientation() -> UIImage? { + switch imageOrientation { + case .up: + return self + default: + UIGraphicsBeginImageContextWithOptions(size, false, scale) + draw(in: CGRect(origin: .zero, size: size)) + let result = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return result + } + } +} +#endif diff --git a/Memola/Extensions/MTLDevice++.swift b/Memola/Utilies/Extensions/MTLDevice++.swift similarity index 100% rename from Memola/Extensions/MTLDevice++.swift rename to Memola/Utilies/Extensions/MTLDevice++.swift diff --git a/Memola/Extensions/MTLTexture++.swift b/Memola/Utilies/Extensions/MTLTexture++.swift similarity index 100% rename from Memola/Extensions/MTLTexture++.swift rename to Memola/Utilies/Extensions/MTLTexture++.swift diff --git a/Memola/Extensions/NSManagedObject++.swift b/Memola/Utilies/Extensions/NSManagedObject++.swift similarity index 100% rename from Memola/Extensions/NSManagedObject++.swift rename to Memola/Utilies/Extensions/NSManagedObject++.swift diff --git a/Memola/Extensions/NSManagedObjectContext++.swift b/Memola/Utilies/Extensions/NSManagedObjectContext++.swift similarity index 100% rename from Memola/Extensions/NSManagedObjectContext++.swift rename to Memola/Utilies/Extensions/NSManagedObjectContext++.swift diff --git a/Memola/Utilies/Extensions/NavigationSplitViewVisibility++.swift b/Memola/Utilies/Extensions/NavigationSplitViewVisibility++.swift new file mode 100644 index 0000000..1599039 --- /dev/null +++ b/Memola/Utilies/Extensions/NavigationSplitViewVisibility++.swift @@ -0,0 +1,33 @@ +// +// NavigationSplitViewVisibility++.swift +// Memola +// +// Created by Dscyre Scotti on 7/14/24. +// + +import SwiftUI + +extension NavigationSplitViewVisibility: RawRepresentable { + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .all + case 1: self = .automatic + case 2: self = .detailOnly + case 3: self = .doubleColumn + default: self = .all + } + } + + public var rawValue: Int { + switch self { + case .all: 0 + case .automatic: 1 + case .detailOnly: 2 + case .doubleColumn: 3 + default: -1 + } + } + + public typealias RawValue = Int +} + diff --git a/Memola/Extensions/View++.swift b/Memola/Utilies/Extensions/View++.swift similarity index 100% rename from Memola/Extensions/View++.swift rename to Memola/Utilies/Extensions/View++.swift diff --git a/Memola/Extensions/simd_float4x4++.swift b/Memola/Utilies/Extensions/simd_float4x4++.swift similarity index 100% rename from Memola/Extensions/simd_float4x4++.swift rename to Memola/Utilies/Extensions/simd_float4x4++.swift diff --git a/Memola/Utilies/Platform/Platform.swift b/Memola/Utilies/Platform/Platform.swift new file mode 100644 index 0000000..0b8c939 --- /dev/null +++ b/Memola/Utilies/Platform/Platform.swift @@ -0,0 +1,30 @@ +// +// Platform.swift +// Memola +// +// Created by Dscyre Scotti on 7/6/24. +// + +import SwiftUI + +enum Platform { + #if os(macOS) + typealias View = NSView + typealias Color = NSColor + typealias Image = NSImage + typealias ScrollView = NSSyncScrollView + typealias Application = NSApplication + typealias ViewController = NSViewController + typealias TapGestureRecognizer = NSClickGestureRecognizer + typealias ViewControllerRepresentable = NSViewControllerRepresentable + #else + typealias View = UIView + typealias Color = UIColor + typealias Image = UIImage + typealias ScrollView = UIScrollView + typealias Application = UIApplication + typealias ViewController = UIViewController + typealias TapGestureRecognizer = UITapGestureRecognizer + typealias ViewControllerRepresentable = UIViewControllerRepresentable + #endif +} diff --git a/Memola/Utilies/SidebarVisibility/SidebarVisibility.swift b/Memola/Utilies/SidebarVisibility/SidebarVisibility.swift new file mode 100644 index 0000000..7125d01 --- /dev/null +++ b/Memola/Utilies/SidebarVisibility/SidebarVisibility.swift @@ -0,0 +1,13 @@ +// +// SidebarVisibility.swift +// Memola +// +// Created by Dscyre Scotti on 7/14/24. +// + +import Foundation + +enum SidebarVisibility { + case shown + case hidden +}