Compare commits

..
10 Commits
Author SHA1 Message Date
marcoandClaude Opus 4.6 d878e8e333 Rename app from Snipping Tool to SnipMC
Changes bundle ID, URL scheme, display name, project name,
and all user-facing references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 22:49:38 +02:00
marcoandClaude Opus 4.6 b55947e536 Add clipboard image editing
Open the editor with an image from the clipboard via menu
("Aus Zwischenablage bearbeiten") or URL (snippingtool://clipboard).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 18:57:30 +02:00
marcoandClaude Opus 4.6 d50a1d550a Add documentation tab to About window
Two-tab layout: Info (version, creator) and Dokumentation (capture
modes, editor tools, URL schema reference, Logitech Options+ setup
guide, permission hints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 18:52:47 +02:00
marcoandClaude Opus 4.6 e2c3ef7f37 Add snippingtool://editlast URL to edit last capture
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 18:48:16 +02:00
marcoandClaude Opus 4.6 7a78176183 Fix flipped image in editor canvas
Use respectFlipped: true when drawing NSImage so it renders correctly
in the flipped NSView coordinate system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 18:42:12 +02:00
marcoandClaude Opus 4.6 8c73c5fbc5 Fix editor integration and add snippingtool://edit URL
- Fix menu button by deferring NSOpenPanel to next run loop
- Add snippingtool://edit URL to open file editor directly
- Add app activation before showing open panel (LSUIElement fix)
- Remove debug logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 18:25:07 +02:00
marcoandClaude Opus 4.6 78d6809445 Bump version to 1.0.1 (build 2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 17:53:17 +02:00
marcoandClaude Opus 4.6 59b3f8d2a1 Add edit parameter to URL scheme for capture-then-edit workflow
snippingtool://region?edit=true now captures a region screenshot and
immediately opens the editor. Works with all modes: fullscreen, window,
region. Without ?edit=true, behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 17:51:44 +02:00
marcoandClaude Opus 4.6 f6e90047a8 Add About window with creator and version info
Shows app icon, version, "Freeware" badge, creator (Marco Wende —
Wende.IT), and link to wende.it. Accessible via "Über Snipping Tool…"
in the menu bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 17:47:27 +02:00
marcoandClaude Opus 4.6 062285abbf Add image editor with annotation tools
Integrates an editor accessible via "Letzte Aufnahme bearbeiten…" or
"Bild bearbeiten…" in the menu bar. Supports arrow, rectangle, ellipse,
and text annotations with color picker, line width, undo/redo, and
export to file or clipboard.

New files in Sources/Editor/:
- AnnotationItem/Document: data model and state management
- AnnotationCanvasView: NSView with CG rendering and mouse interaction
- AnnotationRenderer: shared rendering for canvas and image export
- EditorToolbarView: SwiftUI toolbar with tool selection and actions
- EditorWindowController: window lifecycle following SettingsWindowController pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 17:44:20 +02:00
15 changed files with 1599 additions and 38 deletions
+395
View File
@@ -0,0 +1,395 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
130D45C3838D4526C57BAE83 /* EditorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35857E1B1439529A1C6932B7 /* EditorWindowController.swift */; };
1BDB0D69E351CA5C221B94EA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081CCD02ACD35FC227278E48 /* SettingsView.swift */; };
1D8FC537A5AC329C446C4218 /* SnippingToolApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */; };
24C10B8D3D9AD133DE9920BC /* HotKeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275CE51E9539509ACD429507 /* HotKeyCombo.swift */; };
2919075053BD6EBD40F49085 /* KeyCaptureRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */; };
30D358DCC80875B5AE8E7CC9 /* CaptureMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */; };
34615E753CFF584C1561040F /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF14927359614F91B42F70A0 /* SettingsWindowController.swift */; };
449C4A7698D1DA7F8CD8B6BC /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */; };
475DE4CC16A7A1EA786348F0 /* AnnotationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2048886E3C9DB924A3F618C /* AnnotationItem.swift */; };
4B8591A347CDB31A5866A532 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6120576595AA8DB5C96DD665 /* AboutWindowController.swift */; };
54D578ADAA0EA4B1B4E977BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CE7E1EFC83123C2285157A /* AppDelegate.swift */; };
6FBABC7A889F4B1C0553ECEB /* KeyCodeSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */; };
735EDBCCACD86A435C087FCA /* MenuBarContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */; };
7CF577EBE926EBE85AD18CFF /* AnnotationDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */; };
7FD8CD2B11ECC27CF23976B1 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F76602A1109742C19A7F88 /* ImageFormat.swift */; };
8DCD5BBF7C77AD494D4296F4 /* OutputAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56887E8312DA185054D0C9B2 /* OutputAction.swift */; };
AC0CAFD6D28E8B65BDB17976 /* HotKeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */; };
B68ACECC44BB1E5907336177 /* AnnotationRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */; };
BE94EAB3F172FA6B73752298 /* EditorToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */; };
CB4219E3C65E1700F5D38149 /* HotKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */; };
E0DDAD12E91158BF18D77A94 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0122BC6EC2818009EA7F3D53 /* Assets.xcassets */; };
EE6381E9BC6F06BAA563A376 /* AnnotationCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
0122BC6EC2818009EA7F3D53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
081CCD02ACD35FC227278E48 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
275CE51E9539509ACD429507 /* HotKeyCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyCombo.swift; sourceTree = "<group>"; };
27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyManager.swift; sourceTree = "<group>"; };
35857E1B1439529A1C6932B7 /* EditorWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorWindowController.swift; sourceTree = "<group>"; };
415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyRecorderView.swift; sourceTree = "<group>"; };
47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCodeSymbols.swift; sourceTree = "<group>"; };
56887E8312DA185054D0C9B2 /* OutputAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputAction.swift; sourceTree = "<group>"; };
5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
6120576595AA8DB5C96DD665 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = "<group>"; };
61F76602A1109742C19A7F88 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = "<group>"; };
7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarContentView.swift; sourceTree = "<group>"; };
829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCaptureRepresentable.swift; sourceTree = "<group>"; };
83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureMode.swift; sourceTree = "<group>"; };
97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationCanvasView.swift; sourceTree = "<group>"; };
A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationDocument.swift; sourceTree = "<group>"; };
A2048886E3C9DB924A3F618C /* AnnotationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationItem.swift; sourceTree = "<group>"; };
B4CE7E1EFC83123C2285157A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DC94AA0BA2B93F0EC38D151B /* SnipMC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SnipMC.app; sourceTree = BUILT_PRODUCTS_DIR; };
DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationRenderer.swift; sourceTree = "<group>"; };
DF14927359614F91B42F70A0 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnippingToolApp.swift; sourceTree = "<group>"; };
F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorToolbarView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
27F119FCBEB53FC15BA69005 = {
isa = PBXGroup;
children = (
B94195006D6A63C034170E68 /* Sources */,
627FB11FD8CF42D3297E06E1 /* Products */,
);
sourceTree = "<group>";
};
627FB11FD8CF42D3297E06E1 /* Products */ = {
isa = PBXGroup;
children = (
DC94AA0BA2B93F0EC38D151B /* SnipMC.app */,
);
name = Products;
sourceTree = "<group>";
};
68AAE9FD7418A399448CE045 /* Editor */ = {
isa = PBXGroup;
children = (
97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */,
A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */,
A2048886E3C9DB924A3F618C /* AnnotationItem.swift */,
DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */,
F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */,
35857E1B1439529A1C6932B7 /* EditorWindowController.swift */,
);
path = Editor;
sourceTree = "<group>";
};
B94195006D6A63C034170E68 /* Sources */ = {
isa = PBXGroup;
children = (
6120576595AA8DB5C96DD665 /* AboutWindowController.swift */,
5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */,
B4CE7E1EFC83123C2285157A /* AppDelegate.swift */,
0122BC6EC2818009EA7F3D53 /* Assets.xcassets */,
83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */,
275CE51E9539509ACD429507 /* HotKeyCombo.swift */,
27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */,
415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */,
61F76602A1109742C19A7F88 /* ImageFormat.swift */,
829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */,
47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */,
7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */,
56887E8312DA185054D0C9B2 /* OutputAction.swift */,
081CCD02ACD35FC227278E48 /* SettingsView.swift */,
DF14927359614F91B42F70A0 /* SettingsWindowController.swift */,
E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */,
68AAE9FD7418A399448CE045 /* Editor */,
);
path = Sources;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
EAED69AFD4EF2FC3C57FA1B8 /* SnipMC */ = {
isa = PBXNativeTarget;
buildConfigurationList = 99C7A42A282B29A64B2B7688 /* Build configuration list for PBXNativeTarget "SnipMC" */;
buildPhases = (
117C9BBC9123725FE6AD71C0 /* Sources */,
C24E14B384D8A0D263EED133 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SnipMC;
packageProductDependencies = (
);
productName = SnipMC;
productReference = DC94AA0BA2B93F0EC38D151B /* SnipMC.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
E5D0ACF0D9F5E10B2B3D9BB2 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
};
};
buildConfigurationList = 7F1515B2EDB59F08F4421C1E /* Build configuration list for PBXProject "SnipMC" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 27F119FCBEB53FC15BA69005;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 627FB11FD8CF42D3297E06E1 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
EAED69AFD4EF2FC3C57FA1B8 /* SnipMC */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C24E14B384D8A0D263EED133 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E0DDAD12E91158BF18D77A94 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
117C9BBC9123725FE6AD71C0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4B8591A347CDB31A5866A532 /* AboutWindowController.swift in Sources */,
EE6381E9BC6F06BAA563A376 /* AnnotationCanvasView.swift in Sources */,
7CF577EBE926EBE85AD18CFF /* AnnotationDocument.swift in Sources */,
475DE4CC16A7A1EA786348F0 /* AnnotationItem.swift in Sources */,
B68ACECC44BB1E5907336177 /* AnnotationRenderer.swift in Sources */,
449C4A7698D1DA7F8CD8B6BC /* AppCoordinator.swift in Sources */,
54D578ADAA0EA4B1B4E977BB /* AppDelegate.swift in Sources */,
30D358DCC80875B5AE8E7CC9 /* CaptureMode.swift in Sources */,
BE94EAB3F172FA6B73752298 /* EditorToolbarView.swift in Sources */,
130D45C3838D4526C57BAE83 /* EditorWindowController.swift in Sources */,
24C10B8D3D9AD133DE9920BC /* HotKeyCombo.swift in Sources */,
CB4219E3C65E1700F5D38149 /* HotKeyManager.swift in Sources */,
AC0CAFD6D28E8B65BDB17976 /* HotKeyRecorderView.swift in Sources */,
7FD8CD2B11ECC27CF23976B1 /* ImageFormat.swift in Sources */,
2919075053BD6EBD40F49085 /* KeyCaptureRepresentable.swift in Sources */,
6FBABC7A889F4B1C0553ECEB /* KeyCodeSymbols.swift in Sources */,
735EDBCCACD86A435C087FCA /* MenuBarContentView.swift in Sources */,
8DCD5BBF7C77AD494D4296F4 /* OutputAction.swift in Sources */,
1BDB0D69E351CA5C221B94EA /* SettingsView.swift in Sources */,
34615E753CFF584C1561040F /* SettingsWindowController.swift in Sources */,
1D8FC537A5AC329C446C4218 /* SnippingToolApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A8AD90B7DF485B4FE48C4DA2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
AAF2F2D5D59F12E7F040E326 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "-";
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = Generated/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
PRODUCT_NAME = SnipMC;
SDKROOT = macosx;
SWIFT_VERSION = 5.0;
};
name = Release;
};
BF982956FBC79F78D1D42A71 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
};
name = Release;
};
EB9AB62157886891881D482F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "-";
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = Generated/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
PRODUCT_NAME = SnipMC;
SDKROOT = macosx;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7F1515B2EDB59F08F4421C1E /* Build configuration list for PBXProject "SnipMC" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A8AD90B7DF485B4FE48C4DA2 /* Debug */,
BF982956FBC79F78D1D42A71 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
99C7A42A282B29A64B2B7688 /* Build configuration list for PBXNativeTarget "SnipMC" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EB9AB62157886891881D482F /* Debug */,
AAF2F2D5D59F12E7F040E326 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = E5D0ACF0D9F5E10B2B3D9BB2 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
BuildableName = "SnipMC.app"
BlueprintName = "SnipMC"
ReferencedContainer = "container:SnipMC.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
BuildableName = "SnipMC.app"
BlueprintName = "SnipMC"
ReferencedContainer = "container:SnipMC.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
BuildableName = "SnipMC.app"
BlueprintName = "SnipMC"
ReferencedContainer = "container:SnipMC.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
BuildableName = "SnipMC.app"
BlueprintName = "SnipMC"
ReferencedContainer = "container:SnipMC.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+180
View File
@@ -0,0 +1,180 @@
import AppKit
import SwiftUI
final class AboutWindowController: NSWindowController, NSWindowDelegate {
static let shared = AboutWindowController()
private convenience init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 520),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.title = "SnipMC"
window.isReleasedWhenClosed = false
window.center()
self.init(window: window)
window.delegate = self
}
func show() {
window?.contentView = NSHostingView(rootView: AboutAndHelpView())
NSApplication.shared.setActivationPolicy(.regular)
NSApplication.shared.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
}
func windowWillClose(_ notification: Notification) {
let otherVisible = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
if otherVisible.isEmpty {
NSApplication.shared.setActivationPolicy(.accessory)
}
}
}
struct AboutAndHelpView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
AboutTab()
.tabItem { Label("Info", systemImage: "info.circle") }
.tag(0)
HelpTab()
.tabItem { Label("Dokumentation", systemImage: "book") }
.tag(1)
}
.frame(width: 480, height: 500)
}
}
struct AboutTab: View {
private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
var body: some View {
VStack(spacing: 16) {
Spacer()
if let appIcon = NSImage(named: NSImage.applicationIconName) {
Image(nsImage: appIcon)
.resizable()
.frame(width: 80, height: 80)
}
Text("SnipMC")
.font(.title.bold())
Text("Version \(version) (\(build))")
.foregroundStyle(.secondary)
.font(.callout)
Text("Freeware")
.font(.callout)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(.quaternary)
.clipShape(Capsule())
VStack(spacing: 4) {
Text("Marco Wende \u{2014} Wende.IT")
.font(.body)
Link("wende.it", destination: URL(string: "https://wende.it")!)
.font(.body)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct HelpTab: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
helpSection("Aufnahme-Modi") {
helpRow("Ganzer Bildschirm", "Nimmt den gesamten Bildschirm auf")
helpRow("Fenster", "Klicke auf ein Fenster, um es aufzunehmen")
helpRow("Bereich", "Ziehe einen Rahmen um den gew\u{00fc}nschten Bereich")
}
helpSection("Bildeditor") {
helpRow("Pfeil", "Linie mit Pfeilspitze zeichnen")
helpRow("Rechteck", "Rahmen um einen Bereich zeichnen")
helpRow("Ellipse", "Kreis oder Oval zeichnen")
helpRow("Text", "Klicke auf eine Stelle und tippe Text ein")
helpRow("\u{2318}Z / \u{2318}\u{21e7}Z", "R\u{00fc}ckg\u{00e4}ngig / Wiederholen")
helpRow("\u{232b} / Entf", "Ausgew\u{00e4}hlte Annotation l\u{00f6}schen")
}
helpSection("URL-Schema") {
Text("Zum Ausl\u{00f6}sen per Logitech Options+, Kurzb efehle, Stream Deck o.\u{202f}\u{00e4}.")
.font(.callout)
.foregroundStyle(.secondary)
.padding(.bottom, 4)
urlRow("snipmc://fullscreen", "Vollbild-Screenshot")
urlRow("snipmc://window", "Fenster-Screenshot")
urlRow("snipmc://region", "Bereich-Screenshot")
urlRow("snipmc://region?edit=true", "Bereich + Editor")
urlRow("snipmc://fullscreen?edit=true", "Vollbild + Editor")
urlRow("snipmc://edit", "Bild aus Datei bearbeiten")
urlRow("snipmc://editlast", "Letzten Screenshot bearbeiten")
urlRow("snipmc://clipboard", "Zwischenablage bearbeiten")
}
helpSection("Einrichtung in Logitech Options+") {
VStack(alignment: .leading, spacing: 4) {
Text("1. Logi Options+ \u{00f6}ffnen und Ger\u{00e4}t w\u{00e4}hlen")
Text("2. Taste zuweisen: Aktion = URL \u{00f6}ffnen")
Text("3. URL eintragen, z.\u{202f}B.:")
Text(" snipmc://region?edit=true")
.font(.system(.body, design: .monospaced))
.foregroundStyle(.secondary)
}
.font(.callout)
}
helpSection("Hinweise") {
VStack(alignment: .leading, spacing: 4) {
Text("\u{2022} Berechtigung Bildschirmaufnahme erforderlich (Systemeinstellungen \u{2192} Datenschutz & Sicherheit)")
Text("\u{2022} Speicherort: ~/Bilder/Screenshots (\u{00e4}nderbar in Einstellungen)")
Text("\u{2022} Tastenk\u{00fc}rzel frei konfigurierbar in den Einstellungen")
}
.font(.callout)
}
}
.padding(20)
}
}
private func helpSection(_ title: String, @ViewBuilder content: () -> some View) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
content()
}
}
private func helpRow(_ label: String, _ description: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text(label)
.fontWeight(.medium)
.frame(width: 120, alignment: .leading)
Text(description)
.foregroundStyle(.secondary)
}
.font(.callout)
}
private func urlRow(_ url: String, _ description: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text(url)
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
.frame(width: 260, alignment: .leading)
Text(description)
.font(.callout)
.foregroundStyle(.secondary)
}
}
}
+120 -22
View File
@@ -36,6 +36,9 @@ final class AppCoordinator: ObservableObject {
didSet { persistAndRegister(mode: .region, combo: hotKeyRegion) } didSet { persistAndRegister(mode: .region, combo: hotKeyRegion) }
} }
@Published var lastCapturedURL: URL?
private var editorController: EditorWindowController?
init() { init() {
let defaults = UserDefaults.standard let defaults = UserDefaults.standard
@@ -94,42 +97,62 @@ final class AppCoordinator: ObservableObject {
// MARK: - External triggers (URL scheme) // MARK: - External triggers (URL scheme)
/// Handles `snippingtool://<mode>` URLs so external tools (Logitech
/// Options, Shortcuts, Stream Deck, `open` from the command line, …)
/// can trigger a specific capture mode instead of just launching the app.
/// Accepts the mode either as the host (`snippingtool://region`) or as
/// a query item (`snippingtool://capture?mode=region`).
func handle(url: URL) { func handle(url: URL) {
guard url.scheme?.caseInsensitiveCompare("snippingtool") == .orderedSame else { return } guard url.scheme?.caseInsensitiveCompare("snipmc") == .orderedSame else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let queryItems = components?.queryItems ?? []
let modeString = url.host?.lowercased() let modeString = url.host?.lowercased()
?? URLComponents(url: url, resolvingAgainstBaseURL: false)? ?? queryItems.first(where: { $0.name.caseInsensitiveCompare("mode") == .orderedSame })?
.queryItems? .value?.lowercased()
.first(where: { $0.name.caseInsensitiveCompare("mode") == .orderedSame })?
.value?
.lowercased()
let wantsEditor = queryItems.first(where: { $0.name.caseInsensitiveCompare("edit") == .orderedSame })?
.value?.lowercased() ?? "false"
let openEditor = ["true", "1", "yes"].contains(wantsEditor)
if modeString == "edit" {
openEditorForFile()
return
}
if modeString == "editlast" || modeString == "edit-last" || modeString == "lastedit" {
openEditorForLastCapture()
return
}
if modeString == "paste" || modeString == "clipboard" || modeString == "editclipboard" {
openEditorFromClipboard()
return
}
let mode: CaptureMode?
switch modeString { switch modeString {
case "fullscreen", "full", "full-screen", "screen": case "fullscreen", "full", "full-screen", "screen":
capture(mode: .fullScreen) mode = .fullScreen
case "window": case "window":
capture(mode: .window) mode = .window
case "region", "area", "selection": case "region", "area", "selection":
capture(mode: .region) mode = .region
default: default:
NSLog("SnippingTool: unrecognized URL trigger \(url.absoluteString)") mode = nil
NSLog("SnipMC: unrecognized URL trigger \(url.absoluteString)")
}
if let mode {
capture(mode: mode, openEditor: openEditor)
} }
} }
// MARK: - Capture // MARK: - Capture
func capture(mode: CaptureMode) { func capture(mode: CaptureMode, openEditor: Bool = false) {
let saves = outputAction.savesToFile let saves = outputAction.savesToFile
let copies = outputAction.copiesToClipboard let copies = outputAction.copiesToClipboard
let ext = imageFormat.fileExtension let ext = imageFormat.fileExtension
let targetURL: URL let targetURL: URL
if saves { if saves || openEditor {
try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true)
targetURL = saveFolder.appendingPathComponent(Self.filename(ext: ext)) targetURL = saveFolder.appendingPathComponent(Self.filename(ext: ext))
} else { } else {
@@ -145,20 +168,24 @@ final class AppCoordinator: ObservableObject {
process.arguments = arguments process.arguments = arguments
process.terminationHandler = { [weak self] _ in process.terminationHandler = { [weak self] _ in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.handleCaptured(at: targetURL, saves: saves, copies: copies) self?.handleCaptured(at: targetURL, saves: saves, copies: copies, openEditor: openEditor)
} }
} }
do { do {
try process.run() try process.run()
} catch { } catch {
NSLog("SnippingTool: failed to launch screencapture: \(error)") NSLog("SnipMC: failed to launch screencapture: \(error)")
} }
} }
private func handleCaptured(at url: URL, saves: Bool, copies: Bool) { private func handleCaptured(at url: URL, saves: Bool, copies: Bool, openEditor: Bool = false) {
guard FileManager.default.fileExists(atPath: url.path) else { guard FileManager.default.fileExists(atPath: url.path) else { return }
// User cancelled the interactive capture (Esc).
lastCapturedURL = url
if openEditor, let image = NSImage(contentsOf: url) {
self.openEditor(image: image, sourceURL: url)
return return
} }
@@ -173,6 +200,77 @@ final class AppCoordinator: ObservableObject {
} }
} }
// MARK: - Editor
func openEditorForLastCapture() {
guard let url = lastCapturedURL,
FileManager.default.fileExists(atPath: url.path),
let image = NSImage(contentsOf: url) else { return }
DispatchQueue.main.async {
self.openEditor(image: image, sourceURL: url)
}
}
func openEditorFromClipboard() {
guard let pasteboard = NSPasteboard.general.readObjects(forClasses: [NSImage.self], options: nil),
let image = pasteboard.first as? NSImage else { return }
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(Self.filename(ext: imageFormat.fileExtension))
DispatchQueue.main.async {
self.openEditor(image: image, sourceURL: tempURL)
}
}
func openEditorForFile() {
DispatchQueue.main.async {
NSApplication.shared.setActivationPolicy(.regular)
NSApplication.shared.activate(ignoringOtherApps: true)
let panel = NSOpenPanel()
panel.allowedContentTypes = [.png, .jpeg, .tiff, .bmp]
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
if panel.runModal() == .OK, let url = panel.url,
let image = NSImage(contentsOf: url) {
self.openEditor(image: image, sourceURL: url)
} else {
NSApplication.shared.setActivationPolicy(.accessory)
}
}
}
private func openEditor(image: NSImage, sourceURL: URL) {
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
guard let self, let annotatedImage else { return }
self.saveAnnotatedImage(annotatedImage, replacingFileAt: sourceURL)
}
editorController = controller
controller.showEditor()
}
private func saveAnnotatedImage(_ image: NSImage, replacingFileAt url: URL) {
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData) else { return }
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
? [.compressionFactor: 0.9] : [:]
guard let data = bitmap.representation(using: fileType, properties: properties) else { return }
do {
try data.write(to: url, options: .atomic)
} catch {
NSLog("SnipMC: failed to save annotated image: \(error)")
}
if outputAction.copiesToClipboard {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([image])
}
}
private static func filename(ext: String) -> String { private static func filename(ext: String) -> String {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd 'um' HH.mm.ss" formatter.dateFormat = "yyyy-MM-dd 'um' HH.mm.ss"
+310
View File
@@ -0,0 +1,310 @@
import AppKit
import Foundation
final class AnnotationCanvasView: NSView {
var baseImage: NSImage? { didSet { needsDisplay = true } }
var document: AnnotationDocument? { didSet { needsDisplay = true } }
var onDocumentChanged: (() -> Void)?
private enum Interaction {
case idle
case creating(AnnotationKind, startPoint: CGPoint)
case dragging(UUID, lastPoint: CGPoint)
}
private var interaction: Interaction = .idle
private var inProgressItem: AnnotationItem?
private var textField: NSTextField?
override var isFlipped: Bool { true }
override var acceptsFirstResponder: Bool { true }
// MARK: - Coordinate transform
private var imageRect: CGRect {
guard let image = baseImage else { return bounds }
let imageSize = image.size
let scale = min(bounds.width / imageSize.width, bounds.height / imageSize.height)
let scaledSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
return CGRect(
x: (bounds.width - scaledSize.width) / 2,
y: (bounds.height - scaledSize.height) / 2,
width: scaledSize.width,
height: scaledSize.height
)
}
private var viewToImageScale: CGFloat {
guard let image = baseImage else { return 1 }
return image.size.width / imageRect.width
}
private func viewToImage(_ point: CGPoint) -> CGPoint {
let rect = imageRect
let scale = viewToImageScale
return CGPoint(
x: (point.x - rect.origin.x) * scale,
y: (point.y - rect.origin.y) * scale
)
}
private func imageToView(_ point: CGPoint) -> CGPoint {
let rect = imageRect
let scale = viewToImageScale
return CGPoint(
x: point.x / scale + rect.origin.x,
y: point.y / scale + rect.origin.y
)
}
private func imageRectToView(_ r: CGRect) -> CGRect {
let origin = imageToView(r.origin)
let scale = viewToImageScale
return CGRect(x: origin.x, y: origin.y,
width: r.width / scale, height: r.height / scale)
}
// MARK: - Drawing
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
let bgColor: CGColor = NSColor.windowBackgroundColor.cgColor
context.setFillColor(bgColor)
context.fill(bounds)
guard let image = baseImage else { return }
let imgRect = imageRect
NSGraphicsContext.current?.imageInterpolation = .high
image.draw(in: imgRect, from: .zero, operation: .sourceOver, fraction: 1, respectFlipped: true, hints: nil)
context.saveGState()
context.translateBy(x: imgRect.origin.x, y: imgRect.origin.y)
let scale = 1.0 / viewToImageScale
context.scaleBy(x: scale, y: scale)
guard let doc = document else {
context.restoreGState()
return
}
for item in doc.items {
AnnotationRenderer.draw(item, in: context)
if item.id == doc.selectedItemID {
drawSelectionIndicator(for: item, in: context)
}
}
if let inProgress = inProgressItem {
AnnotationRenderer.draw(inProgress, in: context)
}
context.restoreGState()
}
private func drawSelectionIndicator(for item: AnnotationItem, in context: CGContext) {
let handleRect: CGRect
if item.kind == .arrow {
let start = item.rect.origin
let end = CGPoint(x: item.rect.maxX, y: item.rect.maxY)
handleRect = CGRect(x: min(start.x, end.x), y: min(start.y, end.y),
width: abs(end.x - start.x), height: abs(end.y - start.y))
} else if item.kind == .text {
guard let text = item.text, !text.isEmpty else { return }
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
let size = (text as NSString).size(withAttributes: [.font: font])
handleRect = CGRect(origin: item.rect.origin, size: size)
} else {
handleRect = item.rect
}
context.saveGState()
let dashPattern: [CGFloat] = [6, 4]
context.setLineDash(phase: 0, lengths: dashPattern)
context.setStrokeColor(NSColor.controlAccentColor.cgColor)
context.setLineWidth(1.5)
context.stroke(handleRect.insetBy(dx: -4, dy: -4))
context.restoreGState()
}
// MARK: - Mouse events
override func mouseDown(with event: NSEvent) {
commitTextField()
window?.makeFirstResponder(self)
let viewPoint = convert(event.locationInWindow, from: nil)
let imgPoint = viewToImage(viewPoint)
guard let doc = document else { return }
// Hit-test existing items (reverse order = topmost first)
for item in doc.items.reversed() {
if AnnotationRenderer.hitTest(point: imgPoint, item: item) {
doc.selectedItemID = item.id
interaction = .dragging(item.id, lastPoint: imgPoint)
needsDisplay = true
return
}
}
doc.selectedItemID = nil
interaction = .creating(doc.currentTool, startPoint: imgPoint)
needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
let viewPoint = convert(event.locationInWindow, from: nil)
let imgPoint = viewToImage(viewPoint)
guard let doc = document else { return }
switch interaction {
case .creating(let kind, let start):
if kind == .text { return }
let rect = CGRect(x: start.x, y: start.y,
width: imgPoint.x - start.x,
height: imgPoint.y - start.y)
inProgressItem = AnnotationItem(
kind: kind,
rect: rect,
color: doc.currentColor,
lineWidth: doc.currentLineWidth
)
needsDisplay = true
case .dragging(let id, let lastPoint):
let delta = CGSize(width: imgPoint.x - lastPoint.x,
height: imgPoint.y - lastPoint.y)
doc.moveSelected(by: delta)
interaction = .dragging(id, lastPoint: imgPoint)
needsDisplay = true
case .idle:
break
}
}
override func mouseUp(with event: NSEvent) {
let viewPoint = convert(event.locationInWindow, from: nil)
let imgPoint = viewToImage(viewPoint)
guard let doc = document else { return }
switch interaction {
case .creating(let kind, let start):
if kind == .text {
showTextField(at: viewPoint, imagePoint: imgPoint)
} else if let item = inProgressItem {
let dx = abs(item.rect.width)
let dy = abs(item.rect.height)
if dx > 4 || dy > 4 {
let normalized = normalizedRect(from: start, to: imgPoint, kind: kind)
var finalItem = item
finalItem.rect = normalized
doc.addItem(finalItem)
onDocumentChanged?()
}
}
inProgressItem = nil
case .dragging(_, _):
doc.pushUndo()
onDocumentChanged?()
case .idle:
break
}
interaction = .idle
needsDisplay = true
}
private func normalizedRect(from start: CGPoint, to end: CGPoint, kind: AnnotationKind) -> CGRect {
if kind == .arrow {
return CGRect(x: start.x, y: start.y,
width: end.x - start.x, height: end.y - start.y)
}
return CGRect(
x: min(start.x, end.x), y: min(start.y, end.y),
width: abs(end.x - start.x), height: abs(end.y - start.y)
)
}
// MARK: - Text editing
private func showTextField(at viewPoint: CGPoint, imagePoint: CGPoint) {
let field = NSTextField(frame: NSRect(x: viewPoint.x, y: viewPoint.y, width: 200, height: 30))
field.font = .systemFont(ofSize: 16, weight: .semibold)
field.textColor = document?.currentColor ?? .systemRed
field.backgroundColor = .white.withAlphaComponent(0.9)
field.isBordered = true
field.focusRingType = .exterior
field.placeholderString = "Text eingeben…"
field.target = self
field.action = #selector(textFieldCommitted(_:))
field.tag = Int(imagePoint.x) // store image coordinates in tag
addSubview(field)
window?.makeFirstResponder(field)
// Store image Y in field's identifier
field.identifier = NSUserInterfaceItemIdentifier("\(imagePoint.x),\(imagePoint.y)")
textField = field
}
@objc private func textFieldCommitted(_ sender: NSTextField) {
commitTextField()
}
private func commitTextField() {
guard let field = textField, let doc = document else { return }
let text = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
if !text.isEmpty, let identifier = field.identifier?.rawValue {
let parts = identifier.split(separator: ",")
if parts.count == 2,
let x = Double(parts[0]),
let y = Double(parts[1]) {
let item = AnnotationItem(
kind: .text,
rect: CGRect(x: x, y: y, width: 0, height: 0),
color: doc.currentColor,
lineWidth: doc.currentLineWidth,
text: text,
fontSize: 24 * viewToImageScale
)
doc.addItem(item)
onDocumentChanged?()
needsDisplay = true
}
}
field.removeFromSuperview()
textField = nil
}
// MARK: - Keyboard
override func keyDown(with event: NSEvent) {
if event.modifierFlags.contains(.command) {
if event.charactersIgnoringModifiers == "z" {
if event.modifierFlags.contains(.shift) {
document?.redo()
} else {
document?.undo()
}
needsDisplay = true
onDocumentChanged?()
return
}
}
if event.keyCode == 51 || event.keyCode == 117 { // Backspace or Delete
document?.deleteSelected()
needsDisplay = true
onDocumentChanged?()
return
}
super.keyDown(with: event)
}
}
+56
View File
@@ -0,0 +1,56 @@
import AppKit
import Foundation
@Observable
final class AnnotationDocument {
var items: [AnnotationItem] = []
var selectedItemID: UUID?
var currentTool: AnnotationKind = .arrow
var currentColor: NSColor = .systemRed
var currentLineWidth: CGFloat = 3
private var undoStack: [[AnnotationItem]] = []
private var redoStack: [[AnnotationItem]] = []
var canUndo: Bool { !undoStack.isEmpty }
var canRedo: Bool { !redoStack.isEmpty }
func pushUndo() {
undoStack.append(items)
redoStack.removeAll()
}
func undo() {
guard let previous = undoStack.popLast() else { return }
redoStack.append(items)
items = previous
selectedItemID = nil
}
func redo() {
guard let next = redoStack.popLast() else { return }
undoStack.append(items)
items = next
selectedItemID = nil
}
func addItem(_ item: AnnotationItem) {
pushUndo()
items.append(item)
selectedItemID = item.id
}
func deleteSelected() {
guard let id = selectedItemID else { return }
pushUndo()
items.removeAll { $0.id == id }
selectedItemID = nil
}
func moveSelected(by delta: CGSize) {
guard let id = selectedItemID,
let index = items.firstIndex(where: { $0.id == id }) else { return }
items[index].rect.origin.x += delta.width
items[index].rect.origin.y += delta.height
}
}
+36
View File
@@ -0,0 +1,36 @@
import AppKit
import Foundation
enum AnnotationKind: String, CaseIterable, Codable {
case arrow
case rectangle
case ellipse
case text
}
struct AnnotationItem: Identifiable {
let id: UUID
var kind: AnnotationKind
var rect: CGRect
var color: NSColor
var lineWidth: CGFloat
var text: String?
var fontSize: CGFloat
init(
kind: AnnotationKind,
rect: CGRect,
color: NSColor = .systemRed,
lineWidth: CGFloat = 3,
text: String? = nil,
fontSize: CGFloat = 24
) {
self.id = UUID()
self.kind = kind
self.rect = rect
self.color = color
self.lineWidth = lineWidth
self.text = text
self.fontSize = fontSize
}
}
+139
View File
@@ -0,0 +1,139 @@
import AppKit
import Foundation
enum AnnotationRenderer {
static func draw(_ item: AnnotationItem, in context: CGContext) {
context.saveGState()
context.setStrokeColor(item.color.cgColor)
context.setFillColor(item.color.cgColor)
context.setLineWidth(item.lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
switch item.kind {
case .arrow:
drawArrow(item, in: context)
case .rectangle:
drawRectangle(item, in: context)
case .ellipse:
drawEllipse(item, in: context)
case .text:
drawText(item)
}
context.restoreGState()
}
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
let size = image.size
let result = NSImage(size: size)
result.lockFocus()
image.draw(in: NSRect(origin: .zero, size: size))
if let context = NSGraphicsContext.current?.cgContext {
for item in annotations {
draw(item, in: context)
}
}
result.unlockFocus()
return result
}
// MARK: - Shape drawing
private static func drawArrow(_ item: AnnotationItem, in context: CGContext) {
let start = item.rect.origin
let end = CGPoint(x: item.rect.maxX, y: item.rect.maxY)
context.move(to: start)
context.addLine(to: end)
context.strokePath()
let angle = atan2(end.y - start.y, end.x - start.x)
let headLength = max(item.lineWidth * 4, 14)
let headAngle: CGFloat = .pi / 6
let left = CGPoint(
x: end.x - headLength * cos(angle - headAngle),
y: end.y - headLength * sin(angle - headAngle)
)
let right = CGPoint(
x: end.x - headLength * cos(angle + headAngle),
y: end.y - headLength * sin(angle + headAngle)
)
context.move(to: end)
context.addLine(to: left)
context.addLine(to: right)
context.closePath()
context.fillPath()
}
private static func drawRectangle(_ item: AnnotationItem, in context: CGContext) {
context.stroke(item.rect, width: item.lineWidth)
}
private static func drawEllipse(_ item: AnnotationItem, in context: CGContext) {
context.strokeEllipse(in: item.rect)
}
private static func drawText(_ item: AnnotationItem) {
guard let text = item.text, !text.isEmpty else { return }
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: item.color,
]
let string = NSAttributedString(string: text, attributes: attributes)
string.draw(at: item.rect.origin)
}
// MARK: - Hit testing
static func hitTest(point: CGPoint, item: AnnotationItem, tolerance: CGFloat = 6) -> Bool {
switch item.kind {
case .arrow:
return distanceToLine(point: point,
from: item.rect.origin,
to: CGPoint(x: item.rect.maxX, y: item.rect.maxY)) < tolerance + item.lineWidth
case .rectangle:
let outer = item.rect.insetBy(dx: -(tolerance + item.lineWidth),
dy: -(tolerance + item.lineWidth))
let inner = item.rect.insetBy(dx: tolerance + item.lineWidth,
dy: tolerance + item.lineWidth)
return outer.contains(point) && !inner.contains(point)
case .ellipse:
let center = CGPoint(x: item.rect.midX, y: item.rect.midY)
let rx = item.rect.width / 2
let ry = item.rect.height / 2
guard rx > 0, ry > 0 else { return false }
let dx = (point.x - center.x) / rx
let dy = (point.y - center.y) / ry
let dist = sqrt(dx * dx + dy * dy)
return abs(dist - 1.0) < (tolerance + item.lineWidth) / min(rx, ry)
case .text:
guard let text = item.text, !text.isEmpty else { return false }
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
let size = (text as NSString).size(withAttributes: [.font: font])
let textRect = CGRect(origin: item.rect.origin, size: size)
return textRect.insetBy(dx: -tolerance, dy: -tolerance).contains(point)
}
}
private static func distanceToLine(point: CGPoint, from a: CGPoint, to b: CGPoint) -> CGFloat {
let dx = b.x - a.x
let dy = b.y - a.y
let lengthSq = dx * dx + dy * dy
guard lengthSq > 0 else { return hypot(point.x - a.x, point.y - a.y) }
var t = ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSq
t = max(0, min(1, t))
let projX = a.x + t * dx
let projY = a.y + t * dy
return hypot(point.x - projX, point.y - projY)
}
}
+115
View File
@@ -0,0 +1,115 @@
import SwiftUI
struct EditorToolbarView: View {
@Bindable var document: AnnotationDocument
var onSave: () -> Void
var onCopyToClipboard: () -> Void
var onCancel: () -> Void
var body: some View {
HStack(spacing: 12) {
toolButtons
Divider().frame(height: 24)
colorPicker
lineWidthPicker
Divider().frame(height: 24)
undoRedoButtons
Spacer()
actionButtons
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(.bar)
}
private var toolButtons: some View {
HStack(spacing: 4) {
toolButton(.arrow, icon: "arrow.up.right", label: "Pfeil")
toolButton(.rectangle, icon: "rectangle", label: "Rechteck")
toolButton(.ellipse, icon: "circle", label: "Ellipse")
toolButton(.text, icon: "textformat", label: "Text")
}
}
private func toolButton(_ kind: AnnotationKind, icon: String, label: String) -> some View {
Button {
document.currentTool = kind
} label: {
Image(systemName: icon)
.frame(width: 28, height: 28)
}
.buttonStyle(.bordered)
.tint(document.currentTool == kind ? .accentColor : nil)
.help(label)
}
private var colorPicker: some View {
HStack(spacing: 4) {
ForEach(presetColors, id: \.self) { color in
Circle()
.fill(Color(nsColor: color))
.frame(width: 20, height: 20)
.overlay {
if document.currentColor == color {
Circle().strokeBorder(.white, lineWidth: 2)
}
}
.shadow(radius: document.currentColor == color ? 2 : 0)
.onTapGesture { document.currentColor = color }
}
ColorPicker("", selection: Binding(
get: { Color(nsColor: document.currentColor) },
set: { document.currentColor = NSColor($0) }
))
.labelsHidden()
.frame(width: 28)
}
}
private var presetColors: [NSColor] {
[.systemRed, .systemBlue, .systemGreen, .systemYellow, .black, .white]
}
private var lineWidthPicker: some View {
Picker("", selection: $document.currentLineWidth) {
Text("Dünn").tag(CGFloat(2))
Text("Mittel").tag(CGFloat(3))
Text("Dick").tag(CGFloat(5))
}
.pickerStyle(.segmented)
.frame(width: 160)
.labelsHidden()
}
private var undoRedoButtons: some View {
HStack(spacing: 4) {
Button {
document.undo()
} label: {
Image(systemName: "arrow.uturn.backward")
}
.disabled(!document.canUndo)
.help("Widerrufen (⌘Z)")
Button {
document.redo()
} label: {
Image(systemName: "arrow.uturn.forward")
}
.disabled(!document.canRedo)
.help("Wiederholen (⌘⇧Z)")
}
}
private var actionButtons: some View {
HStack(spacing: 8) {
Button("Abbrechen", role: .cancel) { onCancel() }
.keyboardShortcut(.escape)
Button("In Zwischenablage") { onCopyToClipboard() }
Button("Speichern") { onSave() }
.keyboardShortcut("s")
.buttonStyle(.borderedProminent)
}
}
}
+113
View File
@@ -0,0 +1,113 @@
import AppKit
import SwiftUI
final class EditorWindowController: NSWindowController, NSWindowDelegate {
private let annotationDoc = AnnotationDocument()
private let baseImage: NSImage
private let sourceURL: URL?
private let completion: (NSImage?) -> Void
private let canvasView = AnnotationCanvasView()
init(image: NSImage, sourceURL: URL?, completion: @escaping (NSImage?) -> Void) {
self.baseImage = image
self.sourceURL = sourceURL
self.completion = completion
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 800)
let maxWidth = screenFrame.width * 0.8
let maxHeight = screenFrame.height * 0.8
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
let windowWidth = max(image.size.width * scale, 600)
let windowHeight = max(image.size.height * scale, 400) + 52 // toolbar height
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Bildeditor"
window.isReleasedWhenClosed = false
window.center()
window.minSize = NSSize(width: 500, height: 400)
super.init(window: window)
window.delegate = self
canvasView.baseImage = image
canvasView.document = annotationDoc
canvasView.onDocumentChanged = { [weak self] in
self?.canvasView.needsDisplay = true
}
canvasView.autoresizingMask = [.width, .height]
let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
document: annotationDoc,
onSave: { [weak self] in self?.save() },
onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
onCancel: { [weak self] in self?.cancel() }
))
toolbarHosting.translatesAutoresizingMaskIntoConstraints = true
let container = NSView(frame: window.contentRect(forFrameRect: window.frame))
container.autoresizingMask = [.width, .height]
toolbarHosting.frame = NSRect(x: 0, y: container.bounds.height - 52,
width: container.bounds.width, height: 52)
toolbarHosting.autoresizingMask = [.width, .minYMargin]
canvasView.frame = NSRect(x: 0, y: 0,
width: container.bounds.width,
height: container.bounds.height - 52)
canvasView.autoresizingMask = [.width, .height]
container.addSubview(toolbarHosting)
container.addSubview(canvasView)
window.contentView = container
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
func showEditor() {
NSApplication.shared.setActivationPolicy(.regular)
NSApplication.shared.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
window?.makeFirstResponder(canvasView)
}
// MARK: - Actions
private func renderFinal() -> NSImage {
AnnotationRenderer.render(annotations: annotationDoc.items, onto: baseImage)
}
private func save() {
let image = renderFinal()
completion(image)
close()
}
private func copyToClipboard() {
let image = renderFinal()
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([image])
close()
}
private func cancel() {
completion(nil)
close()
}
// MARK: - NSWindowDelegate
func windowWillClose(_ notification: Notification) {
let visibleWindows = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
if visibleWindows.isEmpty {
NSApplication.shared.setActivationPolicy(.accessory)
}
}
}
+19
View File
@@ -11,11 +11,30 @@ struct MenuBarContentView: View {
Divider() Divider()
Button("Letzte Aufnahme bearbeiten…") {
coordinator.openEditorForLastCapture()
}
.disabled(coordinator.lastCapturedURL == nil)
Button("Aus Zwischenablage bearbeiten") {
coordinator.openEditorFromClipboard()
}
Button("Bild bearbeiten…") {
coordinator.openEditorForFile()
}
Divider()
Button("Einstellungen…") { Button("Einstellungen…") {
SettingsWindowController.shared.show(coordinator: coordinator) SettingsWindowController.shared.show(coordinator: coordinator)
} }
.keyboardShortcut(",") .keyboardShortcut(",")
Button("Über SnipMC…") {
AboutWindowController.shared.show()
}
Divider() Divider()
Button("Beenden") { Button("Beenden") {
+3 -1
View File
@@ -32,7 +32,9 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
} }
func windowWillClose(_ notification: Notification) { func windowWillClose(_ notification: Notification) {
// Menu bar-only apps should not keep a Dock icon after the window closes. let otherVisible = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
if otherVisible.isEmpty {
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
} }
}
} }
+2 -2
View File
@@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
@main @main
struct SnippingToolApp: App { struct SnipMCApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene { var body: some Scene {
MenuBarExtra("Snipping Tool", systemImage: "camera.viewfinder") { MenuBarExtra("SnipMC", systemImage: "camera.viewfinder") {
MenuBarContentView() MenuBarContentView()
.environmentObject(appDelegate.coordinator) .environmentObject(appDelegate.coordinator)
} }
+12 -12
View File
@@ -1,11 +1,11 @@
name: SnippingTool name: SnipMC
options: options:
bundleIdPrefix: it.wende bundleIdPrefix: it.wende
deploymentTarget: deploymentTarget:
macOS: "14.0" macOS: "14.0"
createIntermediateGroups: true createIntermediateGroups: true
targets: targets:
SnippingTool: SnipMC:
type: application type: application
platform: macOS platform: macOS
deploymentTarget: "14.0" deploymentTarget: "14.0"
@@ -14,29 +14,29 @@ targets:
info: info:
path: Generated/Info.plist path: Generated/Info.plist
properties: properties:
CFBundleDisplayName: Snipping Tool CFBundleDisplayName: SnipMC
CFBundleName: Snipping Tool CFBundleName: SnipMC
LSUIElement: true LSUIElement: true
NSHumanReadableCopyright: "Copyright © 2026" NSHumanReadableCopyright: "Copyright © 2026"
CFBundleURLTypes: CFBundleURLTypes:
- CFBundleURLName: it.wende.snippingtool - CFBundleURLName: it.wende.snipmc
CFBundleURLSchemes: CFBundleURLSchemes:
- snippingtool - snipmc
settings: settings:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snippingtool PRODUCT_BUNDLE_IDENTIFIER: it.wende.snipmc
PRODUCT_NAME: Snipping Tool PRODUCT_NAME: SnipMC
MARKETING_VERSION: "1.0" MARKETING_VERSION: "1.0.1"
CURRENT_PROJECT_VERSION: "1" CURRENT_PROJECT_VERSION: "2"
SWIFT_VERSION: "5.0" SWIFT_VERSION: "5.0"
MACOSX_DEPLOYMENT_TARGET: "14.0" MACOSX_DEPLOYMENT_TARGET: "14.0"
ENABLE_HARDENED_RUNTIME: false ENABLE_HARDENED_RUNTIME: false
CODE_SIGN_IDENTITY: "-" CODE_SIGN_IDENTITY: "-"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
schemes: schemes:
SnippingTool: SnipMC:
build: build:
targets: targets:
SnippingTool: all SnipMC: all
run: run:
config: Debug config: Debug