Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
188b952be7 | ||
|
|
8d6c2213ad | ||
|
|
519706dd41 | ||
|
|
0fce3e6d28 | ||
|
|
4267650739 | ||
|
|
052663b356 | ||
|
|
827af0b239 |
@@ -0,0 +1,119 @@
|
|||||||
|
# SnipMC
|
||||||
|
|
||||||
|
**macOS Menu Bar Screenshot Tool mit integriertem Bildeditor**
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
SnipMC ist ein schlankes Screenshot-Tool für macOS, das in der Menüleiste lebt. Es bietet drei Aufnahme-Modi, einen integrierten Bildeditor für Annotationen und ein URL-Schema zur Steuerung über externe Tools wie Logitech Options+, Kurzbefehle oder Stream Deck.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Screenshot-Modi**: Ganzer Bildschirm, Fenster oder frei wählbarer Bereich
|
||||||
|
- **Bildeditor**: Pfeile, Rechtecke, Ellipsen und Text-Annotationen
|
||||||
|
- **Flexibler Zugriff**: Letzte Aufnahme bearbeiten, Bild aus Zwischenablage, Datei öffnen
|
||||||
|
- **URL-Schema** (`snipmc://`): Externe Steuerung per Tastenkürzel, Stream Deck etc.
|
||||||
|
- **Einstellungen**: Speicherort, Bildformat (PNG/JPG), konfigurierbare Tastenkürzel
|
||||||
|
- **Menüleisten-App**: Kein Dock-Icon, läuft dezent im Hintergrund
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. `SnipMC.dmg` aus dem [aktuellen Release](https://github.com/mwende/SnipMC/releases/latest) herunterladen
|
||||||
|
2. DMG öffnen und `SnipMC.app` nach `/Programme` ziehen
|
||||||
|
3. Gatekeeper-Sperre aufheben (da die App nicht mit einem Apple Developer Zertifikat signiert ist):
|
||||||
|
```bash
|
||||||
|
xattr -cr /Applications/SnipMC.app
|
||||||
|
```
|
||||||
|
4. App starten — das Kamera-Icon erscheint in der Menüleiste
|
||||||
|
5. Beim ersten Start die Berechtigung **Bildschirmaufnahme** erteilen:
|
||||||
|
*Systemeinstellungen → Datenschutz & Sicherheit → Bildschirmaufnahme*
|
||||||
|
|
||||||
|
## Screenshot-Modi
|
||||||
|
|
||||||
|
| Modus | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| Ganzer Bildschirm | Nimmt den gesamten Bildschirm auf |
|
||||||
|
| Fenster | Klicke auf ein Fenster, um es aufzunehmen |
|
||||||
|
| Bereich auswählen | Ziehe einen Rahmen um den gewünschten Bereich |
|
||||||
|
|
||||||
|
## Bildeditor
|
||||||
|
|
||||||
|
Der integrierte Editor öffnet sich nicht automatisch nach jeder Aufnahme, sondern nur auf expliziten Wunsch — über das Menü, per URL-Schema oder über die Tastenkombination.
|
||||||
|
|
||||||
|
### Werkzeuge
|
||||||
|
|
||||||
|
| Werkzeug | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| Pfeil | Linie mit Pfeilspitze zeichnen |
|
||||||
|
| Rechteck | Rahmen um einen Bereich zeichnen |
|
||||||
|
| Ellipse | Kreis oder Oval zeichnen |
|
||||||
|
| Text | Klicke auf eine Stelle und tippe Text ein |
|
||||||
|
|
||||||
|
### Tastenkürzel im Editor
|
||||||
|
|
||||||
|
| Kürzel | Aktion |
|
||||||
|
|---|---|
|
||||||
|
| ⌘Z | Rückgängig |
|
||||||
|
| ⌘⇧Z | Wiederholen |
|
||||||
|
| ⌫ / Entf | Ausgewählte Annotation löschen |
|
||||||
|
| ⌘S | Speichern |
|
||||||
|
|
||||||
|
## URL-Schema
|
||||||
|
|
||||||
|
SnipMC registriert das URL-Schema `snipmc://` und kann so von externen Tools gesteuert werden.
|
||||||
|
|
||||||
|
| URL | Aktion |
|
||||||
|
|---|---|
|
||||||
|
| `snipmc://fullscreen` | Vollbild-Screenshot |
|
||||||
|
| `snipmc://window` | Fenster-Screenshot |
|
||||||
|
| `snipmc://region` | Bereich-Screenshot |
|
||||||
|
| `snipmc://region?edit=true` | Bereich aufnehmen + Editor öffnen |
|
||||||
|
| `snipmc://fullscreen?edit=true` | Vollbild aufnehmen + Editor öffnen |
|
||||||
|
| `snipmc://edit` | Bild aus Datei im Editor öffnen |
|
||||||
|
| `snipmc://editlast` | Letzten Screenshot im Editor bearbeiten |
|
||||||
|
| `snipmc://clipboard` | Bild aus Zwischenablage bearbeiten |
|
||||||
|
|
||||||
|
### Einrichtung per macOS Kurzbefehle
|
||||||
|
|
||||||
|
1. Kurzbefehle-App öffnen
|
||||||
|
2. Neuer Kurzbefehl → Aktion **URL öffnen**
|
||||||
|
3. URL eintragen, z. B.: `snipmc://region`
|
||||||
|
4. Optional: Tastaturkurzbefehl zuweisen
|
||||||
|
|
||||||
|
### Einrichtung per Terminal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open snipmc://region
|
||||||
|
open "snipmc://region?edit=true"
|
||||||
|
open snipmc://editlast
|
||||||
|
```
|
||||||
|
|
||||||
|
## Einstellungen
|
||||||
|
|
||||||
|
- **Ausgabe**: Nur speichern, nur Zwischenablage, oder beides
|
||||||
|
- **Bildformat**: PNG oder JPG
|
||||||
|
- **Speicherort**: Standard `~/Bilder/Screenshots`, frei wählbar
|
||||||
|
- **Tastenkürzel**: Für jeden Aufnahme-Modus individuell konfigurierbar
|
||||||
|
|
||||||
|
## Systemvoraussetzungen
|
||||||
|
|
||||||
|
- macOS 14.0 (Sonoma) oder neuer
|
||||||
|
- Berechtigung **Bildschirmaufnahme** erforderlich
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voraussetzung: Xcode + XcodeGen
|
||||||
|
brew install xcodegen
|
||||||
|
|
||||||
|
# Projekt generieren und bauen
|
||||||
|
xcodegen generate
|
||||||
|
xcodebuild -project SnipMC.xcodeproj -scheme SnipMC -configuration Release build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Autor
|
||||||
|
|
||||||
|
**Marco Wende — [Wende.IT](https://wende.it)**
|
||||||
|
|
||||||
|
Freeware — kostenlos nutzbar.
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Generated/Info.plist;
|
INFOPLIST_FILE = Generated/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.0.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
||||||
PRODUCT_NAME = SnipMC;
|
PRODUCT_NAME = SnipMC;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
INFOPLIST_FILE = Generated/Info.plist;
|
INFOPLIST_FILE = Generated/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
MARKETING_VERSION = 1.0.1;
|
MARKETING_VERSION = 1.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
||||||
PRODUCT_NAME = SnipMC;
|
PRODUCT_NAME = SnipMC;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|||||||
@@ -122,18 +122,6 @@ struct HelpTab: View {
|
|||||||
urlRow("snipmc://clipboard", "Zwischenablage 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") {
|
helpSection("Hinweise") {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("\u{2022} Berechtigung Bildschirmaufnahme erforderlich (Systemeinstellungen \u{2192} Datenschutz & Sicherheit)")
|
Text("\u{2022} Berechtigung Bildschirmaufnahme erforderlich (Systemeinstellungen \u{2192} Datenschutz & Sicherheit)")
|
||||||
|
|||||||
@@ -242,24 +242,41 @@ final class AppCoordinator: ObservableObject {
|
|||||||
private func openEditor(image: NSImage, sourceURL: URL) {
|
private func openEditor(image: NSImage, sourceURL: URL) {
|
||||||
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
|
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
|
||||||
guard let self, let annotatedImage else { return }
|
guard let self, let annotatedImage else { return }
|
||||||
self.saveAnnotatedImage(annotatedImage, replacingFileAt: sourceURL)
|
let targetURL: URL
|
||||||
|
if sourceURL.path.hasPrefix(NSTemporaryDirectory()) {
|
||||||
|
try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true)
|
||||||
|
targetURL = saveFolder.appendingPathComponent(Self.filename(ext: imageFormat.fileExtension))
|
||||||
|
} else {
|
||||||
|
targetURL = sourceURL
|
||||||
|
}
|
||||||
|
self.saveAnnotatedImage(annotatedImage, to: targetURL)
|
||||||
}
|
}
|
||||||
editorController = controller
|
editorController = controller
|
||||||
controller.showEditor()
|
controller.showEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAnnotatedImage(_ image: NSImage, replacingFileAt url: URL) {
|
private func saveAnnotatedImage(_ image: NSImage, to url: URL) {
|
||||||
guard let tiffData = image.tiffRepresentation,
|
let bitmap: NSBitmapImageRep
|
||||||
let bitmap = NSBitmapImageRep(data: tiffData) else { return }
|
if let rep = image.representations.first as? NSBitmapImageRep {
|
||||||
|
bitmap = rep
|
||||||
|
} else if let cgRef = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
||||||
|
bitmap = NSBitmapImageRep(cgImage: cgRef)
|
||||||
|
} else {
|
||||||
|
NSLog("SnipMC: could not create bitmap for saving")
|
||||||
|
return
|
||||||
|
}
|
||||||
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
|
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
|
||||||
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
|
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
|
||||||
? [.compressionFactor: 0.9] : [:]
|
? [.compressionFactor: 0.9] : [:]
|
||||||
|
|
||||||
guard let data = bitmap.representation(using: fileType, properties: properties) else { return }
|
guard let data = bitmap.representation(using: fileType, properties: properties) else {
|
||||||
|
NSLog("SnipMC: could not encode image as \(imageFormat.rawValue)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try data.write(to: url, options: .atomic)
|
try data.write(to: url, options: .atomic)
|
||||||
|
NSLog("SnipMC: saved to \(url.path)")
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("SnipMC: failed to save annotated image: \(error)")
|
NSLog("SnipMC: failed to save annotated image: \(error)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ final class AnnotationCanvasView: NSView {
|
|||||||
let handleRect: CGRect
|
let handleRect: CGRect
|
||||||
if item.kind == .arrow {
|
if item.kind == .arrow {
|
||||||
let start = item.rect.origin
|
let start = item.rect.origin
|
||||||
let end = CGPoint(x: item.rect.maxX, y: item.rect.maxY)
|
let end = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||||
|
y: item.rect.origin.y + item.rect.size.height)
|
||||||
handleRect = CGRect(x: min(start.x, end.x), y: min(start.y, end.y),
|
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))
|
width: abs(end.x - start.x), height: abs(end.y - start.y))
|
||||||
} else if item.kind == .text {
|
} else if item.kind == .text {
|
||||||
|
|||||||
@@ -26,19 +26,39 @@ enum AnnotationRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
|
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
|
||||||
|
guard let srcCG = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return image }
|
||||||
|
let w = srcCG.width
|
||||||
|
let h = srcCG.height
|
||||||
let size = image.size
|
let size = image.size
|
||||||
let result = NSImage(size: size)
|
|
||||||
result.lockFocus()
|
|
||||||
|
|
||||||
image.draw(in: NSRect(origin: .zero, size: size))
|
guard let cs = srcCG.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB),
|
||||||
|
let ctx = CGContext(
|
||||||
|
data: nil, width: w, height: h,
|
||||||
|
bitsPerComponent: 8, bytesPerRow: 0, space: cs,
|
||||||
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||||
|
) else { return image }
|
||||||
|
|
||||||
if let context = NSGraphicsContext.current?.cgContext {
|
ctx.draw(srcCG, in: CGRect(x: 0, y: 0, width: w, height: h))
|
||||||
for item in annotations {
|
|
||||||
draw(item, in: context)
|
ctx.translateBy(x: 0, y: CGFloat(h))
|
||||||
}
|
let sx = CGFloat(w) / size.width
|
||||||
|
let sy = CGFloat(h) / size.height
|
||||||
|
ctx.scaleBy(x: sx, y: -sy)
|
||||||
|
|
||||||
|
NSGraphicsContext.saveGraphicsState()
|
||||||
|
NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true)
|
||||||
|
|
||||||
|
for item in annotations {
|
||||||
|
draw(item, in: ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.unlockFocus()
|
NSGraphicsContext.restoreGraphicsState()
|
||||||
|
|
||||||
|
guard let resultCG = ctx.makeImage() else { return image }
|
||||||
|
let bitmapRep = NSBitmapImageRep(cgImage: resultCG)
|
||||||
|
bitmapRep.size = size
|
||||||
|
let result = NSImage(size: size)
|
||||||
|
result.addRepresentation(bitmapRep)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +66,8 @@ enum AnnotationRenderer {
|
|||||||
|
|
||||||
private static func drawArrow(_ item: AnnotationItem, in context: CGContext) {
|
private static func drawArrow(_ item: AnnotationItem, in context: CGContext) {
|
||||||
let start = item.rect.origin
|
let start = item.rect.origin
|
||||||
let end = CGPoint(x: item.rect.maxX, y: item.rect.maxY)
|
let end = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||||
|
y: item.rect.origin.y + item.rect.size.height)
|
||||||
|
|
||||||
context.move(to: start)
|
context.move(to: start)
|
||||||
context.addLine(to: end)
|
context.addLine(to: end)
|
||||||
@@ -96,9 +117,11 @@ enum AnnotationRenderer {
|
|||||||
static func hitTest(point: CGPoint, item: AnnotationItem, tolerance: CGFloat = 6) -> Bool {
|
static func hitTest(point: CGPoint, item: AnnotationItem, tolerance: CGFloat = 6) -> Bool {
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
case .arrow:
|
case .arrow:
|
||||||
|
let arrowEnd = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||||
|
y: item.rect.origin.y + item.rect.size.height)
|
||||||
return distanceToLine(point: point,
|
return distanceToLine(point: point,
|
||||||
from: item.rect.origin,
|
from: item.rect.origin,
|
||||||
to: CGPoint(x: item.rect.maxX, y: item.rect.maxY)) < tolerance + item.lineWidth
|
to: arrowEnd) < tolerance + item.lineWidth
|
||||||
case .rectangle:
|
case .rectangle:
|
||||||
let outer = item.rect.insetBy(dx: -(tolerance + item.lineWidth),
|
let outer = item.rect.insetBy(dx: -(tolerance + item.lineWidth),
|
||||||
dy: -(tolerance + item.lineWidth))
|
dy: -(tolerance + item.lineWidth))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
private final class FlippedContainerView: NSView {
|
||||||
|
override var isFlipped: Bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
||||||
|
|
||||||
private let annotationDoc = AnnotationDocument()
|
private let annotationDoc = AnnotationDocument()
|
||||||
@@ -19,7 +23,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
let maxHeight = screenFrame.height * 0.8
|
let maxHeight = screenFrame.height * 0.8
|
||||||
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
|
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
|
||||||
let windowWidth = max(image.size.width * scale, 600)
|
let windowWidth = max(image.size.width * scale, 600)
|
||||||
let windowHeight = max(image.size.height * scale, 400) + 52 // toolbar height
|
let windowHeight = max(image.size.height * scale, 400) + 52
|
||||||
|
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
|
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
|
||||||
@@ -40,7 +44,6 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
canvasView.onDocumentChanged = { [weak self] in
|
canvasView.onDocumentChanged = { [weak self] in
|
||||||
self?.canvasView.needsDisplay = true
|
self?.canvasView.needsDisplay = true
|
||||||
}
|
}
|
||||||
canvasView.autoresizingMask = [.width, .height]
|
|
||||||
|
|
||||||
let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
|
let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
|
||||||
document: annotationDoc,
|
document: annotationDoc,
|
||||||
@@ -48,23 +51,25 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
|||||||
onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
|
onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
|
||||||
onCancel: { [weak self] in self?.cancel() }
|
onCancel: { [weak self] in self?.cancel() }
|
||||||
))
|
))
|
||||||
toolbarHosting.translatesAutoresizingMaskIntoConstraints = true
|
toolbarHosting.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
canvasView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
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]
|
|
||||||
|
|
||||||
|
let container = FlippedContainerView()
|
||||||
container.addSubview(toolbarHosting)
|
container.addSubview(toolbarHosting)
|
||||||
container.addSubview(canvasView)
|
container.addSubview(canvasView)
|
||||||
window.contentView = container
|
window.contentView = container
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
toolbarHosting.topAnchor.constraint(equalTo: container.topAnchor),
|
||||||
|
toolbarHosting.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
toolbarHosting.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
toolbarHosting.heightAnchor.constraint(equalToConstant: 52),
|
||||||
|
|
||||||
|
canvasView.topAnchor.constraint(equalTo: toolbarHosting.bottomAnchor),
|
||||||
|
canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
import ServiceManagement
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@EnvironmentObject var coordinator: AppCoordinator
|
@EnvironmentObject var coordinator: AppCoordinator
|
||||||
|
@State private var launchAtLogin = SMAppService.mainApp.status == .enabled
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
Section("Allgemein") {
|
||||||
|
Toggle("Mit dem System starten", isOn: $launchAtLogin)
|
||||||
|
.onChange(of: launchAtLogin) { _, newValue in
|
||||||
|
do {
|
||||||
|
if newValue {
|
||||||
|
try SMAppService.mainApp.register()
|
||||||
|
} else {
|
||||||
|
try SMAppService.mainApp.unregister()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
launchAtLogin = SMAppService.mainApp.status == .enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section("Nach der Aufnahme") {
|
Section("Nach der Aufnahme") {
|
||||||
Picker("Aktion:", selection: $coordinator.outputAction) {
|
Picker("Aktion:", selection: $coordinator.outputAction) {
|
||||||
ForEach(OutputAction.allCases) { action in
|
ForEach(OutputAction.allCases) { action in
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@ targets:
|
|||||||
base:
|
base:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snipmc
|
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snipmc
|
||||||
PRODUCT_NAME: SnipMC
|
PRODUCT_NAME: SnipMC
|
||||||
MARKETING_VERSION: "1.0.1"
|
MARKETING_VERSION: "1.0.2"
|
||||||
CURRENT_PROJECT_VERSION: "2"
|
CURRENT_PROJECT_VERSION: "3"
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user