├── .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 | 
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
--------------------------------------------------------------------------------