├── .gitignore ├── README.md ├── ScreenNote.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── ScreenNote.xcscheme ├── ScreenNote ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Contents.json │ ├── DashBlack.colorset │ │ └── Contents.json │ ├── DashWhite.colorset │ │ └── Contents.json │ ├── PanelBackground.colorset │ │ └── Contents.json │ ├── StatusIcon.imageset │ │ ├── Contents.json │ │ ├── StatusIcon.png │ │ └── StatusIcon@2x.png │ ├── UniqueBlue.colorset │ │ └── Contents.json │ ├── UniqueGreen.colorset │ │ └── Contents.json │ ├── UniqueOrange.colorset │ │ └── Contents.json │ ├── UniquePurple.colorset │ │ └── Contents.json │ ├── UniqueRed.colorset │ │ └── Contents.json │ ├── UniqueViolet.colorset │ │ └── Contents.json │ ├── UniqueWhite.colorset │ │ └── Contents.json │ └── UniqueYello.colorset │ │ └── Contents.json ├── Data │ ├── Entity │ │ ├── AlignMethod.swift │ │ ├── Anchor.swift │ │ ├── ArrangeMethod.swift │ │ ├── CanvasVisible.swift │ │ ├── Curve.swift │ │ ├── FlipMethod.swift │ │ ├── InputTextProperties.swift │ │ ├── Line.swift │ │ ├── Object.swift │ │ ├── ObjectProperties.swift │ │ ├── ObjectType.swift │ │ ├── RotateMethod.swift │ │ ├── SettingsTabType.swift │ │ ├── TextOrientation.swift │ │ ├── ToggleMethod.swift │ │ ├── ToolBarDirection.swift │ │ └── ToolBarPosition.swift │ └── Repository │ │ ├── LaunchAtLoginRepository.swift │ │ └── UserDefaultsRepository.swift ├── Domain │ ├── Model │ │ ├── IssueReportModel.swift │ │ ├── ObjectModel.swift │ │ └── ShortcutModel.swift │ ├── ScreenNoteAppModel.swift │ └── ViewModel │ │ ├── CanvasSettingsViewModel.swift │ │ ├── CanvasViewModel.swift │ │ ├── GeneralSettingsViewModel.swift │ │ ├── MenuViewModel.swift │ │ ├── ToolBarModel.swift │ │ ├── WindowModel.swift │ │ └── WorkspaceViewModel.swift ├── Helper │ ├── AppKit+Extensions.swift │ ├── Color+Extensions.swift │ ├── CoreGraphics+Extensions.swift │ ├── ModifierFlag+Extensions.swift │ ├── Path+Extensions.swift │ ├── String+Extensions.swift │ └── Utils.swift ├── Info.plist ├── Localizable.xcstrings ├── Presentation │ ├── ScreenNoteApp.swift │ └── View │ │ ├── CanvasView.swift │ │ ├── MenuView.swift │ │ ├── PreActionButtonStyle.swift │ │ ├── Settings │ │ ├── CanvasSettingsView.swift │ │ ├── ColorButtonStyle.swift │ │ ├── GeneralSettingsView.swift │ │ ├── SelectableColorButtonStyle.swift │ │ └── SettingsView.swift │ │ ├── ShortcutPanel.swift │ │ ├── ShortcutView.swift │ │ ├── ToolBar │ │ ├── ColorPaletteButtonStyle.swift │ │ ├── ColorPalettePopover.swift │ │ ├── HorizontalToolBar.swift │ │ ├── LineWidthPopover.swift │ │ ├── ObjectAlignPopover.swift │ │ ├── ObjectArrangePopover.swift │ │ ├── ObjectFlipPopover.swift │ │ ├── ObjectRotatePopover.swift │ │ ├── ToolBarButtonStyle.swift │ │ ├── ToolBarRadioButtonStyle.swift │ │ └── VerticalToolBar.swift │ │ ├── WorkspacePanel.swift │ │ └── WorkspaceView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── ScreenNote.entitlements └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # ScreenNote 5 | materials/ 6 | 7 | # Xcode 8 | xcuserdata/ 9 | *.xcuserstate 10 | *.xccheckout 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenNote 2 | 3 | Paint Tool for Desktop 4 | 5 | AppStore [Download](https://apps.apple.com/us/app/screennote/id1258500140) 6 | 7 | ![image](screenshot.png) 8 | -------------------------------------------------------------------------------- /ScreenNote.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScreenNote.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ScreenNote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "3e417c196ca5eb18a6cf27f545860d94d16f8fdf34b64a6ff26ea46723ffd6be", 3 | "pins" : [ 4 | { 5 | "identity" : "spicekey", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Kyome22/SpiceKey.git", 8 | "state" : { 9 | "revision" : "246e9bdee634e6113c17e47e9184c8f61279718f", 10 | "version" : "5.3.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /ScreenNote.xcodeproj/xcshareddata/xcschemes/ScreenNote.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/DashBlack.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.250" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/DashWhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.795" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/PanelBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.800", 8 | "blue" : "0.894", 9 | "green" : "0.882", 10 | "red" : "0.839" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.800", 26 | "blue" : "0.118", 27 | "green" : "0.118", 28 | "red" : "0.114" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/StatusIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StatusIcon.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "StatusIcon@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | }, 18 | "properties" : { 19 | "template-rendering-intent" : "template" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/StatusIcon.imageset/StatusIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/StatusIcon.imageset/StatusIcon.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/StatusIcon.imageset/StatusIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/ScreenNote/Assets.xcassets/StatusIcon.imageset/StatusIcon@2x.png -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.718", 9 | "green" : "0.408", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.267", 9 | "green" : "0.600", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueOrange.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.596", 10 | "red" : "0.953" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniquePurple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.514", 9 | "green" : "0.027", 10 | "red" : "0.573" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.071", 9 | "green" : "0.000", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueViolet.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.533", 9 | "green" : "0.125", 10 | "red" : "0.114" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueWhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.941", 9 | "green" : "0.941", 10 | "red" : "0.941" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Assets.xcassets/UniqueYello.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.945", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/AlignMethod.swift: -------------------------------------------------------------------------------- 1 | /* 2 | AlignMethod.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/04. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum AlignMethod: Int, CaseIterable, Identifiable { 12 | case horizontalAlignLeft 13 | case horizontalAlignCenter 14 | case horizontalAlignRight 15 | case verticalAlignTop 16 | case verticalAlignCenter 17 | case verticalAlignBottom 18 | 19 | var id: Int { rawValue } 20 | 21 | var symbolName: String { 22 | switch self { 23 | case .horizontalAlignLeft: "align.horizontal.left.fill" 24 | case .horizontalAlignCenter: "align.horizontal.center.fill" 25 | case .horizontalAlignRight: "align.horizontal.right.fill" 26 | case .verticalAlignTop: "align.vertical.top.fill" 27 | case .verticalAlignCenter: "align.vertical.center.fill" 28 | case .verticalAlignBottom: "align.vertical.bottom.fill" 29 | } 30 | } 31 | 32 | var help: LocalizedStringKey { 33 | switch self { 34 | case .horizontalAlignLeft: "horizontalAlignLeft" 35 | case .horizontalAlignCenter: "horizontalAlignCenter" 36 | case .horizontalAlignRight: "horizontalAlignRight" 37 | case .verticalAlignTop: "verticalAlignTop" 38 | case .verticalAlignCenter: "verticalAlignCenter" 39 | case .verticalAlignBottom: "verticalAlignBottom" 40 | } 41 | } 42 | 43 | static let horizontals: [AlignMethod] = [ 44 | .horizontalAlignLeft, .horizontalAlignCenter, .horizontalAlignRight 45 | ] 46 | 47 | static let verticals: [AlignMethod] = [ 48 | .verticalAlignTop, .verticalAlignCenter, .verticalAlignBottom 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/Anchor.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Anchor.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import CoreGraphics 10 | 11 | enum Anchor: Int, CaseIterable { 12 | case topLeft 13 | case top 14 | case topRight 15 | case left 16 | case right 17 | case bottomLeft 18 | case bottom 19 | case bottomRight 20 | 21 | func center(with bounds: CGRect) -> CGPoint { 22 | switch self { 23 | case .topLeft: CGPoint(x: bounds.minX, y: bounds.minY) 24 | case .top: CGPoint(x: bounds.midX, y: bounds.minY) 25 | case .topRight: CGPoint(x: bounds.maxX, y: bounds.minY) 26 | case .left: CGPoint(x: bounds.minX, y: bounds.midY) 27 | case .right: CGPoint(x: bounds.maxX, y: bounds.midY) 28 | case .bottomLeft: CGPoint(x: bounds.minX, y: bounds.maxY) 29 | case .bottom: CGPoint(x: bounds.midX, y: bounds.maxY) 30 | case .bottomRight: CGPoint(x: bounds.maxX, y: bounds.maxY) 31 | } 32 | } 33 | 34 | func resize(bounds: CGRect, with diff: CGPoint) -> CGRect { 35 | // CGRect.size.width と CGRect.width は異なるので注意 36 | // 後者は正規化されて正の値になる 37 | let size = CGSize(width: bounds.width, height: bounds.height) 38 | var b = CGRect(origin: bounds.origin, size: size) 39 | switch self { 40 | case .topLeft: 41 | b.origin.x += diff.x 42 | b.origin.y += diff.y 43 | b.size.width -= diff.x 44 | b.size.height -= diff.y 45 | case .top: 46 | b.origin.y += diff.y 47 | b.size.height -= diff.y 48 | case .topRight: 49 | b.origin.y += diff.y 50 | b.size.width += diff.x 51 | b.size.height -= diff.y 52 | case .left: 53 | b.origin.x += diff.x 54 | b.size.width -= diff.x 55 | case .right: 56 | b.size.width += diff.x 57 | case .bottomLeft: 58 | b.origin.x += diff.x 59 | b.size.width -= diff.x 60 | b.size.height += diff.y 61 | case .bottom: 62 | b.size.height += diff.y 63 | case .bottomRight: 64 | b.size.width += diff.x 65 | b.size.height += diff.y 66 | } 67 | return b 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ArrangeMethod.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ArrangeMethod.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum ArrangeMethod: Int, CaseIterable, Identifiable { 12 | case bringToFrontmost 13 | case sendToBackmost 14 | 15 | var id: Int { rawValue } 16 | 17 | var symbolName: String { 18 | switch self { 19 | case .bringToFrontmost: "square.3.stack.3d.top.filled" 20 | case .sendToBackmost: "square.3.stack.3d.bottom.filled" 21 | } 22 | } 23 | 24 | var help: LocalizedStringKey { 25 | switch self { 26 | case .bringToFrontmost: "bringFront" 27 | case .sendToBackmost: "sendBack" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/CanvasVisible.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasVisible.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/18. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum CanvasVisible { 12 | case show 13 | case hide 14 | 15 | var label: LocalizedStringKey { 16 | switch self { 17 | case .show: "hideCanvas" 18 | case .hide: "showCanvas" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/Curve.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Curve.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import CoreGraphics 10 | 11 | struct Curve { 12 | let p1: CGPoint 13 | let c1: CGPoint 14 | let c2: CGPoint 15 | let p2: CGPoint 16 | 17 | init(_ points: [CGPoint]) { 18 | assert(points.count == 4, "points need 4 points") 19 | p1 = points[0] 20 | c1 = points[1] 21 | c2 = points[2] 22 | p2 = points[3] 23 | } 24 | 25 | func compute(_ t: CGFloat) -> CGPoint { 26 | var point = pow(1.0 - t, 3.0) * p1 27 | point += (3.0 * pow(1.0 - t, 2.0) * t) * c1 28 | point += (3.0 * (1.0 - t) * pow(t, 2.0)) * c2 29 | point += pow(t, 3.0) * p2 30 | return point 31 | } 32 | 33 | var points: [CGPoint] { 34 | (0 ..< 10).map { compute(CGFloat($0 + 1) / 10.0) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/FlipMethod.swift: -------------------------------------------------------------------------------- 1 | /* 2 | FlipMethod.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum FlipMethod: Int, CaseIterable, Identifiable { 12 | case flipHorizontal 13 | case flipVertical 14 | 15 | var id: Int { rawValue } 16 | 17 | var symbolName: String { 18 | switch self { 19 | case .flipHorizontal: 20 | "arrow.left.and.right.righttriangle.left.righttriangle.right.fill" 21 | case .flipVertical: 22 | "arrow.up.and.down.righttriangle.up.righttriangle.down.fill" 23 | } 24 | } 25 | 26 | var help: LocalizedStringKey { 27 | switch self { 28 | case .flipHorizontal: "flipHorizontal" 29 | case .flipVertical: "flipVertical" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/InputTextProperties.swift: -------------------------------------------------------------------------------- 1 | /* 2 | InputTextProperties.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/16. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import Foundation 10 | 11 | struct InputTextProperties { 12 | var object: Object 13 | var inputText: String 14 | var fontSize: CGFloat 15 | 16 | var inputTextOffset: CGSize { 17 | CGSize(width: object.bounds.minX, height: object.bounds.minY) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/Line.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Line.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import CoreGraphics 10 | 11 | struct Line { 12 | let p0: CGPoint 13 | let p1: CGPoint 14 | 15 | init(p0: CGPoint, p1: CGPoint) { 16 | self.p0 = p0 17 | self.p1 = p1 18 | } 19 | 20 | func intersects(_ line: Line) -> Bool { 21 | let p = (p0.x - p1.x) * (line.p0.y - p0.y) - (p0.y - p1.y) * (line.p0.x - p0.x) 22 | let q = (p0.x - p1.x) * (line.p1.y - p0.y) - (p0.y - p1.y) * (line.p1.x - p0.x) 23 | if 0 < p * q { 24 | return false 25 | } 26 | let r = (line.p0.x - line.p1.x) * (p0.y - line.p0.y) - (line.p0.y - line.p1.y) * (p0.x - line.p0.x) 27 | let s = (line.p0.x - line.p1.x) * (p1.y - line.p0.y) - (line.p0.y - line.p1.y) * (p1.x - line.p0.x) 28 | if 0 < r * s { 29 | return false 30 | } 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/Object.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Object.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct Object: Identifiable { 12 | let id: String = UUID().uuidString 13 | let type: ObjectType 14 | var color_: Color 15 | var opacity: CGFloat 16 | var lineWidth: CGFloat 17 | var points: [CGPoint] 18 | var text: String 19 | var textOrientation: TextOrientation 20 | var isSelected: Bool 21 | var isHidden: Bool 22 | 23 | var bounds: CGRect { 24 | let minX = points.min(by: { $0.x < $1.x })?.x ?? 0 25 | let minY = points.min(by: { $0.y < $1.y })?.y ?? 0 26 | let maxX = points.max(by: { $0.x < $1.x })?.x ?? 0 27 | let maxY = points.max(by: { $0.y < $1.y })?.y ?? 0 28 | return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 29 | } 30 | 31 | var path: Path { 32 | switch type { 33 | case .select: 34 | return Path(bounds) 35 | case .text: 36 | return Path(bounds) 37 | case .pen: 38 | if points.isEmpty { 39 | fatalError("points is empty") 40 | } 41 | if points.count < 2 { 42 | let rect = CGRect(origin: points[0] - CGPoint(0.5 * lineWidth), 43 | size: CGSize(lineWidth)) 44 | return Path(ellipseIn: rect) 45 | } else { 46 | var path = Path() 47 | path.addLines(points) 48 | return path 49 | } 50 | case .line: 51 | var path = Path() 52 | path.move(to: points[0]) 53 | path.addLine(to: points[1]) 54 | return path 55 | case .arrow: 56 | var path = Path() 57 | let p0 = points[0] 58 | let p1 = points[1] 59 | if p0 == p1 { return path } 60 | let length = 3.0 * lineWidth 61 | let d = 2.0 * lineWidth 62 | let phi = p0.radian(from: p1) 63 | let p2 = p1 + length * CGPoint(x: cos(phi), y: sin(phi)) 64 | let r = 0.5 * lineWidth 65 | let phi_po_90 = phi + CGFloat.pi / 2.0 66 | let phi_ne_90 = phi - CGFloat.pi / 2.0 67 | let cos_po = cos(phi_po_90) 68 | let sin_po = sin(phi_po_90) 69 | let cos_ne = cos(phi_ne_90) 70 | let sin_ne = sin(phi_ne_90) 71 | if p0.length(from: p1) < length { 72 | path.move(to: p2 + d * CGPoint(x: cos_ne, y: sin_ne)) 73 | path.addLine(to: p1 + r * CGPoint(x: -cos(phi), y: -sin(phi))) 74 | path.addLine(to: p2 + d * CGPoint(x: cos_po, y: sin_po)) 75 | path.closeSubpath() 76 | } else { 77 | path.move(to: p0 + r * CGPoint(x: cos_ne, y: sin_ne)) 78 | path.addLine(to: p2 + r * CGPoint(x: -cos_po, y: -sin_po)) 79 | path.addLine(to: p2 + d * CGPoint(x: -cos_po, y: -sin_po)) 80 | path.addLine(to: p1 + r * CGPoint(x: -cos(phi), y: -sin(phi))) 81 | path.addLine(to: p2 + d * CGPoint(x: -cos_ne, y: -sin_ne)) 82 | path.addLine(to: p2 + r * CGPoint(x: -cos_ne, y: -sin_ne)) 83 | path.addLine(to: p0 + r * CGPoint(x: cos_po, y: sin_po)) 84 | path.addArc(center: p0, 85 | radius: r, 86 | startAngle: Angle(radians: phi_po_90), 87 | endAngle: Angle(radians: phi_ne_90), 88 | clockwise: true) 89 | path.closeSubpath() 90 | } 91 | return path 92 | case .fillRect, .lineRect: 93 | return Path(bounds) 94 | case .fillOval, .lineOval: 95 | return Path(ellipseIn: bounds) 96 | } 97 | } 98 | 99 | var strokeStyle: StrokeStyle? { 100 | switch type { 101 | case .select, .text, .arrow, .fillRect, .fillOval: 102 | nil 103 | case .pen where points.count < 2: 104 | nil 105 | case .pen: 106 | StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round) 107 | case .line: 108 | StrokeStyle(lineWidth: lineWidth, lineCap: .round) 109 | case .lineRect, .lineOval: 110 | StrokeStyle(lineWidth: lineWidth) 111 | } 112 | } 113 | 114 | var color: Color { 115 | color_.opacity(opacity) 116 | } 117 | 118 | var fontSize: CGFloat { 119 | let numberOfLines = text.components(separatedBy: .newlines).count 120 | let height = textOrientation.size(of: bounds).height 121 | let heightOfLine = height / CGFloat(numberOfLines) 122 | var estimatedFontSize = (2.0 * (heightOfLine - 0.2078) / 1.176).rounded() / 2.0 123 | var estimatedSize = text.calculateSize(using: NSFont.systemFont(ofSize: estimatedFontSize)) 124 | while height < estimatedSize.height { 125 | estimatedFontSize -= 0.1 126 | estimatedSize = text.calculateSize(using: NSFont.systemFont(ofSize: estimatedFontSize)) 127 | } 128 | return estimatedFontSize 129 | } 130 | 131 | init ( 132 | _ type: ObjectType, 133 | _ color: Color, 134 | _ opacity: CGFloat, 135 | _ lineWidth: CGFloat, 136 | _ points: [CGPoint], 137 | isSelected: Bool = false 138 | ) { 139 | self.type = type 140 | self.color_ = color 141 | self.opacity = opacity 142 | self.lineWidth = lineWidth 143 | self.points = points 144 | self.text = "" 145 | self.textOrientation = .up 146 | self.isSelected = isSelected 147 | self.isHidden = false 148 | } 149 | 150 | init ( 151 | _ color: Color, 152 | _ opacity: CGFloat, 153 | _ points: [CGPoint], 154 | _ text: String, 155 | _ textOrientation: TextOrientation, 156 | isSelected: Bool = false 157 | ) { 158 | self.type = .text 159 | self.color_ = color 160 | self.opacity = opacity 161 | self.lineWidth = 1.0 162 | self.points = points 163 | self.text = text 164 | self.textOrientation = textOrientation 165 | self.isSelected = isSelected 166 | self.isHidden = false 167 | } 168 | 169 | func isHit(point: CGPoint) -> Bool { 170 | switch type { 171 | case .pen, .line, .lineRect, .lineOval: 172 | path.intersects(with: point, radius: max(4.0, 0.5 * lineWidth)) 173 | case .text, .arrow, .fillRect, .fillOval: 174 | path.contains(point) 175 | default: 176 | false 177 | } 178 | } 179 | 180 | func isHit(rect: CGRect) -> Bool { 181 | if rect.isEmpty { 182 | false 183 | } else if rect.contains(bounds) { 184 | true 185 | } else { 186 | path.intersects(Path(rect)) 187 | } 188 | } 189 | 190 | func copy(needsOffset: Bool = false) -> Object { 191 | let newPoints = needsOffset ? points.map { $0 + CGPoint(20) } : points 192 | return if type == .text { 193 | Object(color_, opacity, newPoints, text, textOrientation, isSelected: true) 194 | } else { 195 | Object(type, color_, opacity, lineWidth, newPoints, isSelected: true) 196 | } 197 | } 198 | 199 | func textOffset(from bounds: CGRect) -> CGPoint { 200 | CGPoint(x: bounds.midX, y: bounds.midY) 201 | } 202 | 203 | func textRect(from bounds: CGRect) -> CGRect { 204 | let size = textOrientation.size(of: bounds) 205 | return CGRect( 206 | x: -0.5 * size.width, 207 | y: -0.5 * size.height, 208 | width: size.width, 209 | height: size.height 210 | ) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ObjectProperties.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectProperties.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/16. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ObjectProperties { 12 | var color: Color 13 | var opacity: CGFloat 14 | var lineWidth: CGFloat 15 | 16 | static let `default` = Self(color: Color(.uniqueWhite), opacity: 0.8, lineWidth: 4.0) 17 | } 18 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ObjectType.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectType.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum ObjectType: Int, CaseIterable, Identifiable { 12 | case select 13 | case text 14 | case pen 15 | case line 16 | case arrow 17 | case fillRect 18 | case lineRect 19 | case fillOval 20 | case lineOval 21 | 22 | var id: Int { rawValue } 23 | 24 | var symbolName: String { 25 | switch self { 26 | case .select: "hand.point.up.left.fill" 27 | case .text: "textbox" 28 | case .pen: "scribble" 29 | case .line: "line.diagonal" 30 | case .arrow: "line.diagonal.arrow" 31 | case .fillRect: "square.fill" 32 | case .lineRect: "square" 33 | case .fillOval: "oval.fill" 34 | case .lineOval: "oval" 35 | } 36 | } 37 | 38 | var label: LocalizedStringKey { 39 | switch self { 40 | case .select: "toolSelect" 41 | case .text: "toolText" 42 | case .pen: "toolPen" 43 | case .line: "toolLine" 44 | case .arrow: "toolArrow" 45 | case .fillRect: "toolFillRect" 46 | case .lineRect: "toolLineRect" 47 | case .fillOval: "toolFillOval" 48 | case .lineOval: "toolLineOval" 49 | } 50 | } 51 | 52 | var help: LocalizedStringKey { 53 | switch self { 54 | case .select: "helpSelect" 55 | case .text: "helpText" 56 | case .pen: "helpPen" 57 | case .line: "helpLine" 58 | case .arrow: "helpArrow" 59 | case .fillRect: "helpFillRect" 60 | case .lineRect: "helpLineRect" 61 | case .fillOval: "helpFillOval" 62 | case .lineOval: "helpLineOval" 63 | } 64 | } 65 | 66 | static let defaultObjects: [ObjectType] = [ 67 | .text, .pen, .line, .arrow, .fillRect, .lineRect, .fillOval, .lineOval 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/RotateMethod.swift: -------------------------------------------------------------------------------- 1 | /* 2 | RotateMethod.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum RotateMethod: Int, CaseIterable, Identifiable { 12 | case rotateRight 13 | case rotateLeft 14 | 15 | var id: Int { rawValue } 16 | 17 | var angle: CGFloat { 18 | (self == .rotateRight ? 0.5 : -0.5) * CGFloat.pi 19 | } 20 | 21 | var symbolName: String { 22 | switch self { 23 | case .rotateRight: "rotate.right.fill" 24 | case .rotateLeft: "rotate.left.fill" 25 | } 26 | } 27 | 28 | var help: LocalizedStringKey { 29 | switch self { 30 | case .rotateRight: "rotateRight" 31 | case .rotateLeft: "rotateLeft" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/SettingsTabType.swift: -------------------------------------------------------------------------------- 1 | /* 2 | SettingsTabType.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | enum SettingsTabType { 10 | case general 11 | case canvas 12 | } 13 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/TextOrientation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | TextOrientation.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/10. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum TextOrientation: Int, CaseIterable { 12 | case up 13 | case right 14 | case down 15 | case left 16 | case upMirrored 17 | case rightMirrored 18 | case downMirrored 19 | case leftMirrored 20 | 21 | var angle: Angle { 22 | switch self { 23 | case .up: Angle(degrees: 0) 24 | case .right: Angle(degrees: 90) 25 | case .down: Angle(degrees: 180) 26 | case .left: Angle(degrees: 270) 27 | case .upMirrored: Angle(degrees: 0) 28 | case .rightMirrored: Angle(degrees: 90) 29 | case .downMirrored: Angle(degrees: 180) 30 | case .leftMirrored: Angle(degrees: 270) 31 | } 32 | } 33 | 34 | var scale: CGFloat { 35 | switch self { 36 | case .up, .right, .down, .left: 37 | 1.0 38 | case .upMirrored, .rightMirrored, .downMirrored, .leftMirrored: 39 | -1.0 40 | } 41 | } 42 | 43 | var angle3D: Angle { 44 | switch self { 45 | case .up: Angle(degrees: 0) 46 | case .right: Angle(degrees: 90) 47 | case .down: Angle(degrees: 180) 48 | case .left: Angle(degrees: 270) 49 | case .upMirrored: Angle(degrees: 180) 50 | case .rightMirrored: Angle(degrees: 180) 51 | case .downMirrored: Angle(degrees: 180) 52 | case .leftMirrored: Angle(degrees: 180) 53 | } 54 | } 55 | 56 | var axis: (x: CGFloat, y: CGFloat, z: CGFloat) { 57 | switch self { 58 | case .up: (x: 0_, y: 0, z: 1) 59 | case .right: (x: 0_, y: 0, z: 1) 60 | case .down: (x: 0_, y: 0, z: 1) 61 | case .left: (x: 0_, y: 0, z: 1) 62 | case .upMirrored: (x: 0_, y: 1, z: 0) 63 | case .rightMirrored: (x: 1_, y: 1, z: 0) 64 | case .downMirrored: (x: 1_, y: 0, z: 0) 65 | case .leftMirrored: (x: -1, y: 1, z: 0) 66 | } 67 | } 68 | 69 | func size(of bounds: CGRect) -> CGSize { 70 | switch self { 71 | case .up, .down, .upMirrored, .downMirrored: 72 | CGSize(width: bounds.width, height: bounds.height) 73 | case .right, .left, .rightMirrored, .leftMirrored: 74 | CGSize(width: bounds.height, height: bounds.width) 75 | } 76 | } 77 | 78 | private func rotateRight() -> Self { 79 | switch self { 80 | case .up: .right 81 | case .right: .down 82 | case .down: .left 83 | case .left: .up 84 | case .upMirrored: .leftMirrored 85 | case .rightMirrored: .upMirrored 86 | case .downMirrored: .rightMirrored 87 | case .leftMirrored: .downMirrored 88 | } 89 | } 90 | 91 | private func rotateLeft() -> Self { 92 | switch self { 93 | case .up: .left 94 | case .right: .up 95 | case .down: .right 96 | case .left: .down 97 | case .upMirrored: .rightMirrored 98 | case .rightMirrored: .downMirrored 99 | case .downMirrored: .leftMirrored 100 | case .leftMirrored: .upMirrored 101 | } 102 | } 103 | 104 | func rotate(_ rotateMethod: RotateMethod) -> Self { 105 | switch rotateMethod { 106 | case .rotateRight: rotateRight() 107 | case .rotateLeft: rotateLeft() 108 | } 109 | } 110 | 111 | private func flipHorizontal() -> Self { 112 | switch self { 113 | case .up: .upMirrored 114 | case .right: .rightMirrored 115 | case .down: .downMirrored 116 | case .left: .leftMirrored 117 | case .upMirrored: .up 118 | case .rightMirrored: .right 119 | case .downMirrored: .down 120 | case .leftMirrored: .left 121 | } 122 | } 123 | 124 | private func flipVertical() -> Self { 125 | switch self { 126 | case .up: .downMirrored 127 | case .right: .leftMirrored 128 | case .down: .upMirrored 129 | case .left: .rightMirrored 130 | case .upMirrored: .down 131 | case .rightMirrored: .left 132 | case .downMirrored: .up 133 | case .leftMirrored: .right 134 | } 135 | } 136 | 137 | func flip(_ flipMethod: FlipMethod) -> Self { 138 | switch flipMethod { 139 | case .flipHorizontal: flipHorizontal() 140 | case .flipVertical: flipVertical() 141 | } 142 | } 143 | 144 | func endPosition(with position: CGPoint, size: CGSize) -> CGPoint { 145 | switch self { 146 | case .up: CGPoint(x: position.x + size.width, y: position.y + size.height) 147 | case .right: CGPoint(x: position.x - size.height, y: position.y + size.width) 148 | case .down: CGPoint(x: position.x - size.width, y: position.y - size.height) 149 | case .left: CGPoint(x: position.x + size.height, y: position.y - size.width) 150 | case .upMirrored: CGPoint(x: position.x - size.width, y: position.y + size.height) 151 | case .rightMirrored: CGPoint(x: position.x + size.height, y: position.y + size.width) 152 | case .downMirrored: CGPoint(x: position.x + size.width, y: position.y - size.height) 153 | case .leftMirrored: CGPoint(x: position.x - size.height, y: position.y - size.width) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ToggleMethod.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToggleMethod.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum ToggleMethod: Int, CaseIterable, Identifiable { 12 | case longPressKey 13 | case pressBothSideKeys 14 | 15 | var id: Int { rawValue } 16 | 17 | var label: LocalizedStringKey { 18 | switch self { 19 | case .longPressKey: "longPressModifierKey" 20 | case .pressBothSideKeys: "pressBothSideModifierKeys" 21 | } 22 | } 23 | 24 | var panelWidth: CGFloat { 25 | switch self { 26 | case .longPressKey: 280 27 | case .pressBothSideKeys: 380 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ToolBarDirection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToolBarDirection.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/01. 6 | 7 | */ 8 | 9 | enum ToolBarDirection: Int { 10 | case horizontal 11 | case vertical 12 | } 13 | -------------------------------------------------------------------------------- /ScreenNote/Data/Entity/ToolBarPosition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToolBarPosition.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | enum ToolBarPosition: Int, CaseIterable, Identifiable { 12 | case top 13 | case right 14 | case bottom 15 | case left 16 | 17 | var id: Int { rawValue } 18 | 19 | var label: LocalizedStringKey { 20 | switch self { 21 | case .top: "top" 22 | case .right: "right" 23 | case .bottom: "bottom" 24 | case .left: "left" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ScreenNote/Data/Repository/LaunchAtLoginRepository.swift: -------------------------------------------------------------------------------- 1 | /* 2 | LaunchAtLoginRepository.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Foundation 10 | import ServiceManagement 11 | 12 | protocol LaunchAtLoginRepository: AnyObject { 13 | var current: Bool { get } 14 | 15 | init() 16 | 17 | func switchRegistration(_ newValue: Bool, failureHandler: @escaping () -> Void) 18 | } 19 | 20 | final class LaunchAtLoginRepositoryImpl: LaunchAtLoginRepository { 21 | var current: Bool { 22 | SMAppService.mainApp.status == .enabled 23 | } 24 | 25 | func switchRegistration(_ newValue: Bool, failureHandler: @escaping () -> Void) { 26 | do { 27 | if newValue { 28 | try SMAppService.mainApp.register() 29 | } else { 30 | try SMAppService.mainApp.unregister() 31 | } 32 | } catch { 33 | logput(error.localizedDescription) 34 | } 35 | if current != newValue { 36 | failureHandler() 37 | } 38 | } 39 | } 40 | 41 | // MARK: - Preview Mock 42 | extension PreviewMock { 43 | final class LaunchAtLoginRepositoryMock: LaunchAtLoginRepository { 44 | var current: Bool = false 45 | func switchRegistration(_ newValue: Bool, failureHandler: @escaping () -> Void) {} 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ScreenNote/Data/Repository/UserDefaultsRepository.swift: -------------------------------------------------------------------------------- 1 | /* 2 | UserDefaultsRepository.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Combine 10 | import Foundation 11 | import SpiceKey 12 | 13 | fileprivate let RESET_USER_DEFAULTS = false 14 | 15 | protocol UserDefaultsRepository: AnyObject { 16 | var updateShortcutPublisher: AnyPublisher { get } 17 | 18 | var toggleMethod: ToggleMethod { get set } 19 | var modifierFlag: ModifierFlag { get set } 20 | var longPressSeconds: Double { get set } 21 | var toolBarPosition: ToolBarPosition { get set } 22 | var showToggleMethod: Bool { get set } 23 | var clearAllObjects: Bool { get set } 24 | var defaultObjectType: ObjectType { get set } 25 | var defaultColorIndex: Int { get set } 26 | var defaultOpacity: CGFloat { get set } 27 | var defaultLineWidth: CGFloat { get set } 28 | var backgroundColorIndex: Int { get set } 29 | var backgroundOpacity: CGFloat { get set } 30 | } 31 | 32 | final class UserDefaultsRepositoryImpl: UserDefaultsRepository { 33 | private let userDefaults: UserDefaults 34 | 35 | private let updateShortcutSubject = PassthroughSubject() 36 | var updateShortcutPublisher: AnyPublisher { 37 | updateShortcutSubject.eraseToAnyPublisher() 38 | } 39 | 40 | var toggleMethod: ToggleMethod { 41 | get { ToggleMethod(rawValue: userDefaults.integer(forKey: "toggleMethod"))! } 42 | set { 43 | userDefaults.set(newValue.rawValue, forKey: "toggleMethod") 44 | updateShortcutSubject.send() 45 | } 46 | } 47 | 48 | var modifierFlag: ModifierFlag { 49 | get { ModifierFlag(rawValue: userDefaults.integer(forKey: "modifierFlag"))! } 50 | set { 51 | userDefaults.set(newValue.rawValue, forKey: "modifierFlag") 52 | updateShortcutSubject.send() 53 | } 54 | } 55 | 56 | var longPressSeconds: Double { 57 | get { userDefaults.double(forKey: "longPressSeconds") } 58 | set { 59 | userDefaults.set(newValue, forKey: "longPressSeconds") 60 | updateShortcutSubject.send() 61 | } 62 | } 63 | 64 | var toolBarPosition: ToolBarPosition { 65 | get { ToolBarPosition(rawValue: userDefaults.integer(forKey: "toolBarPosition"))! } 66 | set { userDefaults.set(newValue.rawValue, forKey: "toolBarPosition") } 67 | } 68 | 69 | var showToggleMethod: Bool { 70 | get { userDefaults.bool(forKey: "showToggleMethod") } 71 | set { userDefaults.set(newValue, forKey: "showToggleMethod") } 72 | } 73 | 74 | var clearAllObjects: Bool { 75 | get { userDefaults.bool(forKey: "clearAllObjects") } 76 | set { userDefaults.set(newValue, forKey: "clearAllObjects") } 77 | } 78 | 79 | var defaultObjectType: ObjectType { 80 | get { ObjectType(rawValue: userDefaults.integer(forKey: "defaultObjectType"))! } 81 | set { userDefaults.set(newValue.rawValue, forKey: "defaultObjectType") } 82 | } 83 | 84 | var defaultColorIndex: Int { 85 | get { userDefaults.integer(forKey: "defaultColorIndex") } 86 | set { userDefaults.set(newValue, forKey: "defaultColorIndex") } 87 | } 88 | 89 | var defaultOpacity: CGFloat { 90 | get { userDefaults.double(forKey: "defaultOpacity") } 91 | set { userDefaults.set(newValue, forKey: "defaultOpacity") } 92 | } 93 | 94 | var defaultLineWidth: CGFloat { 95 | get { userDefaults.double(forKey: "defaultLineWidth") } 96 | set { userDefaults.set(newValue, forKey: "defaultLineWidth") } 97 | } 98 | 99 | var backgroundColorIndex: Int { 100 | get { userDefaults.integer(forKey: "backgroundColorIndex") } 101 | set { userDefaults.set(newValue, forKey: "backgroundColorIndex") } 102 | } 103 | 104 | var backgroundOpacity: CGFloat { 105 | get { userDefaults.double(forKey: "backgroundOpacity") } 106 | set { userDefaults.set(newValue, forKey: "backgroundOpacity") } 107 | } 108 | 109 | init(userDefaults: UserDefaults = .standard) { 110 | self.userDefaults = userDefaults 111 | #if DEBUG 112 | if RESET_USER_DEFAULTS { 113 | self.userDefaults.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) 114 | } 115 | #endif 116 | self.userDefaults.register(defaults: [ 117 | "toggleMethod": ToggleMethod.longPressKey.rawValue, 118 | "modifierFlag": ModifierFlag.control.rawValue, 119 | "longPressSeconds": Double(0.5), 120 | "toolBarPosition": ToolBarPosition.top.rawValue, 121 | "showToggleMethod": true, 122 | "clearAllObjects": false, 123 | "defaultObjectType": ObjectType.pen.rawValue, 124 | "defaultColorIndex": Int(0), 125 | "defaultOpacity": Double(0.8), 126 | "defaultLineWidth": Double(4.0), 127 | "backgroundColorIndex": Int(0), 128 | "backgroundOpacity": Double(0.02) 129 | ]) 130 | #if DEBUG 131 | showAllData() 132 | #endif 133 | } 134 | 135 | private func showAllData() { 136 | if let dict = userDefaults.persistentDomain(forName: Bundle.main.bundleIdentifier!) { 137 | for (key, value) in dict.sorted(by: { $0.0 < $1.0 }) { 138 | Swift.print("\(key) => \(value)") 139 | } 140 | } 141 | } 142 | } 143 | 144 | // MARK: - Preview Mock 145 | extension PreviewMock { 146 | final class UserDefaultsRepositoryMock: UserDefaultsRepository { 147 | var updateShortcutPublisher: AnyPublisher { 148 | Just(()).eraseToAnyPublisher() 149 | } 150 | 151 | var toggleMethod: ToggleMethod = .longPressKey 152 | var modifierFlag: ModifierFlag = .control 153 | var longPressSeconds: Double = 0.5 154 | var toolBarPosition: ToolBarPosition = .top 155 | var showToggleMethod: Bool = true 156 | var clearAllObjects: Bool = false 157 | var defaultObjectType: ObjectType = .pen 158 | var defaultColorIndex: Int = 0 159 | var defaultOpacity: CGFloat = 0.8 160 | var defaultLineWidth: CGFloat = 4.0 161 | var backgroundColorIndex: Int = 0 162 | var backgroundOpacity: CGFloat = 0.02 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /ScreenNote/Domain/Model/IssueReportModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | IssueReportModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Cocoa 10 | 11 | protocol IssueReportModel { 12 | static func send() 13 | } 14 | 15 | struct IssueReporterModelImpl: IssueReportModel { 16 | static func send() { 17 | let appName = "CFBundleName".infoString 18 | let appVersion = "CFBundleShortVersionString".infoString 19 | let os = ProcessInfo.processInfo.operatingSystemVersion 20 | let systemVersion = "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)" 21 | 22 | let service = NSSharingService(named: NSSharingService.Name.composeEmail)! 23 | service.recipients = ["kyomesuke@icloud.com"] 24 | service.subject = "\(appName) \(String(localized: "issueReport"))" 25 | service.perform(withItems: [ 26 | String(localized: "environment\(appName)\(appVersion)\(systemVersion)"), 27 | String(localized: "whatYouTried"), 28 | String(localized: "shortDescription"), 29 | String(localized: "reproduceIssue"), 30 | String(localized: "expectedResult") 31 | ]) 32 | } 33 | } 34 | 35 | // MARK: - Preview Mock 36 | extension PreviewMock { 37 | struct IssueReportModelMock: IssueReportModel { 38 | static func send() {} 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ScreenNote/Domain/Model/ObjectModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | protocol ObjectModel: AnyObject { 13 | var colors: [[Color]] { get } 14 | var objectTypePublisher: AnyPublisher { get } 15 | var objectType: ObjectType { get } 16 | var objectsPublisher: AnyPublisher<[Object], Never> { get } 17 | var selectedObjectsBoundsPublisher: AnyPublisher { get } 18 | var isSelectingPublisher: AnyPublisher { get } 19 | var objectPropertiesPublisher: AnyPublisher { get } 20 | var color: Color { get } 21 | var opacity: CGFloat { get } 22 | var lineWidth: CGFloat { get } 23 | var inputTextPropertiesPublisher: AnyPublisher { get } 24 | 25 | init(_ userDefaultsRepository: UserDefaultsRepository) 26 | 27 | func endEditing(_ inputTextProperties: InputTextProperties) 28 | func dragBegan(location: CGPoint, 29 | selectBeganHandler: @escaping () -> Void) 30 | func dragMoved(startLocation: CGPoint, 31 | location: CGPoint, 32 | selectMovedHandler: @escaping () -> Void) 33 | func dragEnded(startLocation: CGPoint, 34 | location: CGPoint, 35 | selectionBounds: CGRect?, 36 | selectEndedHandler: @escaping () -> Void) 37 | 38 | func resetDefaultSettings() 39 | func undo() 40 | func redo() 41 | func resetHistory() 42 | func updateObjectType(_ objectType: ObjectType) 43 | func updateColor(_ color: Color) 44 | func startUpdatingOpacity() 45 | func updateOpacity(_ opacity: CGFloat) 46 | func startUpdatingLineWidth() 47 | func updateLineWidth(_ lineWidth: CGFloat) 48 | func arrange(_ arrangeMethod: ArrangeMethod) 49 | func align(_ alignMethod: AlignMethod) 50 | func flip(_ flipMethod: FlipMethod) 51 | func rotate(_ rotateMethod: RotateMethod) 52 | func duplicateSelectedObjects() 53 | func delete() 54 | func selectAll() 55 | func clear() 56 | } 57 | 58 | final class ObjectModelImpl: ObjectModel { 59 | enum Action { 60 | case none 61 | case move 62 | case resize 63 | } 64 | 65 | enum SelectType { 66 | case rectangle 67 | case selectOne(Int) 68 | case keep 69 | } 70 | 71 | private let userDefaultsRepository: UserDefaultsRepository 72 | private let undoManager = UndoManager() 73 | private var lastObjects = [Object]() 74 | private var currentAction: Action = .none 75 | private var currentAnchor: Anchor = .topLeft 76 | private var currentSelectType: SelectType = .rectangle 77 | private var cancellables = Set() 78 | 79 | let colors: [[Color]] 80 | 81 | private let objectTypeSubject = CurrentValueSubject(.pen) 82 | var objectTypePublisher: AnyPublisher { 83 | objectTypeSubject.eraseToAnyPublisher() 84 | } 85 | var objectType: ObjectType { 86 | objectTypeSubject.value 87 | } 88 | 89 | private let objectsSubject = CurrentValueSubject<[Object], Never>([]) 90 | var objectsPublisher: AnyPublisher<[Object], Never> { 91 | objectsSubject.eraseToAnyPublisher() 92 | } 93 | private var objects: [Object] { 94 | objectsSubject.value 95 | } 96 | private var selectedObjects: [Object] { 97 | objectsSubject.value.filter { $0.isSelected } 98 | } 99 | 100 | private var selectedObjectsBoundsSubject = CurrentValueSubject(nil) 101 | var selectedObjectsBoundsPublisher: AnyPublisher { 102 | selectedObjectsBoundsSubject.eraseToAnyPublisher() 103 | } 104 | private var selectedObjectsBounds: CGRect? { 105 | selectedObjectsBoundsSubject.value 106 | } 107 | 108 | private let isSelectingSubject = CurrentValueSubject(false) 109 | var isSelectingPublisher: AnyPublisher { 110 | isSelectingSubject.eraseToAnyPublisher() 111 | } 112 | private var isSelecting: Bool { 113 | isSelectingSubject.value 114 | } 115 | 116 | private let objectPropertiesSubject = CurrentValueSubject(.default) 117 | var objectPropertiesPublisher: AnyPublisher { 118 | objectPropertiesSubject.eraseToAnyPublisher() 119 | } 120 | var color: Color { 121 | objectPropertiesSubject.value.color 122 | } 123 | var opacity: CGFloat { 124 | objectPropertiesSubject.value.opacity 125 | } 126 | var lineWidth: CGFloat { 127 | objectPropertiesSubject.value.lineWidth 128 | } 129 | 130 | private let inputTextPropertiesSubject = CurrentValueSubject(nil) 131 | var inputTextPropertiesPublisher: AnyPublisher { 132 | inputTextPropertiesSubject.eraseToAnyPublisher() 133 | } 134 | 135 | init(_ userDefaultsRepository: UserDefaultsRepository) { 136 | self.userDefaultsRepository = userDefaultsRepository 137 | colors = Color.palette 138 | undoManager.levelsOfUndo = 15 139 | objectsPublisher 140 | .sink { [weak self] _ in 141 | self?.updatedObjects() 142 | } 143 | .store(in: &cancellables) 144 | } 145 | 146 | private func unselectedObjects() -> [Object] { 147 | var objects_ = objects 148 | for i in objects_.indices where objects_[i].isSelected { 149 | objects_[i].isSelected = false 150 | } 151 | return objects_ 152 | } 153 | 154 | private func objectsBounds(_ objects: [Object]) -> CGRect? { 155 | objects.reduce(CGRect?.none) { partialResult, object in 156 | return partialResult?.union(object.bounds) ?? object.bounds 157 | } 158 | } 159 | 160 | private func updatedObjects() { 161 | let bounds = objectsBounds(selectedObjects) 162 | selectedObjectsBoundsSubject.send(bounds) 163 | let isSelecting = (objectType == .select && bounds != nil) 164 | isSelectingSubject.send(isSelecting) 165 | } 166 | 167 | private func hitAnchor(_ point: CGPoint) -> Anchor? { 168 | guard let bounds = objectsBounds(selectedObjects) else { return nil } 169 | let anchors = Path.anchorPaths(bounds: bounds) 170 | return zip(anchors, Anchor.allCases) 171 | .first { $0.0.contains(point) }?.1 172 | } 173 | 174 | private func move(_ diff: CGPoint) -> [Object] { 175 | lastObjects.map { object in 176 | guard object.isSelected else { return object } 177 | var copyObject = object.copy() 178 | copyObject.points = copyObject.points.map { $0 + diff } 179 | return copyObject 180 | } 181 | } 182 | 183 | private func resize(_ diff: CGPoint) -> [Object] { 184 | let lastSelectedObjects = lastObjects.filter { $0.isSelected } 185 | guard let oldBounds = objectsBounds(lastSelectedObjects) else { 186 | return lastObjects 187 | } 188 | let newBounds = currentAnchor.resize(bounds: oldBounds, with: diff) 189 | // Do not refactor 190 | let transforms: [CGAffineTransform] = [ 191 | .init(translationX: -oldBounds.origin.x, y: -oldBounds.origin.y), 192 | .init(scaleX: newBounds.size.width / oldBounds.width, 193 | y: newBounds.size.height / oldBounds.height), 194 | .init(translationX: newBounds.origin.x, y: newBounds.origin.y) 195 | ] 196 | return lastObjects.map { object in 197 | guard object.isSelected else { return object } 198 | var copyObject = object.copy() 199 | copyObject.points = copyObject.points.map { point in 200 | transforms.reduce(point) { partialResult, transform in 201 | partialResult.applying(transform) 202 | } 203 | } 204 | return copyObject 205 | } 206 | } 207 | 208 | func dragBegan( 209 | location: CGPoint, 210 | selectBeganHandler: @escaping () -> Void 211 | ) { 212 | switch objectType { 213 | case .select: 214 | pushHistory() 215 | if let anchor = hitAnchor(location) { 216 | currentSelectType = .keep 217 | // もしも選択中の図形が大きさを持たなかった場合はresizeではなくmoveにする 218 | if selectedObjects.count == 1, 219 | let index = objects.firstIndex(where: { $0.isSelected }), 220 | objects[index].bounds.isEmpty { 221 | currentAction = .move 222 | } else { 223 | currentAction = .resize 224 | currentAnchor = anchor 225 | } 226 | } else if selectedObjects.contains(where: { $0.isHit(point: location) }) { 227 | currentSelectType = .keep 228 | currentAction = .move 229 | } else if let index = objects.lastIndex(where: { $0.isHit(point: location) }) { 230 | currentSelectType = .selectOne(index) 231 | currentAction = .move 232 | var objects_ = unselectedObjects() 233 | objects_[index].isSelected = true 234 | objectsSubject.send(objects_) 235 | } else { 236 | currentSelectType = .rectangle 237 | currentAction = .none 238 | objectsSubject.send(unselectedObjects()) 239 | selectBeganHandler() 240 | } 241 | lastObjects = objects 242 | case .text: 243 | if let inputTextProperties = inputTextPropertiesSubject.value { 244 | endEditing(inputTextProperties) 245 | } 246 | if let index = objects.lastIndex(where: { object in 247 | object.type == .text && object.isHit(point: location) 248 | }) { 249 | let properties = InputTextProperties( 250 | object: objects[index], 251 | inputText: objects[index].text, 252 | fontSize: objects[index].fontSize 253 | ) 254 | inputTextPropertiesSubject.send(properties) 255 | objectsSubject.value[index].isHidden = true 256 | } else { 257 | let properties = InputTextProperties( 258 | object: Object(color, opacity, [location], "", .up), 259 | inputText: "", 260 | fontSize: 40.0 261 | ) 262 | inputTextPropertiesSubject.send(properties) 263 | } 264 | case .pen: 265 | pushHistory() 266 | objectsSubject.value 267 | .append(Object(.pen, color, opacity, lineWidth, [location])) 268 | default: 269 | pushHistory() 270 | objectsSubject.value 271 | .append(Object(objectType, color, opacity, lineWidth, [location, location])) 272 | } 273 | } 274 | 275 | func dragMoved( 276 | startLocation: CGPoint, 277 | location: CGPoint, 278 | selectMovedHandler: @escaping () -> Void 279 | ) { 280 | switch objectType { 281 | case .select: 282 | selectMovedHandler() 283 | let diff: CGPoint = location - startLocation 284 | switch currentAction { 285 | case .none: 286 | break 287 | case .move: 288 | objectsSubject.send(move(diff)) 289 | case .resize: 290 | objectsSubject.send(resize(diff)) 291 | } 292 | case .text: 293 | break 294 | case .pen: 295 | let count = objects.count 296 | if 0 < count { 297 | objectsSubject.value[count - 1].points.append(location) 298 | } 299 | default: 300 | let count = objects.count 301 | if 0 < count { 302 | objectsSubject.value[count - 1].points[1] = location 303 | } 304 | } 305 | } 306 | 307 | func dragEnded( 308 | startLocation: CGPoint, 309 | location: CGPoint, 310 | selectionBounds: CGRect?, 311 | selectEndedHandler: @escaping () -> Void 312 | ) { 313 | switch objectType { 314 | case .select: 315 | if startLocation == location { 316 | // マウスを動かさなかった時はundo() 317 | undo() 318 | } else if case .rectangle = currentSelectType { 319 | // マウスは動かしたけれど、選択状態以外に変化が起きない時もundo() 320 | undo() 321 | } 322 | // 選択状態の更新 323 | if case .selectOne(let index) = currentSelectType { 324 | objectsSubject.value[index].isSelected = true 325 | } else if case .rectangle = currentSelectType { 326 | if let selectionBounds { 327 | var objects_ = objects 328 | for i in objects_.indices { 329 | objects_[i].isSelected = objects_[i].isHit(rect: selectionBounds) 330 | } 331 | objectsSubject.send(objects_) 332 | } 333 | } 334 | currentAction = .none 335 | selectEndedHandler() 336 | lastObjects.removeAll() 337 | case .text: 338 | break 339 | case .pen: 340 | break 341 | default: 342 | if startLocation.length(from: location) < 5 { 343 | undo() 344 | } 345 | } 346 | } 347 | 348 | func endEditing(_ inputTextProperties: InputTextProperties) { 349 | let position = inputTextProperties.object.points[0] 350 | let inputText = inputTextProperties.inputText 351 | let fontSize = inputTextProperties.fontSize 352 | let size = inputText.calculateSize(using: NSFont.systemFont(ofSize: fontSize)) 353 | if let index = objects.firstIndex(where: { object in 354 | object.type == .text && object.isHidden 355 | }) { 356 | // 既存テキスト 357 | objectsSubject.value[index].isHidden = false 358 | if inputText.isEmpty { 359 | pushHistory() 360 | objectsSubject.value.remove(at: index) 361 | } else { 362 | var object = objectsSubject.value[index] 363 | if inputText != object.text { 364 | let endPosition = object.textOrientation.endPosition(with: position, size: size) 365 | pushHistory() 366 | object.points = [position, endPosition] 367 | object.text = inputText 368 | objectsSubject.value[index] = object 369 | } 370 | } 371 | } else { 372 | // 新規テキスト 373 | if !inputText.isEmpty { 374 | let endPosition = CGPoint(x: position.x + size.width, 375 | y: position.y + size.height) 376 | pushHistory() 377 | objectsSubject.value 378 | .append(Object(color, opacity, [position, endPosition], inputText, .up)) 379 | } 380 | } 381 | inputTextPropertiesSubject.send(nil) 382 | } 383 | 384 | func resetDefaultSettings() { 385 | objectTypeSubject.send(userDefaultsRepository.defaultObjectType) 386 | 387 | let index = userDefaultsRepository.defaultColorIndex 388 | objectPropertiesSubject.send(.init( 389 | color: colors[index % 8][index / 8], 390 | opacity: userDefaultsRepository.defaultOpacity, 391 | lineWidth: userDefaultsRepository.defaultLineWidth 392 | )) 393 | } 394 | 395 | // MARK: History Operation 396 | private func timeTravel(objects: [Object]) { 397 | let currentObjects = unselectedObjects() 398 | objectsSubject.send(objects) 399 | undoManager.registerUndo(withTarget: self) { target in 400 | target.timeTravel(objects: currentObjects) 401 | } 402 | } 403 | 404 | // 変化が起きる前に叩く 405 | private func pushHistory() { 406 | let objects_ = unselectedObjects() 407 | undoManager.registerUndo(withTarget: self) { target in 408 | target.timeTravel(objects: objects_) 409 | } 410 | } 411 | 412 | func undo() { 413 | if inputTextPropertiesSubject.value == nil, undoManager.canUndo { 414 | undoManager.undo() 415 | } 416 | } 417 | 418 | func redo() { 419 | if inputTextPropertiesSubject.value == nil, undoManager.canRedo { 420 | undoManager.redo() 421 | } 422 | } 423 | 424 | func resetHistory() { 425 | objectTypeSubject.send(.pen) 426 | objectsSubject.send([]) 427 | let index = userDefaultsRepository.defaultColorIndex 428 | let properties = ObjectProperties( 429 | color: colors[index % 8][index / 8], 430 | opacity: userDefaultsRepository.defaultOpacity, 431 | lineWidth: userDefaultsRepository.defaultLineWidth 432 | ) 433 | objectPropertiesSubject.send(properties) 434 | inputTextPropertiesSubject.send(nil) 435 | undoManager.removeAllActions() 436 | } 437 | 438 | // MARK: Operation to Selected Objects 439 | func updateObjectType(_ objectType: ObjectType) { 440 | guard self.objectType != objectType else { return } 441 | if self.objectType == .text, 442 | let inputTextProperties = inputTextPropertiesSubject.value { 443 | endEditing(inputTextProperties) 444 | } 445 | objectTypeSubject.send(objectType) 446 | guard objectType != .select else { return } 447 | isSelectingSubject.send(false) 448 | if !selectedObjects.isEmpty { 449 | objectsSubject.send(unselectedObjects()) 450 | } 451 | } 452 | 453 | func updateColor(_ color: Color) { 454 | objectPropertiesSubject.value.color = color 455 | guard !selectedObjects.isEmpty else { return } 456 | pushHistory() 457 | var objects_ = objects 458 | for i in objects_.indices where objects_[i].isSelected { 459 | objects_[i].color_ = color 460 | } 461 | objectsSubject.send(objects_) 462 | } 463 | 464 | func startUpdatingOpacity() { 465 | guard !selectedObjects.isEmpty else { return } 466 | pushHistory() 467 | } 468 | 469 | func updateOpacity(_ opacity: CGFloat) { 470 | objectPropertiesSubject.value.opacity = opacity 471 | guard !selectedObjects.isEmpty else { return } 472 | var objects_ = objects 473 | for i in objects_.indices where objects_[i].isSelected { 474 | objects_[i].opacity = opacity 475 | } 476 | objectsSubject.send(objects_) 477 | } 478 | 479 | func startUpdatingLineWidth() { 480 | guard !selectedObjects.isEmpty else { return } 481 | pushHistory() 482 | } 483 | 484 | func updateLineWidth(_ lineWidth: CGFloat) { 485 | objectPropertiesSubject.value.lineWidth = lineWidth 486 | guard !selectedObjects.isEmpty else { return } 487 | var objects_ = objects 488 | for i in objects_.indices where objects_[i].isSelected { 489 | objects_[i].lineWidth = lineWidth 490 | } 491 | objectsSubject.send(objects_) 492 | } 493 | 494 | func arrange(_ arrangeMethod: ArrangeMethod) { 495 | guard isSelecting else { return } 496 | pushHistory() 497 | let selectedObjects_ = selectedObjects 498 | var objects_ = objects 499 | objects_.removeAll { $0.isSelected } 500 | switch arrangeMethod { 501 | case .bringToFrontmost: 502 | objects_.append(contentsOf: selectedObjects_) 503 | case .sendToBackmost: 504 | objects_.insert(contentsOf: selectedObjects_, at: 0) 505 | } 506 | objectsSubject.send(objects_) 507 | } 508 | 509 | func align(_ alignMethod: AlignMethod) { 510 | guard isSelecting, let bounds = selectedObjectsBounds else { return } 511 | pushHistory() 512 | var objects_ = objects 513 | for i in objects_.indices where objects_[i].isSelected { 514 | let diff: CGFloat 515 | switch alignMethod { 516 | case .horizontalAlignLeft: 517 | diff = bounds.minX - objects_[i].bounds.minX 518 | case .horizontalAlignCenter: 519 | diff = bounds.midX - objects_[i].bounds.midX 520 | case .horizontalAlignRight: 521 | diff = bounds.maxX - objects_[i].bounds.maxX 522 | case .verticalAlignTop: 523 | diff = bounds.minY - objects_[i].bounds.minY 524 | case .verticalAlignCenter: 525 | diff = bounds.midY - objects_[i].bounds.midY 526 | case .verticalAlignBottom: 527 | diff = bounds.maxY - objects_[i].bounds.maxY 528 | } 529 | for j in objects_[i].points.indices { 530 | if AlignMethod.horizontals.contains(alignMethod) { 531 | objects_[i].points[j].x += diff 532 | } else { 533 | objects_[i].points[j].y += diff 534 | } 535 | } 536 | } 537 | objectsSubject.send(objects_) 538 | } 539 | 540 | func flip(_ flipMethod: FlipMethod) { 541 | guard isSelecting, let bounds = selectedObjectsBounds else { return } 542 | pushHistory() 543 | var objects_ = objects 544 | for i in objects_.indices where objects_[i].isSelected { 545 | let center = CGPoint(x: bounds.midX, y: bounds.midY) 546 | objects_[i].points = objects_[i].points.map { point in 547 | switch flipMethod { 548 | case .flipHorizontal: 549 | CGPoint(x: 2.0 * center.x - point.x, y: point.y) 550 | case .flipVertical: 551 | CGPoint(x: point.x, y: 2.0 * center.y - point.y) 552 | } 553 | } 554 | if objects_[i].type == .text { 555 | objects_[i].textOrientation = objects_[i].textOrientation.flip(flipMethod) 556 | } 557 | } 558 | objectsSubject.send(objects_) 559 | } 560 | 561 | func rotate(_ rotateMethod: RotateMethod) { 562 | guard isSelecting, let bounds = selectedObjectsBounds else { return } 563 | pushHistory() 564 | let offset = CGPoint(x: bounds.origin.x + 0.5 * bounds.width, 565 | y: bounds.origin.y + 0.5 * bounds.height) 566 | let transforms: [CGAffineTransform] = [ 567 | .init(translationX: -offset.x, y: -offset.y), 568 | .init(rotationAngle: rotateMethod.angle), 569 | .init(translationX: offset.x, y: offset.y) 570 | ] 571 | var objects_ = objects 572 | for i in objects_.indices where objects_[i].isSelected { 573 | objects_[i].points = objects_[i].points.map { point in 574 | transforms.reduce(point) { partialResult, transform in 575 | partialResult.applying(transform) 576 | } 577 | } 578 | if objects_[i].type == .text { 579 | objects_[i].textOrientation = objects_[i].textOrientation.rotate(rotateMethod) 580 | } 581 | } 582 | objectsSubject.send(objects_) 583 | } 584 | 585 | func duplicateSelectedObjects() { 586 | guard isSelecting else { return } 587 | pushHistory() 588 | let copyObjects = selectedObjects.map { $0.copy(needsOffset: true) } 589 | var objects_ = unselectedObjects() 590 | objects_.append(contentsOf: copyObjects) 591 | objectsSubject.send(objects_) 592 | } 593 | 594 | func delete() { 595 | guard isSelecting else { return } 596 | pushHistory() 597 | objectsSubject.value.removeAll { $0.isSelected } 598 | } 599 | 600 | func selectAll() { 601 | guard objectType == .select else { return } 602 | var objects_ = objects 603 | objects_.indices.forEach { i in 604 | objects_[i].isSelected = true 605 | } 606 | objectsSubject.send(objects_) 607 | } 608 | 609 | func clear() { 610 | guard inputTextPropertiesSubject.value == nil, !objects.isEmpty else { 611 | return 612 | } 613 | pushHistory() 614 | objectsSubject.send([]) 615 | } 616 | } 617 | 618 | // MARK: - Preview Mock 619 | extension PreviewMock { 620 | final class ObjectModelMock: ObjectModel { 621 | let colors: [[Color]] 622 | var objectTypePublisher: AnyPublisher { 623 | Just(ObjectType.pen).eraseToAnyPublisher() 624 | } 625 | let objectType: ObjectType = .pen 626 | var objectsPublisher: AnyPublisher<[Object], Never> { 627 | Just([]).eraseToAnyPublisher() 628 | } 629 | var selectedObjectsBoundsPublisher: AnyPublisher { 630 | Just(nil).eraseToAnyPublisher() 631 | } 632 | var isSelectingPublisher: AnyPublisher { 633 | Just(false).eraseToAnyPublisher() 634 | } 635 | var objectPropertiesPublisher: AnyPublisher { 636 | Just(ObjectProperties(color: color, opacity: opacity, lineWidth: lineWidth)).eraseToAnyPublisher() 637 | } 638 | let color: Color 639 | let opacity: CGFloat = 0.8 640 | let lineWidth: CGFloat = 4.0 641 | var inputTextPropertiesPublisher: AnyPublisher { 642 | Just(nil).eraseToAnyPublisher() 643 | } 644 | 645 | init(_ userDefaultsRepository: UserDefaultsRepository) { 646 | colors = Color.palette 647 | color = colors[0][0] 648 | } 649 | init() { 650 | colors = Color.palette 651 | color = colors[0][0] 652 | } 653 | 654 | func endEditing(_ inputTextProperties: InputTextProperties) {} 655 | func dragBegan(location: CGPoint, 656 | selectBeganHandler: @escaping () -> Void) {} 657 | func dragMoved(startLocation: CGPoint, 658 | location: CGPoint, 659 | selectMovedHandler: @escaping () -> Void) {} 660 | func dragEnded(startLocation: CGPoint, 661 | location: CGPoint, 662 | selectionBounds: CGRect?, 663 | selectEndedHandler: @escaping () -> Void) {} 664 | 665 | func resetDefaultSettings() {} 666 | func undo() {} 667 | func redo() {} 668 | func resetHistory() {} 669 | func updateObjectType(_ objectType: ObjectType) {} 670 | func updateColor(_ color: Color) {} 671 | func startUpdatingOpacity() {} 672 | func updateOpacity(_ opacity: CGFloat) {} 673 | func startUpdatingLineWidth() {} 674 | func updateLineWidth(_ lineWidth: CGFloat) {} 675 | func arrange(_ arrangeMethod: ArrangeMethod) {} 676 | func align(_ alignMethod: AlignMethod) {} 677 | func flip(_ flipMethod: FlipMethod) {} 678 | func rotate(_ rotateMethod: RotateMethod) {} 679 | func duplicateSelectedObjects() {} 680 | func delete() {} 681 | func selectAll() {} 682 | func clear() {} 683 | } 684 | } 685 | -------------------------------------------------------------------------------- /ScreenNote/Domain/Model/ShortcutModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ShortcutModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/31. 6 | 7 | */ 8 | 9 | import Combine 10 | import Foundation 11 | import SpiceKey 12 | 13 | protocol ShortcutModel: AnyObject { 14 | var showOrHideCanvasPublisher: AnyPublisher { get } 15 | 16 | init(_ userDefaultsRepository: UserDefaultsRepository) 17 | 18 | func setShortcut() 19 | } 20 | 21 | final class ShortcutModelImpl: ShortcutModel { 22 | private let showOrHideCanvasSubject = PassthroughSubject() 23 | var showOrHideCanvasPublisher: AnyPublisher { 24 | showOrHideCanvasSubject.eraseToAnyPublisher() 25 | } 26 | 27 | private let userDefaultsRepository: UserDefaultsRepository 28 | private var spiceKey: SpiceKey? 29 | private var cancellables = Set() 30 | 31 | init(_ userDefaultsRepository: UserDefaultsRepository) { 32 | self.userDefaultsRepository = userDefaultsRepository 33 | self.userDefaultsRepository.updateShortcutPublisher 34 | .sink { [weak self] in 35 | self?.setShortcut() 36 | } 37 | .store(in: &cancellables) 38 | } 39 | 40 | func setShortcut() { 41 | spiceKey?.unregister() 42 | let toggleMethod = userDefaultsRepository.toggleMethod 43 | let modifierFlag = userDefaultsRepository.modifierFlag 44 | let longPressSeconds = userDefaultsRepository.longPressSeconds 45 | switch toggleMethod { 46 | case .longPressKey: 47 | spiceKey = SpiceKey(modifierFlag.flags, longPressSeconds, modifierKeysLongPressHandler: { [weak self] in 48 | self?.showOrHideCanvasSubject.send() 49 | }) 50 | spiceKey?.register() 51 | case .pressBothSideKeys: 52 | spiceKey = SpiceKey(modifierFlag, bothModifierKeysPressHandler: { [weak self] in 53 | self?.showOrHideCanvasSubject.send() 54 | }) 55 | spiceKey?.register() 56 | } 57 | } 58 | } 59 | 60 | // MARK: - Preview Mock 61 | extension PreviewMock { 62 | final class ShortcutModelMock: ShortcutModel { 63 | var showOrHideCanvasPublisher: AnyPublisher { 64 | Just(()).eraseToAnyPublisher() 65 | } 66 | 67 | init(_ userDefaultsRepository: UserDefaultsRepository) {} 68 | init() {} 69 | 70 | func setShortcut() {} 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ScreenNoteAppModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ScreenNoteAppModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import AppKit 10 | import Combine 11 | import SpiceKey 12 | 13 | protocol ScreenNoteAppModel: ObservableObject { 14 | associatedtype UR: UserDefaultsRepository 15 | associatedtype SM: ShortcutModel 16 | associatedtype OM: ObjectModel 17 | associatedtype WM: WindowModel 18 | associatedtype MVM: MenuViewModel 19 | associatedtype GVM: GeneralSettingsViewModel 20 | associatedtype CsVM: CanvasSettingsViewModel 21 | 22 | var settingsTab: SettingsTabType { get set } 23 | var userDefaultsRepository: UR { get } 24 | var shortcutModel: SM { get } 25 | var windowModel: WM { get } 26 | } 27 | 28 | final class ScreenNoteAppModelImpl: NSObject, ScreenNoteAppModel { 29 | typealias UR = UserDefaultsRepositoryImpl 30 | typealias SM = ShortcutModelImpl 31 | typealias OM = ObjectModelImpl 32 | typealias WM = WindowModelImpl 33 | typealias MVM = MenuViewModelImpl 34 | typealias GVM = GeneralSettingsViewModelImpl 35 | typealias CsVM = CanvasSettingsViewModelImpl 36 | 37 | @Published var settingsTab: SettingsTabType = .general 38 | 39 | let userDefaultsRepository: UR 40 | let shortcutModel: SM 41 | private let objectModel: OM 42 | let windowModel: WM 43 | private var cancellables = Set() 44 | 45 | override init() { 46 | userDefaultsRepository = UR() 47 | shortcutModel = SM(userDefaultsRepository) 48 | objectModel = OM(userDefaultsRepository) 49 | windowModel = WM(userDefaultsRepository, shortcutModel, objectModel) 50 | super.init() 51 | 52 | NotificationCenter.default 53 | .publisher(for: NSApplication.didFinishLaunchingNotification) 54 | .sink { [weak self] _ in 55 | self?.applicationDidFinishLaunching() 56 | } 57 | .store(in: &cancellables) 58 | NotificationCenter.default 59 | .publisher(for: NSApplication.willTerminateNotification) 60 | .sink { [weak self] _ in 61 | self?.applicationWillTerminate() 62 | } 63 | .store(in: &cancellables) 64 | } 65 | 66 | private func applicationDidFinishLaunching() { 67 | shortcutModel.setShortcut() 68 | 69 | if userDefaultsRepository.showToggleMethod { 70 | let toggleMethod = userDefaultsRepository.toggleMethod 71 | let modifierFlag = userDefaultsRepository.modifierFlag 72 | windowModel.fadeInShortcutPanel(toggleMethod, modifierFlag) 73 | } 74 | } 75 | 76 | private func applicationWillTerminate() { 77 | windowModel.fadeOutShortcutPanel() 78 | } 79 | } 80 | 81 | // MARK: - Preview Mock 82 | extension PreviewMock { 83 | final class ScreenNoteAppModelMock: ScreenNoteAppModel { 84 | typealias UR = UserDefaultsRepositoryMock 85 | typealias SM = ShortcutModelMock 86 | typealias OM = ObjectModelMock 87 | typealias WM = WindowModelMock 88 | typealias MVM = MenuViewModelMock 89 | typealias GVM = GeneralSettingsViewModelMock 90 | typealias CsVM = CanvasSettingsViewModelMock 91 | 92 | @Published var settingsTab: SettingsTabType = .general 93 | var userDefaultsRepository = UR() 94 | var shortcutModel = SM() 95 | var windowModel = WM() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/CanvasSettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasSettingsViewModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | protocol CanvasSettingsViewModel: ObservableObject { 12 | var clearAllObjects: Bool { get set } 13 | var showColorPopover: Bool { get set } 14 | var defaultObjectType: ObjectType { get set } 15 | var defaultColorIndex: Int { get set } 16 | var defaultOpacity: CGFloat { get set } 17 | var defaultLineWidth: CGFloat { get set } 18 | var backgroundColorIndex: Int { get set } 19 | var backgroundOpacity: CGFloat { get set } 20 | var colors: [[Color]] { get } 21 | var defaultColor: Color { get } 22 | var backgrounds: [Color] { get } 23 | 24 | init(_ userDefaultsRepository: UserDefaultsRepository) 25 | 26 | func updateDefaultColor(_ index: Int) 27 | func updateBackgroundColor(_ index: Int) 28 | func endUpdatingDefaultOpacity() 29 | func endUpdatingDefaultLineWidth() 30 | func endUpdatingBackgroundOpacity() 31 | } 32 | 33 | final class CanvasSettingsViewModelImpl: CanvasSettingsViewModel { 34 | @Published var clearAllObjects: Bool { 35 | didSet { userDefaultsRepository.clearAllObjects = clearAllObjects } 36 | } 37 | @Published var showColorPopover: Bool = false 38 | @Published var defaultObjectType: ObjectType { 39 | didSet { userDefaultsRepository.defaultObjectType = defaultObjectType } 40 | } 41 | @Published var defaultColorIndex: Int 42 | @Published var defaultOpacity: CGFloat 43 | @Published var defaultLineWidth: CGFloat 44 | @Published var backgroundColorIndex: Int 45 | @Published var backgroundOpacity: CGFloat 46 | let colors: [[Color]] 47 | let backgrounds: [Color] = [.white, .black] 48 | private let userDefaultsRepository: UserDefaultsRepository 49 | 50 | var defaultColor: Color { 51 | colors[defaultColorIndex % 8][defaultColorIndex / 8] 52 | } 53 | 54 | init(_ userDefaultsRepository: UserDefaultsRepository) { 55 | self.userDefaultsRepository = userDefaultsRepository 56 | clearAllObjects = userDefaultsRepository.clearAllObjects 57 | colors = Color.palette 58 | defaultObjectType = userDefaultsRepository.defaultObjectType 59 | defaultColorIndex = userDefaultsRepository.defaultColorIndex 60 | defaultOpacity = userDefaultsRepository.defaultOpacity 61 | defaultLineWidth = userDefaultsRepository.defaultLineWidth 62 | backgroundColorIndex = userDefaultsRepository.backgroundColorIndex 63 | backgroundOpacity = userDefaultsRepository.backgroundOpacity 64 | } 65 | 66 | func updateDefaultColor(_ index: Int) { 67 | defaultColorIndex = index 68 | userDefaultsRepository.defaultColorIndex = index 69 | } 70 | 71 | func updateBackgroundColor(_ index: Int) { 72 | backgroundColorIndex = index 73 | userDefaultsRepository.backgroundColorIndex = index 74 | } 75 | 76 | func endUpdatingDefaultOpacity() { 77 | userDefaultsRepository.defaultOpacity = defaultOpacity 78 | } 79 | 80 | func endUpdatingDefaultLineWidth() { 81 | userDefaultsRepository.defaultLineWidth = defaultLineWidth 82 | } 83 | 84 | func endUpdatingBackgroundOpacity() { 85 | userDefaultsRepository.backgroundOpacity = backgroundOpacity 86 | } 87 | } 88 | 89 | // MARK: - Preview Mock 90 | extension PreviewMock { 91 | final class CanvasSettingsViewModelMock: CanvasSettingsViewModel { 92 | @Published var clearAllObjects: Bool = false 93 | @Published var defaultObjectType: ObjectType = .pen 94 | @Published var defaultColorIndex: Int = 0 95 | @Published var showColorPopover: Bool = false 96 | @Published var defaultColor: Color = .clear 97 | @Published var defaultOpacity: CGFloat = 0.8 98 | @Published var defaultLineWidth: CGFloat = 4.0 99 | @Published var backgroundColorIndex: Int = 0 100 | @Published var backgroundOpacity: CGFloat = 0.0 101 | let colors: [[Color]] 102 | let backgrounds: [Color] = [.white, .black] 103 | 104 | init(_ userDefaultsRepository: UserDefaultsRepository) { 105 | colors = [] 106 | } 107 | init() { 108 | colors = Color.palette 109 | defaultColor = colors[0][0] 110 | } 111 | 112 | func updateDefaultColor(_ index: Int) {} 113 | func updateBackgroundColor(_ index: Int) {} 114 | func endUpdatingDefaultOpacity() {} 115 | func endUpdatingDefaultLineWidth() {} 116 | func endUpdatingBackgroundOpacity() {} 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/CanvasViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasViewModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/16. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | protocol CanvasViewModel: ObservableObject { 13 | var selectedObjectsBounds: CGRect? { get set } 14 | var objects: [Object] { get set } 15 | var inputTextProperties: InputTextProperties? { get set } 16 | var inputText: String { get set } 17 | var textColor: Color { get set } 18 | var dragging: Bool { get set } 19 | var rectangleForSelection: Object? { get set } 20 | 21 | init(objectModel: ObjectModel) 22 | 23 | func dragBegan(location: CGPoint) 24 | func dragMoved(startLocation: CGPoint, location: CGPoint) 25 | func dragEnded(startLocation: CGPoint, location: CGPoint) 26 | func endEditing(inputTextObject: Object, fontSize: CGFloat) 27 | } 28 | 29 | final class CanvasViewModelImpl: CanvasViewModel { 30 | private let objectModel: ObjectModel 31 | private var lineWidth: CGFloat = 0.8 32 | private var cancellables = Set() 33 | 34 | @Published var objects = [Object]() 35 | @Published var inputTextProperties: InputTextProperties? 36 | @Published var inputText: String = "" 37 | @Published var textColor: Color 38 | @Published var dragging: Bool = false 39 | @Published var selectedObjectsBounds: CGRect? 40 | @Published var rectangleForSelection: Object? 41 | 42 | init(objectModel: ObjectModel) { 43 | self.objectModel = objectModel 44 | textColor = objectModel.color.opacity(objectModel.opacity) 45 | 46 | objectModel.objectsPublisher 47 | .sink { [weak self] objects in 48 | self?.objects = objects 49 | } 50 | .store(in: &cancellables) 51 | 52 | objectModel.selectedObjectsBoundsPublisher 53 | .sink { [weak self] bounds in 54 | self?.selectedObjectsBounds = bounds 55 | } 56 | .store(in: &cancellables) 57 | 58 | objectModel.objectPropertiesPublisher 59 | .sink { [weak self] properties in 60 | self?.textColor = properties.color.opacity(properties.opacity) 61 | self?.lineWidth = properties.lineWidth 62 | } 63 | .store(in: &cancellables) 64 | 65 | objectModel.inputTextPropertiesPublisher 66 | .sink { [weak self] properties in 67 | self?.inputTextProperties = properties 68 | } 69 | .store(in: &cancellables) 70 | } 71 | 72 | func dragBegan(location: CGPoint) { 73 | objectModel.dragBegan(location: location) { [weak self] in 74 | guard let self else { return } 75 | rectangleForSelection = Object(.select, .black, 1.0, lineWidth, [location, location]) 76 | } 77 | } 78 | 79 | func dragMoved(startLocation: CGPoint, location: CGPoint) { 80 | objectModel.dragMoved( 81 | startLocation: startLocation, 82 | location: location 83 | ) { [weak self] in 84 | self?.rectangleForSelection?.points[1] = location 85 | } 86 | } 87 | 88 | func dragEnded(startLocation: CGPoint, location: CGPoint) { 89 | objectModel.dragEnded( 90 | startLocation: startLocation, 91 | location: location, 92 | selectionBounds: rectangleForSelection?.bounds 93 | ) { [weak self] in 94 | self?.rectangleForSelection = nil 95 | } 96 | } 97 | 98 | func endEditing(inputTextObject: Object, fontSize: CGFloat) { 99 | objectModel.endEditing(InputTextProperties( 100 | object: inputTextObject, 101 | inputText: inputText, 102 | fontSize: fontSize 103 | )) 104 | } 105 | } 106 | 107 | // MARK: - Preview Mock 108 | extension PreviewMock { 109 | final class CanvasViewModelMock: CanvasViewModel { 110 | @Published var selectedObjectsBounds: CGRect? 111 | 112 | @Published var objects = [Object]() 113 | @Published var inputTextProperties: InputTextProperties? 114 | @Published var inputText: String = "" 115 | @Published var textColor: Color = .white 116 | @Published var dragging: Bool = false 117 | @Published var rectangleForSelection: Object? 118 | 119 | init(objectModel: ObjectModel) {} 120 | init() {} 121 | 122 | func dragBegan(location: CGPoint) {} 123 | func dragMoved(startLocation: CGPoint, location: CGPoint) {} 124 | func dragEnded(startLocation: CGPoint, location: CGPoint) {} 125 | func endEditing(inputTextObject: Object, fontSize: CGFloat) {} 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/GeneralSettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | GeneralSettingsViewModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Foundation 10 | import SpiceKey 11 | 12 | protocol GeneralSettingsViewModel: ObservableObject { 13 | var toggleMethod: ToggleMethod { get set } 14 | var modifierFlag: ModifierFlag { get set } 15 | var longPressSeconds: Double { get set } 16 | var toolBarPosition: ToolBarPosition { get set } 17 | var showToggleMethod: Bool { get set } 18 | var launchAtLogin: Bool { get set } 19 | 20 | init(_ userDefaultsRepository: UserDefaultsRepository) 21 | 22 | func endUpdatingLongPressSeconds() 23 | } 24 | 25 | final class GeneralSettingsViewModelImpl: GeneralSettingsViewModel { 26 | @Published var toggleMethod: ToggleMethod { 27 | didSet { userDefaultsRepository.toggleMethod = toggleMethod } 28 | } 29 | @Published var modifierFlag: ModifierFlag { 30 | didSet { userDefaultsRepository.modifierFlag = modifierFlag } 31 | } 32 | @Published var longPressSeconds: Double 33 | @Published var toolBarPosition: ToolBarPosition { 34 | didSet { userDefaultsRepository.toolBarPosition = toolBarPosition } 35 | } 36 | @Published var showToggleMethod: Bool { 37 | didSet { userDefaultsRepository.showToggleMethod = showToggleMethod } 38 | } 39 | @Published var launchAtLogin: Bool { 40 | didSet { 41 | launchAtLoginRepository.switchRegistration(launchAtLogin) { [weak self] in 42 | self?.launchAtLogin = oldValue 43 | } 44 | } 45 | } 46 | private let userDefaultsRepository: UserDefaultsRepository 47 | private let launchAtLoginRepository: LR 48 | 49 | init(_ userDefaultsRepository: UserDefaultsRepository) { 50 | self.userDefaultsRepository = userDefaultsRepository 51 | self.launchAtLoginRepository = LR() 52 | toggleMethod = userDefaultsRepository.toggleMethod 53 | modifierFlag = userDefaultsRepository.modifierFlag 54 | longPressSeconds = userDefaultsRepository.longPressSeconds 55 | toolBarPosition = userDefaultsRepository.toolBarPosition 56 | showToggleMethod = userDefaultsRepository.showToggleMethod 57 | launchAtLogin = launchAtLoginRepository.current 58 | } 59 | 60 | func endUpdatingLongPressSeconds() { 61 | userDefaultsRepository.longPressSeconds = longPressSeconds 62 | } 63 | } 64 | 65 | // MARK: - Preview Mock 66 | extension PreviewMock { 67 | final class GeneralSettingsViewModelMock: GeneralSettingsViewModel { 68 | @Published var toggleMethod: ToggleMethod = .longPressKey 69 | @Published var modifierFlag: ModifierFlag = .control 70 | @Published var longPressSeconds: Double = 0.5 71 | @Published var toolBarPosition: ToolBarPosition = .top 72 | @Published var showToggleMethod: Bool = true 73 | @Published var launchAtLogin: Bool = false 74 | 75 | init(_ userDefaultsRepository: UserDefaultsRepository) {} 76 | init() {} 77 | 78 | func endUpdatingLongPressSeconds() {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/MenuViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | MenuViewModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/18. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import AppKit 10 | import SwiftUI 11 | import Combine 12 | 13 | protocol MenuViewModel: ObservableObject { 14 | var canvasVisible: CanvasVisible { get set } 15 | 16 | init(_ windowModel: WindowModel) 17 | 18 | func showOrHide() 19 | func activateApp() 20 | func openSettings() 21 | func openAbout() 22 | func sendIssueReport() 23 | func terminateApp() 24 | } 25 | 26 | final class MenuViewModelImpl: MenuViewModel { 27 | private let windowModel: WindowModel 28 | private var cancellables = Set() 29 | 30 | @Published var canvasVisible: CanvasVisible = .hide 31 | 32 | init(_ windowModel: WindowModel) { 33 | self.windowModel = windowModel 34 | windowModel.canvasVisiblePublisher 35 | .sink { [weak self] canvasVisible in 36 | self?.canvasVisible = canvasVisible 37 | } 38 | .store(in: &cancellables) 39 | } 40 | 41 | func showOrHide() { 42 | if canvasVisible == .hide { 43 | windowModel.showCanvas() 44 | } else { 45 | windowModel.hideCanvas() 46 | } 47 | } 48 | 49 | func activateApp() { 50 | NSApp.activate(ignoringOtherApps: true) 51 | } 52 | 53 | func openSettings() { 54 | windowModel.openSettings() 55 | } 56 | 57 | func openAbout() { 58 | windowModel.openAbout() 59 | } 60 | 61 | func sendIssueReport() { 62 | IR.send() 63 | } 64 | 65 | func terminateApp() { 66 | NSApp.terminate(nil) 67 | } 68 | } 69 | 70 | // MARK: - Preview Mock 71 | extension PreviewMock { 72 | final class MenuViewModelMock: MenuViewModel { 73 | @Published var canvasVisible: CanvasVisible = .hide 74 | 75 | init(_ windowModel: WindowModel) {} 76 | init() {} 77 | 78 | func showOrHide() {} 79 | func activateApp() {} 80 | func openSettings() {} 81 | func openAbout() {} 82 | func sendIssueReport() {} 83 | func terminateApp() {} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/ToolBarModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToolBarModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/15. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | protocol ToolBarModel: ObservableObject { 13 | var objectType: ObjectType { get set } 14 | var color: Color { get set } 15 | var opacity: CGFloat { get set } 16 | var lineWidth: CGFloat { get set } 17 | var disabledWhileInputingText: Bool { get set } 18 | var disabledSelectAll: Bool { get set } 19 | var disabledEditObject: Bool { get set } 20 | var showColorPopover: Bool { get set } 21 | var showLineWidthPopover: Bool { get set } 22 | var showArrangePopover: Bool { get set } 23 | var showAlignPopover: Bool { get set } 24 | var showFlipPopover: Bool { get set } 25 | var showRotatePopover: Bool { get set } 26 | var arrowEdge: Edge { get } 27 | var colors: [[Color]] { get } 28 | 29 | init(objectModel: ObjectModel, arrowEdge: Edge) 30 | 31 | func undo() 32 | func redo() 33 | func startUpdatingOpacity() 34 | func startUpdatingLineWidth() 35 | func arrange(_ arrangeMethod: ArrangeMethod) 36 | func align(_ alignMethod: AlignMethod) 37 | func flip(_ flipMethod: FlipMethod) 38 | func rotate(_ rotateMethod: RotateMethod) 39 | func duplicateSelectedObjects() 40 | func delete() 41 | func clear() 42 | func selectAll() 43 | func updateObjectType(_ objectType: ObjectType) 44 | func updateColor(_ color: Color) 45 | func updateOpacity(_ opacity: CGFloat) 46 | func updateLineWidth(_ lineWidth: CGFloat) 47 | } 48 | 49 | final class ToolBarModelImpl: ToolBarModel { 50 | private let objectModel: ObjectModel 51 | private var cancellables = Set() 52 | 53 | @Published var objectType: ObjectType 54 | @Published var color: Color 55 | @Published var opacity: CGFloat 56 | @Published var lineWidth: CGFloat 57 | @Published var disabledWhileInputingText: Bool = false 58 | @Published var disabledSelectAll: Bool = false 59 | @Published var disabledEditObject: Bool = false 60 | @Published var showColorPopover: Bool = false 61 | @Published var showLineWidthPopover: Bool = false 62 | @Published var showArrangePopover: Bool = false 63 | @Published var showAlignPopover: Bool = false 64 | @Published var showFlipPopover: Bool = false 65 | @Published var showRotatePopover: Bool = false 66 | let arrowEdge: Edge 67 | let colors: [[Color]] 68 | 69 | init(objectModel: ObjectModel, arrowEdge: Edge) { 70 | self.objectModel = objectModel 71 | self.arrowEdge = arrowEdge 72 | objectType = objectModel.objectType 73 | color = objectModel.color 74 | opacity = objectModel.opacity 75 | lineWidth = objectModel.lineWidth 76 | colors = objectModel.colors 77 | 78 | objectModel.objectTypePublisher 79 | .sink { [weak self] objectType in 80 | self?.objectType = objectType 81 | } 82 | .store(in: &cancellables) 83 | 84 | objectModel.isSelectingPublisher 85 | .sink { [weak self] isSelecting in 86 | self?.disabledEditObject = !isSelecting 87 | } 88 | .store(in: &cancellables) 89 | 90 | objectModel.objectPropertiesPublisher 91 | .sink { [weak self] properties in 92 | self?.color = properties.color 93 | self?.opacity = properties.opacity 94 | self?.lineWidth = properties.lineWidth 95 | } 96 | .store(in: &cancellables) 97 | 98 | objectModel.inputTextPropertiesPublisher 99 | .sink { [weak self] properties in 100 | self?.disabledWhileInputingText = (properties != nil) 101 | } 102 | .store(in: &cancellables) 103 | } 104 | 105 | func undo() { 106 | objectModel.undo() 107 | } 108 | 109 | func redo() { 110 | objectModel.redo() 111 | } 112 | 113 | func startUpdatingOpacity() { 114 | objectModel.startUpdatingOpacity() 115 | } 116 | 117 | func startUpdatingLineWidth() { 118 | objectModel.startUpdatingLineWidth() 119 | } 120 | 121 | func arrange(_ arrangeMethod: ArrangeMethod) { 122 | objectModel.arrange(arrangeMethod) 123 | } 124 | 125 | func align(_ alignMethod: AlignMethod) { 126 | objectModel.align(alignMethod) 127 | } 128 | 129 | func flip(_ flipMethod: FlipMethod) { 130 | objectModel.flip(flipMethod) 131 | } 132 | 133 | func rotate(_ rotateMethod: RotateMethod) { 134 | objectModel.rotate(rotateMethod) 135 | } 136 | 137 | func duplicateSelectedObjects() { 138 | objectModel.duplicateSelectedObjects() 139 | } 140 | 141 | func delete() { 142 | objectModel.delete() 143 | } 144 | 145 | func clear() { 146 | objectModel.clear() 147 | } 148 | 149 | func selectAll() { 150 | objectModel.selectAll() 151 | } 152 | 153 | func updateObjectType(_ objectType: ObjectType) { 154 | objectModel.updateObjectType(objectType) 155 | } 156 | 157 | func updateColor(_ color: Color) { 158 | objectModel.updateColor(color) 159 | } 160 | 161 | func updateOpacity(_ opacity: CGFloat) { 162 | objectModel.updateOpacity(opacity) 163 | } 164 | 165 | func updateLineWidth(_ lineWidth: CGFloat) { 166 | objectModel.updateLineWidth(lineWidth) 167 | } 168 | } 169 | 170 | // MARK: - Preview Mock 171 | extension PreviewMock { 172 | final class ToolBarModelMock: ToolBarModel { 173 | @Published var objectType: ObjectType = .pen 174 | @Published var color: Color = .white 175 | @Published var opacity: CGFloat = 0.8 176 | @Published var lineWidth: CGFloat = 4.0 177 | @Published var disabledWhileInputingText: Bool = false 178 | @Published var disabledSelectAll: Bool = false 179 | @Published var disabledEditObject: Bool = false 180 | @Published var showColorPopover: Bool = false 181 | @Published var showLineWidthPopover: Bool = false 182 | @Published var showArrangePopover: Bool = false 183 | @Published var showAlignPopover: Bool = false 184 | @Published var showFlipPopover: Bool = false 185 | @Published var showRotatePopover: Bool = false 186 | let arrowEdge: Edge = .bottom 187 | let colors: [[Color]] = [] 188 | 189 | init(objectModel: ObjectModel, arrowEdge: Edge) {} 190 | init() {} 191 | 192 | func undo() {} 193 | func redo() {} 194 | func startUpdatingOpacity() {} 195 | func startUpdatingLineWidth() {} 196 | func arrange(_ arrangeMethod: ArrangeMethod) {} 197 | func align(_ alignMethod: AlignMethod) {} 198 | func flip(_ flipMethod: FlipMethod) {} 199 | func rotate(_ rotateMethod: RotateMethod) {} 200 | func duplicateSelectedObjects() {} 201 | func delete() {} 202 | func clear() {} 203 | func selectAll() {} 204 | func updateObjectType(_ objectType: ObjectType) {} 205 | func updateColor(_ color: Color) {} 206 | func updateOpacity(_ opacity: CGFloat) {} 207 | func updateLineWidth(_ lineWidth: CGFloat) {} 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/WindowModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | WindowModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import AppKit 10 | import Combine 11 | import SpiceKey 12 | 13 | protocol WindowModel: AnyObject { 14 | var canvasVisiblePublisher: AnyPublisher { get } 15 | 16 | init(_ userDefaultsRepository: UserDefaultsRepository, 17 | _ shortcutModel: ShortcutModel, 18 | _ objectModel: ObjectModel) 19 | 20 | func openSettings() 21 | func openAbout() 22 | func fadeInShortcutPanel(_ toggleMethod: ToggleMethod, _ modifierFlag: ModifierFlag) 23 | func fadeOutShortcutPanel() 24 | func showCanvas() 25 | func hideCanvas() 26 | } 27 | 28 | final class WindowModelImpl: NSObject, WindowModel, NSWindowDelegate { 29 | private let userDefaultsRepository: UserDefaultsRepository 30 | private let shortcutModel: ShortcutModel 31 | private let objectModel: ObjectModel 32 | private var shortcutPanel: ShortcutPanel? 33 | private var workspacePanel: WorkspacePanel? 34 | private var cancellables = Set() 35 | 36 | private let canvasVisibleSubject = PassthroughSubject() 37 | var canvasVisiblePublisher: AnyPublisher { 38 | canvasVisibleSubject.eraseToAnyPublisher() 39 | } 40 | 41 | private var settingsWindow: NSWindow? { 42 | NSApp.windows.first(where: { window in 43 | window.frameAutosaveName == "com_apple_SwiftUI_Settings_window" 44 | }) 45 | } 46 | 47 | init( 48 | _ userDefaultsRepository: UserDefaultsRepository, 49 | _ shortcutModel: ShortcutModel, 50 | _ objectModel: ObjectModel 51 | ) { 52 | self.userDefaultsRepository = userDefaultsRepository 53 | self.shortcutModel = shortcutModel 54 | self.objectModel = objectModel 55 | super.init() 56 | self.shortcutModel.showOrHideCanvasPublisher 57 | .sink { [weak self] in 58 | self?.showOrHideCanvas() 59 | } 60 | .store(in: &cancellables) 61 | } 62 | 63 | func openSettings() { 64 | NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) 65 | guard let window = settingsWindow else { return } 66 | if window.canBecomeMain { 67 | window.center() 68 | window.makeKeyAndOrderFront(nil) 69 | NSApp.activate(ignoringOtherApps: true) 70 | } 71 | } 72 | 73 | func openAbout() { 74 | NSApp.activate(ignoringOtherApps: true) 75 | NSApp.orderFrontStandardAboutPanel(nil) 76 | } 77 | 78 | func fadeInShortcutPanel(_ toggleMethod: ToggleMethod, _ modifierFlag: ModifierFlag) { 79 | guard shortcutPanel == nil else { return } 80 | shortcutPanel = ShortcutPanel(toggleMethod, modifierFlag) 81 | shortcutPanel?.delegate = self 82 | shortcutPanel?.fadeIn() 83 | } 84 | 85 | func fadeOutShortcutPanel() { 86 | shortcutPanel?.fadeOut() 87 | } 88 | 89 | private func showOrHideCanvas() { 90 | if workspacePanel == nil { 91 | showCanvas() 92 | } else { 93 | hideCanvas() 94 | } 95 | } 96 | 97 | func showCanvas() { 98 | fadeOutShortcutPanel() 99 | assert(workspacePanel == nil, "Cannot show canvas when workspacePanel is not nil.") 100 | workspacePanel = WorkspacePanel(userDefaultsRepository, objectModel) 101 | workspacePanel?.delegate = self 102 | workspacePanel?.fadeIn() 103 | canvasVisibleSubject.send(.show) 104 | NSApp.activate(ignoringOtherApps: true) 105 | } 106 | 107 | func hideCanvas() { 108 | workspacePanel?.fadeOut() 109 | canvasVisibleSubject.send(.hide) 110 | } 111 | 112 | // MARK: NSWindowDelegate 113 | func windowWillClose(_ notification: Notification) { 114 | guard let window = notification.object as? NSWindow else { return } 115 | if window === shortcutPanel { 116 | shortcutPanel = nil 117 | } else if window === workspacePanel { 118 | workspacePanel = nil 119 | } 120 | } 121 | } 122 | 123 | // MARK: - Preview Mock 124 | extension PreviewMock { 125 | final class WindowModelMock: WindowModel { 126 | var canvasVisiblePublisher: AnyPublisher { 127 | Just(.show).eraseToAnyPublisher() 128 | } 129 | 130 | init(_ userDefaultsRepository: UserDefaultsRepository, 131 | _ shortcutModel: ShortcutModel, 132 | _ objectModel: ObjectModel) {} 133 | init() {} 134 | 135 | func openSettings() {} 136 | func openAbout() {} 137 | func fadeInShortcutPanel(_ toggleMethod: ToggleMethod, _ modifierFlag: ModifierFlag) {} 138 | func fadeOutShortcutPanel() {} 139 | func showCanvas() {} 140 | func hideCanvas() {} 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ScreenNote/Domain/ViewModel/WorkspaceViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | WorkspaceViewModel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/01. 6 | 7 | */ 8 | 9 | import Foundation 10 | 11 | protocol WorkspaceViewModel: ObservableObject { 12 | associatedtype TBM: ToolBarModel 13 | associatedtype CVM: CanvasViewModel 14 | 15 | var objectModel: ObjectModel { get } 16 | var toolBarPosition: ToolBarPosition { get } 17 | 18 | init(_ objectModel: ObjectModel, 19 | _ toolBarPosition: ToolBarPosition) 20 | } 21 | 22 | final class WorkspaceViewModelImpl: WorkspaceViewModel { 23 | typealias TBM = ToolBarModelImpl 24 | typealias CVM = CanvasViewModelImpl 25 | 26 | let objectModel: ObjectModel 27 | let toolBarPosition: ToolBarPosition 28 | 29 | init( 30 | _ objectModel: ObjectModel, 31 | _ toolBarPosition: ToolBarPosition 32 | ) { 33 | self.objectModel = objectModel 34 | self.toolBarPosition = toolBarPosition 35 | } 36 | } 37 | 38 | // MARK: - Preview Mock 39 | extension PreviewMock { 40 | final class WorkspaceViewModelMock: WorkspaceViewModel { 41 | typealias TBM = ToolBarModelMock 42 | typealias CVM = CanvasViewModelMock 43 | 44 | let objectModel: ObjectModel = ObjectModelMock() 45 | let toolBarPosition: ToolBarPosition = .top 46 | 47 | init(_ objectModel: ObjectModel, 48 | _ toolBarPosition: ToolBarPosition) {} 49 | init() {} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ScreenNote/Helper/AppKit+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | AppKit+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import AppKit 10 | 11 | extension NSStatusItem { 12 | static var `default`: NSStatusItem { 13 | NSStatusBar.system.statusItem(withLength: Self.variableLength) 14 | } 15 | } 16 | 17 | extension NSMenu { 18 | func addItem(title: String, action: Selector, target: AnyObject) { 19 | self.addItem(NSMenuItem(title: title, action: action, target: target)) 20 | } 21 | 22 | func addSeparator() { 23 | self.addItem(NSMenuItem.separator()) 24 | } 25 | } 26 | 27 | extension NSMenuItem { 28 | convenience init(title: String, action: Selector, target: AnyObject) { 29 | self.init(title: title, action: action, keyEquivalent: "") 30 | self.target = target 31 | } 32 | 33 | func setValues(title: String, action: Selector, target: AnyObject) { 34 | self.title = title 35 | self.action = action 36 | self.target = target 37 | } 38 | } 39 | 40 | extension NSTextField { 41 | open override func performKeyEquivalent(with event: NSEvent) -> Bool { 42 | let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) 43 | if flags == [.command] { 44 | let selector: Selector 45 | switch event.charactersIgnoringModifiers?.lowercased() { 46 | case "x": selector = #selector(NSText.cut(_:)) 47 | case "c": selector = #selector(NSText.copy(_:)) 48 | case "v": selector = #selector(NSText.paste(_:)) 49 | case "a": selector = #selector(NSText.selectAll(_:)) 50 | case "z": selector = Selector(("undo:")) 51 | default: return super.performKeyEquivalent(with: event) 52 | } 53 | return NSApp.sendAction(selector, to: nil, from: self) 54 | } else if flags == [.shift, .command] { 55 | if event.charactersIgnoringModifiers?.lowercased() == "z" { 56 | return NSApp.sendAction(Selector(("redo:")), to: nil, from: self) 57 | } 58 | } 59 | return super.performKeyEquivalent(with: event) 60 | } 61 | } 62 | 63 | extension NSTextView { 64 | open override var frame: NSRect { 65 | didSet { 66 | self.insertionPointColor = NSColor.controlAccentColor 67 | } 68 | } 69 | } 70 | 71 | extension String { 72 | func calculateSize(using font: NSFont) -> CGSize { 73 | let attributes = [NSAttributedString.Key.font : font] 74 | return NSAttributedString(string: self, attributes: attributes).size() 75 | } 76 | } 77 | 78 | extension NSColor { 79 | static let primaries: [NSColor] = [ 80 | NSColor(resource: .uniqueWhite), 81 | NSColor(resource: .uniqueRed), 82 | NSColor(resource: .uniqueOrange), 83 | NSColor(resource: .uniqueYello), 84 | NSColor(resource: .uniqueGreen), 85 | NSColor(resource: .uniqueBlue), 86 | NSColor(resource: .uniqueViolet), 87 | NSColor(resource: .uniquePurple) 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /ScreenNote/Helper/Color+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Color+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/10. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | extension Color { 12 | static var palette: [[Color]] { 13 | NSColor.primaries.map { primary in 14 | (0 ..< 5).map { i in 15 | Color(primary.blended(withFraction: 0.2 * CGFloat(i), of: .black)!) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ScreenNote/Helper/CoreGraphics+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CoreGraphics+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import CoreGraphics 10 | 11 | extension CGPoint { 12 | init(_ scalar: CGFloat) { 13 | self.init(x: scalar, y: scalar) 14 | } 15 | 16 | func length(from: CGPoint) -> CGFloat { 17 | sqrt(pow(self.x - from.x, 2.0) + pow(self.y - from.y, 2.0)) 18 | } 19 | 20 | func radian(from: CGPoint) -> CGFloat { 21 | atan2(self.y - from.y, self.x - from.x) 22 | } 23 | 24 | func distance(_ line: Line) -> CGFloat { 25 | let lenX = line.p0.x - line.p1.x 26 | let lenY = line.p0.y - line.p1.y 27 | let sx = line.p0.x - self.x 28 | let sy = line.p0.y - self.y 29 | let s1 = lenX * sx + lenY * sy 30 | guard 0 < s1 else { 31 | return (sx * sx + sy * sy).squareRoot() 32 | } 33 | let ex = self.x - line.p1.x 34 | let ey = self.y - line.p1.y 35 | let s2 = lenX * ex + lenY * ey 36 | guard 0 < s2 else { 37 | return (ex * ex + ey * ey).squareRoot() 38 | } 39 | return abs(lenX * sy - lenY * sx) / (s1 + s2).squareRoot() 40 | } 41 | } 42 | 43 | extension CGSize { 44 | init(_ side: CGFloat) { 45 | self.init(width: side, height: side) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ScreenNote/Helper/ModifierFlag+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ModifierFlag+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/18. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | import SpiceKey 11 | 12 | extension ModifierFlag: Identifiable { 13 | public var id: Int { rawValue } 14 | 15 | var label: String { 16 | String(localized: "\(string)\(title)key") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ScreenNote/Helper/Path+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Path+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/02. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | extension Path { 12 | static func anchorPaths(bounds: CGRect) -> [Path] { 13 | let offset = CGPoint(4.0) 14 | let size = CGSize(8.0) 15 | guard !bounds.isEmpty else { 16 | let origin = bounds.origin - offset 17 | return [Path(CGRect(origin: origin, size: size))] 18 | } 19 | return Anchor.allCases.map { anchor in 20 | let origin = anchor.center(with: bounds) - offset 21 | return Path(CGRect(origin: origin, size: size)) 22 | } 23 | } 24 | 25 | var allPoints: [CGPoint] { 26 | var points = [CGPoint]() 27 | self.cgPath.applyWithBlock { element in 28 | switch element.pointee.type { 29 | case CGPathElementType.moveToPoint: 30 | points.append(element.pointee.points[0]) 31 | case CGPathElementType.addLineToPoint: 32 | points.append(element.pointee.points[0]) 33 | case CGPathElementType.addCurveToPoint: 34 | guard let point = points.last else { break } 35 | let curve = Curve([ 36 | point, 37 | element.pointee.points[0], 38 | element.pointee.points[1], 39 | element.pointee.points[2] 40 | ]) 41 | points.append(contentsOf: curve.points) 42 | case CGPathElementType.addQuadCurveToPoint: 43 | guard let point = points.last else { break } 44 | let curve = Curve([ 45 | point, 46 | element.pointee.points[0], 47 | element.pointee.points[0], 48 | element.pointee.points[1] 49 | ]) 50 | points.append(contentsOf: curve.points) 51 | case CGPathElementType.closeSubpath: 52 | guard let point = points.first else { break } 53 | points.append(point) 54 | @unknown default: 55 | fatalError("impossible") 56 | } 57 | } 58 | return points 59 | } 60 | 61 | func intersects(with point: CGPoint, radius: CGFloat) -> Bool { 62 | let points = self.allPoints 63 | guard 2 <= points.count else { return false } 64 | for i in (0 ..< points.count - 1) { 65 | if point.distance(Line(p0: points[i], p1: points[i + 1])) < radius { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | 72 | func intersects(_ path: Path) -> Bool { 73 | let pointsA = self.allPoints 74 | let pointsB = path.allPoints 75 | guard 2 <= pointsA.count && 2 <= pointsB.count else { 76 | return false 77 | } 78 | for i in (0 ..< pointsA.count - 1) { 79 | let lineA = Line(p0: pointsA[i], p1: pointsA[i + 1]) 80 | for j in (0 ..< pointsB.count - 1) { 81 | let lineB = Line(p0: pointsB[j], p1: pointsB[j + 1]) 82 | if lineA.intersects(lineB) { 83 | return true 84 | } 85 | } 86 | } 87 | return false 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ScreenNote/Helper/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | /* 2 | String+Extensions.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var infoString: String { 13 | guard let str = Bundle.main.object(forInfoDictionaryKey: self) as? String else { 14 | fatalError("infoString key is not found.") 15 | } 16 | return str 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ScreenNote/Helper/Utils.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Utils.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import Foundation 10 | 11 | func logput( 12 | _ items: Any..., 13 | file: String = #file, 14 | line: Int = #line, 15 | function: String = #function 16 | ) { 17 | #if DEBUG 18 | let fileName = URL(fileURLWithPath: file).lastPathComponent 19 | var array: [Any] = ["💫Log: \(fileName)", "Line:\(line)", function] 20 | array.append(contentsOf: items) 21 | Swift.print(array) 22 | #endif 23 | } 24 | 25 | let NOT_IMPLEMENTED = "not implemented" 26 | struct PreviewMock {} 27 | 28 | func + (l: CGPoint, r: CGPoint) -> CGPoint { 29 | CGPoint(x: l.x + r.x, y: l.y + r.y) 30 | } 31 | 32 | func += (left: inout CGPoint, right: CGPoint) { 33 | left = left + right 34 | } 35 | 36 | func - (l: CGPoint, r: CGPoint) -> CGPoint { 37 | CGPoint(x: l.x - r.x, y: l.y - r.y) 38 | } 39 | 40 | func -= (left: inout CGPoint, right: CGPoint) { 41 | left = left - right 42 | } 43 | 44 | func + (left: CGSize, right: CGSize) -> CGSize { 45 | CGSize(width: left.width + right.width, height: left.height + right.height) 46 | } 47 | 48 | func += (left: inout CGSize, right: CGSize) { 49 | left = left + right 50 | } 51 | 52 | func - (left: CGSize, right: CGSize) -> CGSize { 53 | CGSize(width: left.width - right.width, height: left.height - right.height) 54 | } 55 | 56 | func -= (left: inout CGSize, right: CGSize) { 57 | left = left - right 58 | } 59 | 60 | func * (left: CGFloat, right: CGPoint) -> CGPoint { 61 | CGPoint(x: left * right.x, y: left * right.y) 62 | } 63 | 64 | func * (left: CGFloat, right: CGSize) -> CGSize { 65 | CGSize(width: left * right.width, height: left * right.height) 66 | } 67 | -------------------------------------------------------------------------------- /ScreenNote/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/ScreenNoteApp.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ScreenNoteApp.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct ScreenNoteApp: App { 13 | typealias SAM = ScreenNoteAppModelImpl 14 | @StateObject private var appModel = SAM() 15 | 16 | var body: some Scene { 17 | MenuBarExtra { 18 | MenuView(viewModel: SAM.MVM(appModel.windowModel)) 19 | } label: { 20 | Image(.statusIcon) 21 | .environment(\.displayScale, 2.0) 22 | } 23 | Settings { 24 | SettingsView() 25 | .environmentObject(appModel) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/CanvasView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/31. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct CanvasView: View { 12 | @StateObject var viewModel: CVM 13 | @FocusState private var isFocused: Bool 14 | 15 | var body: some View { 16 | ZStack(alignment: .topLeading) { 17 | Canvas { context, size in 18 | viewModel.objects.forEach { object in 19 | switch object.type { 20 | case .select: 21 | break 22 | case .text: 23 | if object.isHidden { break } 24 | let bounds = object.bounds 25 | let offset = object.textOffset(from: bounds) 26 | let orientation = object.textOrientation 27 | context.translateBy(x: offset.x, y: offset.y) 28 | context.scaleBy(x: orientation.scale, y: 1.0) 29 | context.rotate(by: orientation.angle) 30 | context.draw( 31 | Text(object.text) 32 | .foregroundColor(object.color) 33 | .font(.system(size: object.fontSize)), 34 | in: object.textRect(from: bounds) 35 | ) 36 | context.rotate(by: -orientation.angle) 37 | context.scaleBy(x: orientation.scale, y: 1.0) 38 | context.translateBy(x: -offset.x, y: -offset.y) 39 | case .pen: 40 | if let strokeStyle = object.strokeStyle { 41 | context.stroke(object.path, with: .color(object.color), style: strokeStyle) 42 | } else { 43 | context.fill(object.path, with: .color(object.color)) 44 | } 45 | case .line, .lineRect, .lineOval: 46 | if let strokeStyle = object.strokeStyle { 47 | context.stroke(object.path, with: .color(object.color), style: strokeStyle) 48 | } else { 49 | fatalError("strokeStyle is not nil") 50 | } 51 | case .arrow, .fillRect, .fillOval: 52 | context.fill(object.path, with: .color(object.color)) 53 | } 54 | } 55 | if let bounds = viewModel.selectedObjectsBounds { 56 | context.stroke(Path(bounds), with: .color(.gray), lineWidth: 1) 57 | Path.anchorPaths(bounds: bounds).forEach { path in 58 | context.fill(path, with: .color(.gray)) 59 | } 60 | } 61 | if let object = viewModel.rectangleForSelection { 62 | context.stroke(object.path, 63 | with: .color(Color(.dashWhite)), 64 | style: StrokeStyle(lineWidth: 2, dash: [10.0, 30.0])) 65 | context.stroke(object.path, 66 | with: .color(Color(.dashBlack)), 67 | style: StrokeStyle(lineWidth: 2, dash: [10.0, 30.0], dashPhase: 20.0)) 68 | } 69 | } 70 | if let properties = viewModel.inputTextProperties { 71 | TextField(" ", text: $viewModel.inputText) 72 | .textFieldStyle(.plain) 73 | .foregroundColor(viewModel.textColor) 74 | .font(.system(size: properties.fontSize)) 75 | .lineLimit(1) 76 | .fixedSize() 77 | .overlay( 78 | RoundedRectangle(cornerRadius: 2) 79 | .stroke(Color.accentColor, lineWidth: 4.0) 80 | .opacity(0.5) 81 | .padding(-4.0) 82 | ) 83 | .offset(properties.inputTextOffset) 84 | .focused($isFocused) 85 | .onAppear { 86 | isFocused = true 87 | } 88 | .onSubmit { 89 | viewModel.endEditing(inputTextObject: properties.object, 90 | fontSize: properties.fontSize) 91 | } 92 | } 93 | } 94 | .frame(maxWidth: .infinity, maxHeight: .infinity) 95 | .contentShape(Rectangle()) 96 | .gesture( 97 | DragGesture(minimumDistance: 0) 98 | .onChanged({ value in 99 | if viewModel.dragging { 100 | viewModel.dragMoved(startLocation: value.startLocation, 101 | location: value.location) 102 | } else { 103 | viewModel.dragging = true 104 | viewModel.dragBegan(location: value.startLocation) 105 | } 106 | }) 107 | .onEnded({ value in 108 | viewModel.dragEnded(startLocation: value.startLocation, 109 | location: value.location) 110 | viewModel.dragging = false 111 | }) 112 | ) 113 | } 114 | } 115 | 116 | #Preview { 117 | CanvasView(viewModel: PreviewMock.CanvasViewModelMock()) 118 | } 119 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/MenuView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | MenuView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/11/18. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct MenuView: View { 12 | @StateObject var viewModel: MVM 13 | 14 | var body: some View { 15 | VStack { 16 | Button(viewModel.canvasVisible.label) { 17 | viewModel.showOrHide() 18 | } 19 | Divider() 20 | if #available(macOS 14.0, *) { 21 | SettingsLink { 22 | Text("settings") 23 | }.preActionButtonStyle { 24 | viewModel.activateApp() 25 | } 26 | } else { 27 | Button("settings") { 28 | viewModel.openSettings() 29 | } 30 | } 31 | Divider() 32 | Button("aboutApp") { 33 | viewModel.openAbout() 34 | } 35 | Button("reportAnIssue") { 36 | viewModel.sendIssueReport() 37 | } 38 | Button("terminateApp") { 39 | viewModel.terminateApp() 40 | } 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | MenuView(viewModel: PreviewMock.MenuViewModelMock()) 47 | } 48 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/PreActionButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | PreActionButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2024/10/14. 6 | Copyright © 2024 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | @available(macOS 14.0, *) 12 | struct PreActionButtonStyle: PrimitiveButtonStyle { 13 | let preAction: () -> Void 14 | 15 | init(preAction: @escaping () -> Void) { 16 | self.preAction = preAction 17 | } 18 | 19 | func makeBody(configuration: Configuration) -> some View { 20 | Button(role: configuration.role) { 21 | preAction() 22 | configuration.trigger() 23 | } label: { 24 | configuration.label 25 | } 26 | } 27 | } 28 | 29 | @available(macOS 14.0, *) 30 | struct PreActionButtonStyleModifier: ViewModifier { 31 | let preAction: () -> Void 32 | 33 | init(preAction: @escaping () -> Void) { 34 | self.preAction = preAction 35 | } 36 | 37 | func body(content: Content) -> some View { 38 | content.buttonStyle(PreActionButtonStyle(preAction: preAction)) 39 | } 40 | } 41 | 42 | @available(macOS 14.0, *) 43 | extension View { 44 | func preActionButtonStyle(preAction: @escaping () -> Void) -> some View { 45 | modifier(PreActionButtonStyleModifier(preAction: preAction)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/Settings/CanvasSettingsView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasSettingsView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct CanvasSettingsView: View { 12 | @StateObject var viewModel: CVM 13 | 14 | var body: some View { 15 | Form { 16 | LabeledContent("objects:") { 17 | Toggle(isOn: $viewModel.clearAllObjects) { 18 | Text("clearAllObjects") 19 | } 20 | } 21 | Picker(selection: $viewModel.defaultObjectType) { 22 | ForEach(ObjectType.defaultObjects) { objectType in 23 | Label(objectType.label, systemImage: objectType.symbolName) 24 | .labelStyle(.titleAndIcon) 25 | .tag(objectType) 26 | } 27 | } label: { 28 | Text("defaultObjectType:") 29 | EmptyView() 30 | } 31 | .pickerStyle(.menu) 32 | .fixedSize(horizontal: true, vertical: false) 33 | Divider() 34 | LabeledContent("defaultColor:") { 35 | Button("dummy") { 36 | viewModel.showColorPopover = true 37 | } 38 | .buttonStyle(.color(viewModel.defaultColor)) 39 | .popover(isPresented: $viewModel.showColorPopover, arrowEdge: .bottom) { 40 | colorPopover 41 | } 42 | } 43 | Slider(value: $viewModel.defaultOpacity, in: (0.2 ... 1.0)) { 44 | Text("defaultOpacity:") 45 | } minimumValueLabel: { 46 | Text(String(format: "%4.1f", viewModel.defaultOpacity)) 47 | .font(.system(.body, design: .monospaced)) 48 | } maximumValueLabel: { 49 | Text(Image(systemName: "checkerboard.rectangle")) 50 | .foregroundColor(Color.primary.opacity(viewModel.defaultOpacity)) 51 | } onEditingChanged: { flag in 52 | if !flag { 53 | viewModel.endUpdatingDefaultOpacity() 54 | } 55 | } 56 | HStack(spacing: 0) { 57 | Slider(value: $viewModel.defaultLineWidth, in: (1 ... 20)) { 58 | Text("defaultLineWidth:") 59 | } minimumValueLabel: { 60 | Text(String(format: "%4.1f", viewModel.defaultLineWidth)) 61 | .font(.system(.body, design: .monospaced)) 62 | } maximumValueLabel: { 63 | Text(verbatim: "") 64 | .foregroundColor(.clear) 65 | } onEditingChanged: { flag in 66 | if !flag { 67 | viewModel.endUpdatingDefaultLineWidth() 68 | } 69 | } 70 | Rectangle() 71 | .foregroundColor(Color.clear) 72 | .overlay( 73 | RoundedRectangle(cornerRadius: 2) 74 | .frame(height: viewModel.defaultLineWidth) 75 | .foregroundColor(Color.secondary) 76 | ) 77 | .frame(width: 18, height: 20) 78 | } 79 | Divider() 80 | LabeledContent("backgroundColor:") { 81 | HStack(alignment: .center, spacing: 8) { 82 | ForEach(viewModel.backgrounds.indices, id: \.self) { index in 83 | Button("dummy") { 84 | viewModel.updateBackgroundColor(index) 85 | } 86 | .buttonStyle(.selectableColor( 87 | color: viewModel.backgrounds[index], 88 | selection: Binding( 89 | get: { viewModel.backgroundColorIndex == index }, 90 | set: { _, _ in } 91 | ) 92 | )) 93 | } 94 | } 95 | .fixedSize() 96 | } 97 | Slider(value: $viewModel.backgroundOpacity, in: (0.02 ... 1.0)) { 98 | Text("backgroundOpacity:") 99 | } minimumValueLabel: { 100 | Text(String(format: "%4.2f", viewModel.backgroundOpacity)) 101 | .font(.system(.body, design: .monospaced)) 102 | } maximumValueLabel: { 103 | Text(Image(systemName: "checkerboard.rectangle")) 104 | .foregroundColor(Color.primary.opacity(viewModel.backgroundOpacity)) 105 | } onEditingChanged: { flag in 106 | if !flag { 107 | viewModel.endUpdatingBackgroundOpacity() 108 | } 109 | } 110 | } 111 | .formStyle(.columns) 112 | .fixedSize() 113 | } 114 | 115 | var colorPopover: some View { 116 | HStack(spacing: 4) { 117 | ForEach(viewModel.colors.indices, id: \.self) { i in 118 | VStack(spacing: 4) { 119 | ForEach(viewModel.colors[i].indices, id: \.self) { j in 120 | let index = i + 8 * j 121 | Button { 122 | viewModel.updateDefaultColor(index) 123 | } label: { 124 | EmptyView() 125 | } 126 | .buttonStyle(.colorPalette( 127 | color: viewModel.colors[i][j], 128 | selection: Binding( 129 | get: { viewModel.defaultColorIndex == index }, 130 | set: { _ in } 131 | ) 132 | )) 133 | } 134 | } 135 | } 136 | } 137 | .padding(8) 138 | } 139 | } 140 | 141 | #Preview { 142 | ForEach(["en_US", "ja_JP"], id: \.self) { id in 143 | CanvasSettingsView(viewModel: PreviewMock.CanvasSettingsViewModelMock()) 144 | .environment(\.locale, .init(identifier: id)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/Settings/ColorButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ColorButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/09. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ColorButtonStyle: ButtonStyle { 12 | let color: Color 13 | 14 | func makeBody(configuration: Configuration) -> some View { 15 | configuration.label 16 | .frame(width: 32, height: 16) 17 | .foregroundStyle(Color.clear) 18 | .background(color) 19 | .cornerRadius(4) 20 | .overlay( 21 | RoundedRectangle(cornerRadius: 4) 22 | .stroke(Color.secondary) 23 | ) 24 | .opacity(configuration.isPressed ? 0.6 : 1.0) 25 | } 26 | } 27 | 28 | extension ButtonStyle where Self == ColorButtonStyle { 29 | static func color(_ color: Color) -> ColorButtonStyle { 30 | ColorButtonStyle(color: color) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/Settings/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | GeneralSettingsView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SpiceKey 10 | import SwiftUI 11 | 12 | struct GeneralSettingsView: View { 13 | @StateObject var viewModel: GVM 14 | 15 | var body: some View { 16 | Form { 17 | Picker(selection: $viewModel.toggleMethod) { 18 | ForEach(ToggleMethod.allCases) { toggleMethod in 19 | radioContent(toggleMethod) 20 | .tag(toggleMethod) 21 | } 22 | } label: { 23 | Text("toggleMethod:") 24 | EmptyView() 25 | } 26 | .pickerStyle(.radioGroup) 27 | Picker(selection: $viewModel.modifierFlag) { 28 | ForEach(ModifierFlag.allCases) { modifierFlag in 29 | Text(modifierFlag.label) 30 | .tag(modifierFlag) 31 | } 32 | } label: { 33 | Text("modifierKey:") 34 | EmptyView() 35 | } 36 | .pickerStyle(.menu) 37 | .fixedSize(horizontal: true, vertical: false) 38 | Divider() 39 | Picker(selection: $viewModel.toolBarPosition) { 40 | ForEach(ToolBarPosition.allCases) { position in 41 | Text(position.label) 42 | .tag(position) 43 | } 44 | } label: { 45 | Text("toolBarPosition:") 46 | EmptyView() 47 | } 48 | .pickerStyle(.menu) 49 | .fixedSize(horizontal: true, vertical: false) 50 | LabeledContent("introduction:") { 51 | Toggle(isOn: $viewModel.showToggleMethod) { 52 | Text("showToggleMethod") 53 | } 54 | } 55 | LabeledContent("launch:") { 56 | Toggle(isOn: $viewModel.launchAtLogin) { 57 | Text("launchAtLogin") 58 | } 59 | } 60 | } 61 | .formStyle(.columns) 62 | .fixedSize() 63 | } 64 | 65 | func radioContent(_ toggleMethod: ToggleMethod) -> some View { 66 | Group { 67 | switch toggleMethod { 68 | case .longPressKey: 69 | LabeledContent { 70 | Slider(value: $viewModel.longPressSeconds, in: (0.5 ... 1.5)) { 71 | EmptyView() 72 | } minimumValueLabel: { 73 | Text(verbatim: "") 74 | .foregroundColor(.clear) 75 | } maximumValueLabel: { 76 | Text("\(viewModel.longPressSeconds)s") 77 | .font(.system(.body, design: .monospaced)) 78 | } onEditingChanged: { flag in 79 | if !flag { 80 | viewModel.endUpdatingLongPressSeconds() 81 | } 82 | } 83 | .controlSize(.small) 84 | } label: { 85 | Text(toggleMethod.label) 86 | } 87 | case .pressBothSideKeys: 88 | Text(toggleMethod.label) 89 | } 90 | } 91 | } 92 | } 93 | 94 | #Preview { 95 | ForEach(["en_US", "ja_JP"], id: \.self) { id in 96 | GeneralSettingsView(viewModel: PreviewMock.GeneralSettingsViewModelMock()) 97 | .environment(\.locale, .init(identifier: id)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/Settings/SelectableColorButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | SelectableColorButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/11. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct SelectableColorButtonStyle: ButtonStyle { 12 | let color: Color 13 | @Binding var selection: Bool 14 | 15 | func makeBody(configuration: Configuration) -> some View { 16 | configuration.label 17 | .frame(width: 32, height: 16) 18 | .foregroundStyle(Color.clear) 19 | .background(color) 20 | .cornerRadius(4) 21 | .overlay( 22 | RoundedRectangle(cornerRadius: 4) 23 | .stroke(Color.accentColor.opacity(selection ? 0.6 : 0.0), lineWidth: 2) 24 | ) 25 | .opacity(configuration.isPressed ? 0.6 : 1.0) 26 | } 27 | } 28 | 29 | extension ButtonStyle where Self == SelectableColorButtonStyle { 30 | static func selectableColor( 31 | color: Color, 32 | selection: Binding 33 | ) -> SelectableColorButtonStyle { 34 | SelectableColorButtonStyle(color: color, selection: selection) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | SettingsView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct SettingsView: View { 12 | @EnvironmentObject private var appModel: SAM 13 | 14 | var body: some View { 15 | TabView(selection: $appModel.settingsTab) { 16 | GeneralSettingsView( 17 | viewModel: SAM.GVM(appModel.userDefaultsRepository) 18 | ) 19 | .tabItem { 20 | Label("general", systemImage: "gear") 21 | } 22 | .tag(SettingsTabType.general) 23 | CanvasSettingsView( 24 | viewModel: SAM.CsVM(appModel.userDefaultsRepository) 25 | ) 26 | .tabItem { 27 | Label("canvas", systemImage: "square.and.pencil") 28 | } 29 | .tag(SettingsTabType.canvas) 30 | } 31 | .padding(.horizontal, 40) 32 | .padding(.vertical, 20) 33 | .accessibilityIdentifier("Settings") 34 | } 35 | } 36 | 37 | #Preview { 38 | SettingsView() 39 | .environmentObject(PreviewMock.ScreenNoteAppModelMock()) 40 | } 41 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ShortcutPanel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ShortcutPanel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import AppKit 10 | import SpiceKey 11 | import SwiftUI 12 | 13 | final class ShortcutPanel: NSPanel { 14 | init(_ toggleMethod: ToggleMethod, _ modifierFlag: ModifierFlag) { 15 | super.init(contentRect: NSRect(x: 0, y: 0, width: 20, height: 20), 16 | styleMask: [.borderless, .nonactivatingPanel], 17 | backing: .buffered, 18 | defer: false) 19 | self.level = .floating 20 | self.collectionBehavior = [.canJoinAllSpaces] 21 | self.isOpaque = false 22 | self.backgroundColor = NSColor.clear 23 | self.alphaValue = 0.0 24 | 25 | let shortcutView = ShortcutView(toggleMethod, modifierFlag) 26 | let hostingView = NSHostingView(rootView: shortcutView) 27 | hostingView.setFrameSize(hostingView.fittingSize) 28 | self.contentView = hostingView 29 | } 30 | 31 | func fadeIn() { 32 | self.orderFrontRegardless() 33 | if let screenFrame = NSScreen.main?.frame { 34 | let size = self.frame.size 35 | let origin = NSPoint(x: 0.5 * (screenFrame.width - size.width), 36 | y: 0.5 * (screenFrame.height - size.height)) 37 | self.setFrameOrigin(origin) 38 | } else { 39 | self.center() 40 | } 41 | NSAnimationContext.runAnimationGroup { context in 42 | context.duration = 0.2 43 | context.allowsImplicitAnimation = true 44 | self.animator().alphaValue = 1.0 45 | } 46 | } 47 | 48 | func fadeOut() { 49 | self.resignKey() 50 | NSAnimationContext.runAnimationGroup { context in 51 | context.duration = 0.2 52 | context.allowsImplicitAnimation = true 53 | self.animator().alphaValue = 0 54 | } completionHandler: { 55 | self.close() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ShortcutView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ShortcutView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/30. 6 | 7 | */ 8 | 9 | import SpiceKey 10 | import SwiftUI 11 | 12 | struct ShortcutView: View { 13 | let toggleMethod: ToggleMethod 14 | let modifierFlag: ModifierFlag 15 | 16 | init(_ toggleMethod: ToggleMethod, _ modifierFlag: ModifierFlag) { 17 | self.toggleMethod = toggleMethod 18 | self.modifierFlag = modifierFlag 19 | } 20 | 21 | var body: some View { 22 | switch toggleMethod { 23 | case .longPressKey: 24 | longPressNote 25 | case .pressBothSideKeys: 26 | pressBothSideNote 27 | } 28 | } 29 | 30 | var longPressNote: some View { 31 | VStack(spacing: 20) { 32 | Text("longPress\(modifierFlag.title)") 33 | .font(.body) 34 | .foregroundColor(Color.secondary) 35 | Text(modifierFlag.string) 36 | .font(.system(size: 100, weight: .bold)) 37 | .foregroundColor(Color.secondary) 38 | } 39 | .padding(20) 40 | .background(Color(.panelBackground)) 41 | .cornerRadius(12) 42 | .fixedSize() 43 | } 44 | 45 | var pressBothSideNote: some View { 46 | VStack(spacing: 20) { 47 | Text("pressBothSide\(modifierFlag.title)") 48 | .font(.body) 49 | .foregroundColor(Color.secondary) 50 | HStack(spacing: 40) { 51 | Text(modifierFlag.string) 52 | .font(.system(size: 100, weight: .bold)) 53 | .foregroundColor(Color.secondary) 54 | Text(modifierFlag.string) 55 | .font(.system(size: 100, weight: .bold)) 56 | .foregroundColor(Color.secondary) 57 | } 58 | } 59 | .padding(20) 60 | .background(Color(.panelBackground)) 61 | .cornerRadius(12) 62 | .fixedSize() 63 | } 64 | } 65 | 66 | #Preview { 67 | ShortcutView(.longPressKey, .control) 68 | } 69 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ColorPaletteButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ColorPaletteButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/03. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ColorPaletteButtonStyle: ButtonStyle { 12 | let color: Color 13 | @Binding var selection: Bool 14 | 15 | func makeBody(configuration: Configuration) -> some View { 16 | Rectangle() 17 | .frame(width: 16, height: 16) 18 | .foregroundColor(color) 19 | .cornerRadius(2) 20 | .overlay( 21 | RoundedRectangle(cornerRadius: 2) 22 | .stroke(selection ? Color.primary : Color.clear) 23 | ) 24 | .opacity(configuration.isPressed ? 0.6 : 1.0) 25 | } 26 | } 27 | 28 | extension ButtonStyle where Self == ColorPaletteButtonStyle { 29 | static func colorPalette( 30 | color: Color, 31 | selection: Binding 32 | ) -> ColorPaletteButtonStyle { 33 | ColorPaletteButtonStyle(color: color, selection: selection) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ColorPalettePopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ColorPalettePopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/03. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ColorPalettePopover: View { 12 | @Binding private var color: Color 13 | @Binding private var opacity: CGFloat 14 | private let colors: [[Color]] 15 | private let startUpdatingOpacityHandler: () -> Void 16 | 17 | init( 18 | color: Binding, 19 | opacity: Binding, 20 | colors: [[Color]], 21 | startUpdatingOpacityHandler: @escaping () -> Void 22 | ) { 23 | _color = color 24 | _opacity = opacity 25 | self.colors = colors 26 | self.startUpdatingOpacityHandler = startUpdatingOpacityHandler 27 | } 28 | 29 | var body: some View { 30 | VStack { 31 | HStack(spacing: 4) { 32 | ForEach(colors.indices, id: \.self) { i in 33 | VStack(spacing: 4) { 34 | ForEach(colors[i], id: \.hashValue) { color in 35 | Button { 36 | self.color = color 37 | } label: { 38 | EmptyView() 39 | } 40 | .buttonStyle(.colorPalette( 41 | color: color, 42 | selection: Binding( 43 | get: { self.color == color }, 44 | set: { _, _ in } 45 | ) 46 | )) 47 | } 48 | } 49 | } 50 | } 51 | HStack(spacing: 8) { 52 | Text(String(format: "%3.1f", opacity)) 53 | .font(.system(.body, design: .monospaced)) 54 | Slider(value: $opacity, in: (0.2 ... 1)) { flag in 55 | if flag { 56 | startUpdatingOpacityHandler() 57 | } 58 | } 59 | .frame(height: 20) 60 | Image(systemName: "checkerboard.rectangle") 61 | .frame(height: 20) 62 | .opacity(opacity) 63 | } 64 | .help("opacity") 65 | } 66 | .padding(8) 67 | } 68 | } 69 | 70 | #Preview { 71 | ColorPalettePopover(color: .constant(.white), 72 | opacity: .constant(0.8), 73 | colors: [[]], 74 | startUpdatingOpacityHandler: {}) 75 | } 76 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/HorizontalToolBar.swift: -------------------------------------------------------------------------------- 1 | /* 2 | HorizontalToolBar.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/31. 6 | 7 | */ 8 | 9 | 10 | import SwiftUI 11 | 12 | struct HorizontalToolBar: View { 13 | @StateObject var toolBarModel: TBM 14 | 15 | var body: some View { 16 | HStack(alignment: .center, spacing: 20) { 17 | HStack(spacing: 8) { 18 | Button { 19 | toolBarModel.undo() 20 | } label: { 21 | Image(systemName: "arrowshape.turn.up.backward.fill") 22 | } 23 | .buttonStyle(.toolBar(.horizontal)) 24 | .help("goBack") 25 | .keyboardShortcut("z", modifiers: .command) 26 | .disabled(toolBarModel.disabledWhileInputingText) 27 | Button { 28 | toolBarModel.redo() 29 | } label: { 30 | Image(systemName: "arrowshape.turn.up.forward.fill") 31 | } 32 | .buttonStyle(.toolBar(.horizontal)) 33 | .help("goForward") 34 | .keyboardShortcut("z", modifiers: [.shift, .command]) 35 | .disabled(toolBarModel.disabledWhileInputingText) 36 | } 37 | HStack(spacing: 8) { 38 | objectTypeButton(.text) 39 | .keyboardShortcut("t", modifiers: []) 40 | objectTypeButton(.pen) 41 | .keyboardShortcut("p", modifiers: []) 42 | objectTypeButton(.line) 43 | .keyboardShortcut("l", modifiers: []) 44 | objectTypeButton(.arrow) 45 | .keyboardShortcut("a", modifiers: []) 46 | objectTypeButton(.fillRect) 47 | .keyboardShortcut("r", modifiers: []) 48 | objectTypeButton(.lineRect) 49 | .keyboardShortcut("R", modifiers: [.shift]) 50 | objectTypeButton(.fillOval) 51 | .keyboardShortcut("o", modifiers: []) 52 | objectTypeButton(.lineOval) 53 | .keyboardShortcut("O", modifiers: [.shift]) 54 | } 55 | HStack(spacing: 8) { 56 | objectTypeButton(.select) 57 | .keyboardShortcut("s", modifiers: []) 58 | Button { 59 | toolBarModel.showColorPopover = true 60 | } label: { 61 | Image(systemName: "drop.fill") 62 | .foregroundColor(toolBarModel.color) 63 | .opacity(toolBarModel.opacity) 64 | } 65 | .buttonStyle(.toolBar(.horizontal)) 66 | .help("colorPalette") 67 | .popover( 68 | isPresented: $toolBarModel.showColorPopover, 69 | arrowEdge: toolBarModel.arrowEdge 70 | ) { 71 | ColorPalettePopover( 72 | color: Binding( 73 | get: { toolBarModel.color }, 74 | set: { toolBarModel.updateColor($0) } 75 | ), 76 | opacity: Binding( 77 | get: { toolBarModel.opacity }, 78 | set: { toolBarModel.updateOpacity($0) } 79 | ), 80 | colors: toolBarModel.colors, 81 | startUpdatingOpacityHandler: { 82 | toolBarModel.startUpdatingOpacity() 83 | } 84 | ) 85 | } 86 | Button { 87 | toolBarModel.showLineWidthPopover = true 88 | } label: { 89 | Image(systemName: "lineweight") 90 | } 91 | .buttonStyle(.toolBar(.horizontal)) 92 | .help("lineWidth") 93 | .popover( 94 | isPresented: $toolBarModel.showLineWidthPopover, 95 | arrowEdge: toolBarModel.arrowEdge 96 | ) { 97 | LineWidthPopover( 98 | lineWidth: Binding( 99 | get: { toolBarModel.lineWidth }, 100 | set: { toolBarModel.updateLineWidth($0) } 101 | ), 102 | color: toolBarModel.color, 103 | opacity: toolBarModel.opacity, 104 | startUpdatingLineWidthHandler: { 105 | toolBarModel.startUpdatingLineWidth() 106 | } 107 | ) 108 | } 109 | Button { 110 | toolBarModel.showArrangePopover = true 111 | } label: { 112 | Image(systemName: "square.3.stack.3d.middle.filled") 113 | } 114 | .buttonStyle(.toolBar(.horizontal)) 115 | .disabled(toolBarModel.disabledEditObject) 116 | .help("arrange") 117 | .popover( 118 | isPresented: $toolBarModel.showArrangePopover, 119 | arrowEdge: toolBarModel.arrowEdge 120 | ) { 121 | ObjectArrangePopover( 122 | toolBarDirection: .horizontal, 123 | arrangeHandler: { arrangeMethod in 124 | toolBarModel.arrange(arrangeMethod) 125 | } 126 | ) 127 | } 128 | Button { 129 | toolBarModel.showAlignPopover = true 130 | } label: { 131 | Image(systemName: AlignMethod.horizontalAlignLeft.symbolName) 132 | } 133 | .buttonStyle(.toolBar(.horizontal)) 134 | .disabled(toolBarModel.disabledEditObject) 135 | .help("align") 136 | .popover( 137 | isPresented: $toolBarModel.showAlignPopover, 138 | arrowEdge: toolBarModel.arrowEdge 139 | ) { 140 | ObjectAlignPopover( 141 | toolBarDirection: .horizontal, 142 | alignHandler: { alignMethod in 143 | toolBarModel.align(alignMethod) 144 | } 145 | ) 146 | } 147 | Button { 148 | toolBarModel.showFlipPopover = true 149 | } label: { 150 | Image(systemName: FlipMethod.flipHorizontal.symbolName) 151 | } 152 | .buttonStyle(.toolBar(.horizontal)) 153 | .disabled(toolBarModel.disabledEditObject) 154 | .help("flip") 155 | .popover( 156 | isPresented: $toolBarModel.showFlipPopover, 157 | arrowEdge: toolBarModel.arrowEdge 158 | ) { 159 | ObjectFlipPopover( 160 | toolBarDirection: .horizontal, 161 | flipHandler: { flipMethod in 162 | toolBarModel.flip(flipMethod) 163 | } 164 | ) 165 | } 166 | Button { 167 | toolBarModel.showRotatePopover = true 168 | } label: { 169 | Image(systemName: RotateMethod.rotateRight.symbolName) 170 | } 171 | .buttonStyle(.toolBar(.horizontal)) 172 | .disabled(toolBarModel.disabledEditObject) 173 | .help("rotate") 174 | .popover( 175 | isPresented: $toolBarModel.showRotatePopover, 176 | arrowEdge: toolBarModel.arrowEdge 177 | ) { 178 | ObjectRotatePopover( 179 | toolBarDirection: .horizontal, 180 | rotateHandler: { rotateMethod in 181 | toolBarModel.rotate(rotateMethod) 182 | } 183 | ) 184 | } 185 | Button { 186 | toolBarModel.duplicateSelectedObjects() 187 | } label: { 188 | Image(systemName: "plus.rectangle.fill.on.rectangle.fill") 189 | } 190 | .buttonStyle(.toolBar(.horizontal)) 191 | .disabled(toolBarModel.disabledEditObject) 192 | .help("duplicate") 193 | Button { 194 | toolBarModel.delete() 195 | } label: { 196 | Image(systemName: "trash.fill") 197 | } 198 | .buttonStyle(.toolBar(.horizontal)) 199 | .disabled(toolBarModel.disabledEditObject) 200 | .help("delete") 201 | Button { 202 | toolBarModel.clear() 203 | } label: { 204 | Image(systemName: "rays") 205 | } 206 | .buttonStyle(.toolBar(.horizontal)) 207 | .disabled(toolBarModel.disabledWhileInputingText) 208 | .help("clear") 209 | } 210 | // Dummy Buttons for Keyboard Shortcut 211 | HStack(spacing: 8) { 212 | dummyButton { 213 | toolBarModel.selectAll() 214 | } 215 | .keyboardShortcut("a", modifiers: .command) 216 | .disabled(toolBarModel.disabledSelectAll) 217 | dummyButton { 218 | toolBarModel.delete() 219 | } 220 | .keyboardShortcut(.delete, modifiers: []) 221 | .disabled(toolBarModel.disabledEditObject) 222 | dummyButton { 223 | toolBarModel.clear() 224 | } 225 | .keyboardShortcut(.delete, modifiers: .command) 226 | } 227 | .overlay(Rectangle()) 228 | .opacity(0) 229 | } 230 | .padding(.horizontal, 8) 231 | .frame(height: 40) 232 | .frame(maxWidth: .infinity, alignment: .leading) 233 | .background(Color(NSColor.controlBackgroundColor).opacity(0.8)) 234 | } 235 | 236 | private func objectTypeButton(_ objectType: ObjectType) -> some View { 237 | Button { 238 | toolBarModel.updateObjectType(objectType) 239 | } label: { 240 | Image(systemName: objectType.symbolName) 241 | } 242 | .buttonStyle(.toolBarRadio(.horizontal, Binding( 243 | get: { toolBarModel.objectType == objectType }, 244 | set: { _, _ in } 245 | ))) 246 | .help(objectType.label) 247 | } 248 | 249 | private func dummyButton(actionHandler: @escaping () -> Void) -> some View { 250 | Button(action: { 251 | actionHandler() 252 | }, label: { 253 | EmptyView() 254 | }) 255 | } 256 | } 257 | 258 | #Preview { 259 | HorizontalToolBar(toolBarModel: PreviewMock.ToolBarModelMock()) 260 | } 261 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/LineWidthPopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | LineWidthPopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/04. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct LineWidthPopover: View { 12 | @Binding private var lineWidth: CGFloat 13 | private let color: Color 14 | private let opacity: CGFloat 15 | private let startUpdatingLineWidthHandler: () -> Void 16 | 17 | init( 18 | lineWidth: Binding, 19 | color: Color, 20 | opacity: CGFloat, 21 | startUpdatingLineWidthHandler: @escaping () -> Void 22 | ) { 23 | _lineWidth = lineWidth 24 | self.color = color 25 | self.opacity = opacity 26 | self.startUpdatingLineWidthHandler = startUpdatingLineWidthHandler 27 | } 28 | 29 | var body: some View { 30 | VStack() { 31 | Rectangle() 32 | .foregroundColor(Color.clear) 33 | .overlay( 34 | RoundedRectangle(cornerRadius: 0.5 * lineWidth) 35 | .foregroundColor(color) 36 | .opacity(opacity) 37 | .frame(height: lineWidth) 38 | ) 39 | .frame(height: 20) 40 | HStack(spacing: 8) { 41 | Text(String(format: "%4.1f", lineWidth)) 42 | .font(.system(size: 13, design: .monospaced)) 43 | Slider(value: $lineWidth, in: (1 ... 20)) { flag in 44 | if flag { 45 | startUpdatingLineWidthHandler() 46 | } 47 | } 48 | .frame(width: 150, height: 20) 49 | } 50 | } 51 | .padding(8) 52 | } 53 | } 54 | 55 | #Preview { 56 | LineWidthPopover(lineWidth: .constant(4.0), 57 | color: .white, 58 | opacity: 0.8, 59 | startUpdatingLineWidthHandler: {}) 60 | } 61 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ObjectAlignPopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectAlignPopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/04. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ObjectAlignPopover: View { 12 | private let toolBarDirection: ToolBarDirection 13 | private let alignHandler: (AlignMethod) -> Void 14 | 15 | init( 16 | toolBarDirection: ToolBarDirection, 17 | alignHandler: @escaping (AlignMethod) -> Void 18 | ) { 19 | self.toolBarDirection = toolBarDirection 20 | self.alignHandler = alignHandler 21 | } 22 | 23 | var body: some View { 24 | VStack { 25 | HStack(spacing: 8) { 26 | ForEach(AlignMethod.horizontals) { alignMethod in 27 | Button { 28 | alignHandler(alignMethod) 29 | } label: { 30 | Image(systemName: alignMethod.symbolName) 31 | } 32 | .buttonStyle(.toolBar(toolBarDirection)) 33 | .help(alignMethod.help) 34 | } 35 | } 36 | HStack(spacing: 8) { 37 | ForEach(AlignMethod.verticals) { alignMethod in 38 | Button { 39 | alignHandler(alignMethod) 40 | } label: { 41 | Image(systemName: alignMethod.symbolName) 42 | } 43 | .buttonStyle(.toolBar(toolBarDirection)) 44 | .help(alignMethod.help) 45 | } 46 | } 47 | } 48 | .padding(8) 49 | } 50 | } 51 | 52 | #Preview { 53 | ObjectAlignPopover(toolBarDirection: .horizontal, 54 | alignHandler: { _ in }) 55 | } 56 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ObjectArrangePopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectArrangePopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/04. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ObjectArrangePopover: View { 12 | private let toolBarDirection: ToolBarDirection 13 | private let arrangeHandler: (ArrangeMethod) -> Void 14 | 15 | init( 16 | toolBarDirection: ToolBarDirection, 17 | arrangeHandler: @escaping (ArrangeMethod) -> Void 18 | ) { 19 | self.toolBarDirection = toolBarDirection 20 | self.arrangeHandler = arrangeHandler 21 | } 22 | 23 | var body: some View { 24 | HStack(spacing: 8) { 25 | ForEach(ArrangeMethod.allCases) { arrangeMethod in 26 | Button { 27 | arrangeHandler(arrangeMethod) 28 | } label: { 29 | Image(systemName: arrangeMethod.symbolName) 30 | } 31 | .buttonStyle(.toolBar(toolBarDirection)) 32 | .help(arrangeMethod.help) 33 | } 34 | } 35 | .padding(8) 36 | } 37 | } 38 | 39 | #Preview { 40 | ObjectArrangePopover(toolBarDirection: .horizontal, 41 | arrangeHandler: { _ in }) 42 | } 43 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ObjectFlipPopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectFlipPopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ObjectFlipPopover: View { 12 | private let toolBarDirection: ToolBarDirection 13 | private let flipHandler: (FlipMethod) -> Void 14 | 15 | init( 16 | toolBarDirection: ToolBarDirection, 17 | flipHandler: @escaping (FlipMethod) -> Void 18 | ) { 19 | self.toolBarDirection = toolBarDirection 20 | self.flipHandler = flipHandler 21 | } 22 | 23 | var body: some View { 24 | HStack(spacing: 8) { 25 | ForEach(FlipMethod.allCases) { flipMethod in 26 | Button { 27 | flipHandler(flipMethod) 28 | } label: { 29 | Image(systemName: flipMethod.symbolName) 30 | } 31 | .buttonStyle(.toolBar(toolBarDirection)) 32 | .help(flipMethod.help) 33 | } 34 | } 35 | .padding(8) 36 | } 37 | } 38 | 39 | #Preview { 40 | ObjectFlipPopover(toolBarDirection: .horizontal, 41 | flipHandler: { _ in }) 42 | } 43 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ObjectRotatePopover.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ObjectRotatePopover.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/08. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ObjectRotatePopover: View { 12 | private let toolBarDirection: ToolBarDirection 13 | private let rotateHandler: (RotateMethod) -> Void 14 | 15 | init( 16 | toolBarDirection: ToolBarDirection, 17 | rotateHandler: @escaping (RotateMethod) -> Void 18 | ) { 19 | self.toolBarDirection = toolBarDirection 20 | self.rotateHandler = rotateHandler 21 | } 22 | 23 | var body: some View { 24 | HStack(spacing: 8) { 25 | ForEach(RotateMethod.allCases) { rotateMethod in 26 | Button { 27 | rotateHandler(rotateMethod) 28 | } label: { 29 | Image(systemName: rotateMethod.symbolName) 30 | } 31 | .buttonStyle(.toolBar(toolBarDirection)) 32 | .help(rotateMethod.help) 33 | } 34 | } 35 | .padding(8) 36 | } 37 | } 38 | 39 | #Preview { 40 | ObjectRotatePopover(toolBarDirection: .horizontal, 41 | rotateHandler: { _ in }) 42 | } 43 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ToolBarButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToolBarButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/01. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ToolBarButtonStyle: ButtonStyle { 12 | @Environment(\.isEnabled) private var isEnabled: Bool 13 | let width: CGFloat 14 | 15 | func makeBody(configuration: Configuration) -> some View { 16 | configuration.label 17 | .font(.system(size: 15, design: .monospaced)) 18 | .frame(width: width, height: 30, alignment: .center) 19 | .background(Color.gray.opacity(configuration.isPressed ? 0.8 : 0.6)) 20 | .cornerRadius(6) 21 | .opacity(isEnabled ? 1.0 : 0.5) 22 | } 23 | } 24 | 25 | extension ButtonStyle where Self == ToolBarButtonStyle { 26 | static func toolBar(_ toolBarDirection: ToolBarDirection) -> ToolBarButtonStyle { 27 | let width: CGFloat = toolBarDirection == .vertical ? 30 : 40 28 | return ToolBarButtonStyle(width: width) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/ToolBarRadioButtonStyle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ToolBarRadioButtonStyle.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/03. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct ToolBarRadioButtonStyle: ButtonStyle { 12 | let width: CGFloat 13 | @Binding var selection: Bool 14 | 15 | func makeBody(configuration: Configuration) -> some View { 16 | configuration.label 17 | .font(.system(size: 15, design: .monospaced)) 18 | .frame(width: width, height: 30, alignment: .center) 19 | .background( 20 | (selection ? Color.accentColor : Color.gray) 21 | .opacity(configuration.isPressed ? 0.8 : 0.6) 22 | ) 23 | .cornerRadius(6) 24 | } 25 | } 26 | 27 | extension ButtonStyle where Self == ToolBarRadioButtonStyle { 28 | static func toolBarRadio( 29 | _ toolBarDirection: ToolBarDirection, 30 | _ selection: Binding 31 | ) -> ToolBarRadioButtonStyle { 32 | let width: CGFloat = toolBarDirection == .vertical ? 30 : 40 33 | return ToolBarRadioButtonStyle(width: width, selection: selection) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/ToolBar/VerticalToolBar.swift: -------------------------------------------------------------------------------- 1 | /* 2 | VerticalToolBar.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/02/02. 6 | 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct VerticalToolBar: View { 12 | @StateObject var toolBarModel: TBM 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 20) { 16 | HStack(spacing: 8) { 17 | Button { 18 | toolBarModel.undo() 19 | } label: { 20 | Image(systemName: "arrowshape.turn.up.backward.fill") 21 | } 22 | .buttonStyle(.toolBar(.vertical)) 23 | .help("goBack") 24 | .keyboardShortcut("z", modifiers: .command) 25 | .disabled(toolBarModel.disabledWhileInputingText) 26 | Button { 27 | toolBarModel.redo() 28 | } label: { 29 | Image(systemName: "arrowshape.turn.up.forward.fill") 30 | } 31 | .buttonStyle(.toolBar(.vertical)) 32 | .help("goForward") 33 | .keyboardShortcut("z", modifiers: [.shift, .command]) 34 | .disabled(toolBarModel.disabledWhileInputingText) 35 | } 36 | VStack(alignment: .leading, spacing: 8) { 37 | HStack(spacing: 8) { 38 | objectTypeButton(.text) 39 | .keyboardShortcut("t", modifiers: []) 40 | objectTypeButton(.pen) 41 | .keyboardShortcut("p", modifiers: []) 42 | } 43 | HStack(spacing: 8) { 44 | objectTypeButton(.line) 45 | .keyboardShortcut("l", modifiers: []) 46 | objectTypeButton(.arrow) 47 | .keyboardShortcut("a", modifiers: []) 48 | } 49 | HStack(spacing: 8) { 50 | objectTypeButton(.fillRect) 51 | .keyboardShortcut("r", modifiers: []) 52 | objectTypeButton(.lineRect) 53 | .keyboardShortcut("R", modifiers: [.shift]) 54 | } 55 | HStack(spacing: 8) { 56 | objectTypeButton(.fillOval) 57 | .keyboardShortcut("o", modifiers: []) 58 | objectTypeButton(.lineOval) 59 | .keyboardShortcut("O", modifiers: [.shift]) 60 | } 61 | } 62 | VStack(alignment: .leading, spacing: 8) { 63 | HStack(spacing: 8) { 64 | objectTypeButton(.select) 65 | .keyboardShortcut("s", modifiers: []) 66 | Button { 67 | toolBarModel.showColorPopover = true 68 | } label: { 69 | Image(systemName: "drop.fill") 70 | .foregroundColor(toolBarModel.color) 71 | .opacity(toolBarModel.opacity) 72 | } 73 | .buttonStyle(.toolBar(.vertical)) 74 | .help("colorPalette") 75 | .popover( 76 | isPresented: $toolBarModel.showColorPopover, 77 | arrowEdge: toolBarModel.arrowEdge 78 | ) { 79 | ColorPalettePopover( 80 | color: Binding( 81 | get: { toolBarModel.color }, 82 | set: { toolBarModel.updateColor($0) } 83 | ), 84 | opacity: Binding( 85 | get: { toolBarModel.opacity }, 86 | set: { toolBarModel.updateOpacity($0) } 87 | ), 88 | colors: toolBarModel.colors, 89 | startUpdatingOpacityHandler: { 90 | toolBarModel.startUpdatingOpacity() 91 | } 92 | ) 93 | } 94 | } 95 | HStack(spacing: 8) { 96 | Button { 97 | toolBarModel.showLineWidthPopover = true 98 | } label: { 99 | Image(systemName: "lineweight") 100 | } 101 | .buttonStyle(.toolBar(.vertical)) 102 | .help("lineWidth") 103 | .popover( 104 | isPresented: $toolBarModel.showLineWidthPopover, 105 | arrowEdge: toolBarModel.arrowEdge 106 | ) { 107 | LineWidthPopover( 108 | lineWidth: Binding( 109 | get: { toolBarModel.lineWidth }, 110 | set: { toolBarModel.updateLineWidth($0) } 111 | ), 112 | color: toolBarModel.color, 113 | opacity: toolBarModel.opacity, 114 | startUpdatingLineWidthHandler: { 115 | toolBarModel.startUpdatingLineWidth() 116 | } 117 | ) 118 | } 119 | Button { 120 | toolBarModel.showArrangePopover = true 121 | } label: { 122 | Image(systemName: "square.3.stack.3d.middle.filled") 123 | } 124 | .buttonStyle(.toolBar(.vertical)) 125 | .disabled(toolBarModel.disabledEditObject) 126 | .help("arrange") 127 | .popover( 128 | isPresented: $toolBarModel.showArrangePopover, 129 | arrowEdge: toolBarModel.arrowEdge 130 | ) { 131 | ObjectArrangePopover( 132 | toolBarDirection: .vertical, 133 | arrangeHandler: { arrangeMethod in 134 | toolBarModel.arrange(arrangeMethod) 135 | } 136 | ) 137 | } 138 | } 139 | HStack(spacing: 8) { 140 | Button { 141 | toolBarModel.showAlignPopover = true 142 | } label: { 143 | Image(systemName: "align.horizontal.left.fill") 144 | } 145 | .buttonStyle(.toolBar(.vertical)) 146 | .disabled(toolBarModel.disabledEditObject) 147 | .help("align") 148 | .popover( 149 | isPresented: $toolBarModel.showAlignPopover, 150 | arrowEdge: toolBarModel.arrowEdge 151 | ) { 152 | ObjectAlignPopover( 153 | toolBarDirection: .vertical, 154 | alignHandler: { alignMethod in 155 | toolBarModel.align(alignMethod) 156 | } 157 | ) 158 | } 159 | Button { 160 | toolBarModel.showFlipPopover = true 161 | } label: { 162 | Image(systemName: FlipMethod.flipHorizontal.symbolName) 163 | } 164 | .buttonStyle(.toolBar(.vertical)) 165 | .disabled(toolBarModel.disabledEditObject) 166 | .help("flip") 167 | .popover( 168 | isPresented: $toolBarModel.showFlipPopover, 169 | arrowEdge: toolBarModel.arrowEdge 170 | ) { 171 | ObjectFlipPopover( 172 | toolBarDirection: .vertical, 173 | flipHandler: { flipMethod in 174 | toolBarModel.flip(flipMethod) 175 | } 176 | ) 177 | } 178 | } 179 | HStack(spacing: 8) { 180 | Button { 181 | toolBarModel.showRotatePopover = true 182 | } label: { 183 | Image(systemName: RotateMethod.rotateRight.symbolName) 184 | } 185 | .buttonStyle(.toolBar(.vertical)) 186 | .disabled(toolBarModel.disabledEditObject) 187 | .help("rotate") 188 | .popover( 189 | isPresented: $toolBarModel.showRotatePopover, 190 | arrowEdge: toolBarModel.arrowEdge 191 | ) { 192 | ObjectRotatePopover( 193 | toolBarDirection: .vertical, 194 | rotateHandler: { rotateMethod in 195 | toolBarModel.rotate(rotateMethod) 196 | } 197 | ) 198 | } 199 | Button { 200 | toolBarModel.duplicateSelectedObjects() 201 | } label: { 202 | Image(systemName: "plus.rectangle.fill.on.rectangle.fill") 203 | } 204 | .buttonStyle(.toolBar(.vertical)) 205 | .disabled(toolBarModel.disabledEditObject) 206 | .help("duplicate") 207 | } 208 | HStack(spacing: 8) { 209 | Button { 210 | toolBarModel.delete() 211 | } label: { 212 | Image(systemName: "trash.fill") 213 | } 214 | .buttonStyle(.toolBar(.vertical)) 215 | .disabled(toolBarModel.disabledEditObject) 216 | .help("delete") 217 | Button { 218 | toolBarModel.clear() 219 | } label: { 220 | Image(systemName: "rays") 221 | } 222 | .buttonStyle(.toolBar(.vertical)) 223 | .disabled(toolBarModel.disabledWhileInputingText) 224 | .help("clear") 225 | } 226 | } 227 | // Dummy Buttons for Keyboard Shortcut 228 | VStack(alignment: .leading, spacing: 8) { 229 | dummyButton { 230 | toolBarModel.selectAll() 231 | } 232 | .keyboardShortcut("a", modifiers: .command) 233 | .disabled(toolBarModel.disabledSelectAll) 234 | dummyButton { 235 | toolBarModel.delete() 236 | } 237 | .keyboardShortcut(.delete, modifiers: []) 238 | .disabled(toolBarModel.disabledEditObject) 239 | dummyButton { 240 | toolBarModel.clear() 241 | } 242 | .keyboardShortcut(.delete, modifiers: .command) 243 | } 244 | .overlay(Rectangle()) 245 | .opacity(0) 246 | } 247 | .padding(.vertical, 8) 248 | .frame(width: 80) 249 | .frame(maxHeight: .infinity, alignment: .top) 250 | .background(Color(NSColor.controlBackgroundColor).opacity(0.8)) 251 | } 252 | 253 | private func objectTypeButton(_ objectType: ObjectType) -> some View { 254 | Button { 255 | toolBarModel.updateObjectType(objectType) 256 | } label: { 257 | Image(systemName: objectType.symbolName) 258 | } 259 | .buttonStyle(.toolBarRadio(.vertical, Binding( 260 | get: { toolBarModel.objectType == objectType }, 261 | set: { _, _ in } 262 | ))) 263 | .help(objectType.label) 264 | } 265 | 266 | private func dummyButton(actionHandler: @escaping () -> Void) -> some View { 267 | Button(action: { 268 | actionHandler() 269 | }, label: { 270 | EmptyView() 271 | }) 272 | } 273 | } 274 | 275 | #Preview { 276 | VerticalToolBar(toolBarModel: PreviewMock.ToolBarModelMock()) 277 | } 278 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/WorkspacePanel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | CanvasPanel.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/31. 6 | 7 | */ 8 | 9 | import AppKit 10 | import SpiceKey 11 | import SwiftUI 12 | 13 | final class WorkspaceHostingView: NSHostingView { 14 | override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true } 15 | } 16 | 17 | final class WorkspacePanel: NSPanel { 18 | override var canBecomeKey: Bool { true } 19 | 20 | init( 21 | _ userDefaultsRepository: UserDefaultsRepository, 22 | _ objectModel: ObjectModel 23 | ) { 24 | super.init(contentRect: NSRect(x: 0, y: 0, width: 20, height: 20), 25 | styleMask: [.borderless, .nonactivatingPanel], 26 | backing: .buffered, 27 | defer: false) 28 | self.level = .popUpMenu 29 | self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] 30 | self.isOpaque = false 31 | self.hasShadow = false 32 | let white = 1.0 - CGFloat(userDefaultsRepository.backgroundColorIndex) 33 | let alpha = max(0.01, userDefaultsRepository.backgroundOpacity) 34 | self.backgroundColor = NSColor(white: white, alpha: alpha) 35 | self.alphaValue = 0.0 36 | if userDefaultsRepository.clearAllObjects { 37 | objectModel.resetHistory() 38 | } 39 | objectModel.resetDefaultSettings() 40 | let viewModel = WVM(objectModel, userDefaultsRepository.toolBarPosition) 41 | let workspaceView = WorkspaceView(viewModel: viewModel) 42 | self.contentView = WorkspaceHostingView(rootView: workspaceView) 43 | } 44 | 45 | func fadeIn() { 46 | self.orderFrontRegardless() 47 | if let visibleFrame = NSScreen.main?.visibleFrame { 48 | self.setFrame(visibleFrame, display: true, animate: false) 49 | } 50 | NSAnimationContext.runAnimationGroup { context in 51 | context.duration = 0.2 52 | context.allowsImplicitAnimation = true 53 | self.animator().alphaValue = 1.0 54 | } 55 | } 56 | 57 | func fadeOut() { 58 | self.resignKey() 59 | NSAnimationContext.runAnimationGroup { context in 60 | context.duration = 0.2 61 | context.allowsImplicitAnimation = true 62 | self.animator().alphaValue = 0 63 | } completionHandler: { 64 | self.close() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ScreenNote/Presentation/View/WorkspaceView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | WorkspaceView.swift 3 | ScreenNote 4 | 5 | Created by Takuto Nakamura on 2023/01/31. 6 | Copyright © 2023 Studio Kyome. All rights reserved. 7 | */ 8 | 9 | import SwiftUI 10 | 11 | struct WorkspaceView: View { 12 | @StateObject var viewModel: WVM 13 | 14 | var body: some View { 15 | switch viewModel.toolBarPosition { 16 | case .top: 17 | VStack(spacing: 0) { 18 | HorizontalToolBar(toolBarModel: WVM.TBM(objectModel: viewModel.objectModel, 19 | arrowEdge: .bottom)) 20 | CanvasView(viewMode: WVM.CVM(objectModel: viewModel.objectModel)) 21 | } 22 | case .right: 23 | HStack(spacing: 0) { 24 | CanvasView(viewMode: WVM.CVM(objectModel: viewModel.objectModel)) 25 | VerticalToolBar(toolBarModel: WVM.TBM(objectModel: viewModel.objectModel, 26 | arrowEdge: .leading)) 27 | } 28 | case .bottom: 29 | VStack(spacing: 0) { 30 | CanvasView(viewMode: WVM.CVM(objectModel: viewModel.objectModel)) 31 | HorizontalToolBar(toolBarModel: WVM.TBM(objectModel: viewModel.objectModel, 32 | arrowEdge: .top)) 33 | } 34 | case .left: 35 | HStack(spacing: 0) { 36 | VerticalToolBar(toolBarModel: WVM.TBM(objectModel: viewModel.objectModel, 37 | arrowEdge: .trailing)) 38 | CanvasView(viewMode: WVM.CVM(objectModel: viewModel.objectModel)) 39 | } 40 | } 41 | } 42 | } 43 | 44 | #Preview { 45 | WorkspaceView(viewModel: PreviewMock.WorkspaceViewModelMock()) 46 | } 47 | -------------------------------------------------------------------------------- /ScreenNote/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenNote/ScreenNote.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/ScreenNote/4254bb570f560ce00e5839bbe6dba5cd90817c52/screenshot.png --------------------------------------------------------------------------------