From 440847ad1cec147c50012501224874e6ddf6f8c0 Mon Sep 17 00:00:00 2001 From: RoTokyo Date: Sat, 5 Sep 2026 22:22:11 +0900 Subject: [PATCH 1/2] 2026 09 05 aggiunto short cut + fuoco --- Notebar.xcodeproj/project.pbxproj | 20 ++- .../xcshareddata/xcschemes/Notebar.xcscheme | 78 +++++++++ .../xcschemes/xcschememanagement.plist | 22 +++ Notebar/AppDelegate.swift | 161 +++++++++++++++++- Notebar/ShortcutManager.swift | 73 ++++++++ Notebar/Views/ContentView.swift | 16 +- Notebar/Views/DropdownMenuView.swift | 23 +-- Notebar/Views/HeaderView.swift | 5 +- 8 files changed, 364 insertions(+), 34 deletions(-) create mode 100644 Notebar.xcodeproj/xcshareddata/xcschemes/Notebar.xcscheme create mode 100644 Notebar.xcodeproj/xcuserdata/kazuro.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Notebar/ShortcutManager.swift diff --git a/Notebar.xcodeproj/project.pbxproj b/Notebar.xcodeproj/project.pbxproj index 369df92..ea09fa4 100644 --- a/Notebar.xcodeproj/project.pbxproj +++ b/Notebar.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 69BFB696304B8B1E007EAB18 /* ShortcutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69BFB695304B8B1E007EAB18 /* ShortcutManager.swift */; }; C803A1EF25CF941200366CEB /* MbSwiftUIFirstResponder in Frameworks */ = {isa = PBXBuildFile; productRef = C803A1EE25CF941200366CEB /* MbSwiftUIFirstResponder */; }; C803A1F225CF98DF00366CEB /* TextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C803A1F125CF98DF00366CEB /* TextManager.swift */; }; C82C210725C60B3D00CCCDF7 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C82C210625C60B3D00CCCDF7 /* HeaderView.swift */; }; @@ -22,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 69BFB695304B8B1E007EAB18 /* ShortcutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutManager.swift; sourceTree = ""; }; C803A1F125CF98DF00366CEB /* TextManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextManager.swift; sourceTree = ""; }; C82C210625C60B3D00CCCDF7 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; C82C210925C60B5E00CCCDF7 /* DropdownMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownMenuView.swift; sourceTree = ""; }; @@ -83,6 +85,7 @@ C836C55825A0171500BEB83F /* AppDelegate.swift */, C803A1F125CF98DF00366CEB /* TextManager.swift */, C82C211825C771CA00CCCDF7 /* ThemeManager.swift */, + 69BFB695304B8B1E007EAB18 /* ShortcutManager.swift */, C82C211225C63CA800CCCDF7 /* Views */, C836C55C25A0171600BEB83F /* Assets.xcassets */, C82C210F25C62D8100CCCDF7 /* Colors.xcassets */, @@ -132,7 +135,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1220; + LastUpgradeCheck = 1420; TargetAttributes = { C836C55425A0171500BEB83F = { CreatedOnToolsVersion = 12.2; @@ -186,6 +189,7 @@ C836C55925A0171500BEB83F /* AppDelegate.swift in Sources */, C82C211925C771CA00CCCDF7 /* ThemeManager.swift in Sources */, C803A1F225CF98DF00366CEB /* TextManager.swift in Sources */, + 69BFB696304B8B1E007EAB18 /* ShortcutManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -237,6 +241,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -298,6 +303,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -324,10 +330,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Notebar/Notebar.entitlements; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Notebar/Preview Content\""; - DEVELOPMENT_TEAM = F725AN38JF; + DEVELOPMENT_TEAM = 9NFFKKH3LM; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Notebar/Info.plist; @@ -337,7 +345,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = "1.0-alpha"; - PRODUCT_BUNDLE_IDENTIFIER = com.stakelon.Notebar; + PRODUCT_BUNDLE_IDENTIFIER = com.kazuro.notebar; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; @@ -349,10 +357,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Notebar/Notebar.entitlements; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Notebar/Preview Content\""; - DEVELOPMENT_TEAM = F725AN38JF; + DEVELOPMENT_TEAM = 9NFFKKH3LM; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Notebar/Info.plist; @@ -362,7 +372,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = "1.0-alpha"; - PRODUCT_BUNDLE_IDENTIFIER = com.stakelon.Notebar; + PRODUCT_BUNDLE_IDENTIFIER = com.kazuro.notebar; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; diff --git a/Notebar.xcodeproj/xcshareddata/xcschemes/Notebar.xcscheme b/Notebar.xcodeproj/xcshareddata/xcschemes/Notebar.xcscheme new file mode 100644 index 0000000..dcdf52b --- /dev/null +++ b/Notebar.xcodeproj/xcshareddata/xcschemes/Notebar.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Notebar.xcodeproj/xcuserdata/kazuro.xcuserdatad/xcschemes/xcschememanagement.plist b/Notebar.xcodeproj/xcuserdata/kazuro.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..ce7d202 --- /dev/null +++ b/Notebar.xcodeproj/xcuserdata/kazuro.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Notebar.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + C836C55425A0171500BEB83F + + primary + + + + + diff --git a/Notebar/AppDelegate.swift b/Notebar/AppDelegate.swift index d016e44..957bf33 100644 --- a/Notebar/AppDelegate.swift +++ b/Notebar/AppDelegate.swift @@ -7,6 +7,7 @@ import Cocoa import SwiftUI +import Carbon.HIToolbox @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -15,10 +16,31 @@ class AppDelegate: NSObject, NSApplicationDelegate { var popover: NSPopover! var statusBarItem: NSStatusItem! + // Holds the current keyboard shortcut and remembers it between launches. + let shortcutManager = ShortcutManager() + + // Carbon hotkey used to open/close Notebar from anywhere. + // Carbon hotkeys work system-wide WITHOUT needing the "Input Monitoring" + // permission, unlike raw keyboard event monitors. + var toggleHotKeyRef: EventHotKeyRef? + var carbonEventHandler: EventHandlerRef? + + // Local monitor for Esc — only active while Notebar itself is frontmost. + var localMonitor: Any? + + // Local monitor used only while the user is recording a new shortcut. + var recordingMonitor: Any? + + // The app that was frontmost right before Notebar opened, so we can + // give it focus back when Notebar closes. + var previousApp: NSRunningApplication? + + // Notification we use to tell ContentView "put the cursor back in the text field" + static let didOpenNotification = NSNotification.Name("NotebarDidOpen") func applicationDidFinishLaunching(_ aNotification: Notification) { // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() + let contentView = ContentView(shortcutManager: shortcutManager) // Make this into a menu bar app let popover = NSPopover() @@ -33,23 +55,150 @@ class AppDelegate: NSObject, NSApplicationDelegate { button.image = NSImage(named: "MenubarIcon") button.action = #selector(togglePopover(_:)) } + + shortcutManager.onShortcutChanged = { [weak self] in + self?.reregisterToggleHotKey() + } + + installCarbonEventHandler() + registerCarbonHotKeyOnly() + registerEscMonitor() + } + + // MARK: - Global toggle shortcut (Carbon) + + func installCarbonEventHandler() { + var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed)) + + InstallEventHandler(GetApplicationEventTarget(), { (_, eventRef, userData) -> OSStatus in + guard let eventRef = eventRef, let userData = userData else { return noErr } + var hotKeyID = EventHotKeyID() + GetEventParameter(eventRef, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, MemoryLayout.size, nil, &hotKeyID) + if hotKeyID.id == 1 { + let appDelegate = Unmanaged.fromOpaque(userData).takeUnretainedValue() + appDelegate.togglePopover(nil) + } + return noErr + }, 1, &eventType, Unmanaged.passUnretained(self).toOpaque(), &carbonEventHandler) + } + + func registerCarbonHotKeyOnly() { + let hotKeyID = EventHotKeyID(signature: fourCharCode("NbTg"), id: 1) + RegisterEventHotKey(shortcutManager.keyCode, shortcutManager.modifiers, hotKeyID, GetApplicationEventTarget(), 0, &toggleHotKeyRef) + } + + func reregisterToggleHotKey() { + if let ref = toggleHotKeyRef { + UnregisterEventHotKey(ref) + toggleHotKeyRef = nil + } + registerCarbonHotKeyOnly() + } + + private func fourCharCode(_ string: String) -> FourCharCode { + var result: FourCharCode = 0 + for character in string.utf16 { + result = (result << 8) + FourCharCode(character) + } + return result + } + + // MARK: - Recording a new shortcut from the menu + + func startRecordingShortcut() { + // Stop listening for the old shortcut while we record a new one, + // and show a little hint in the menu bar so it's clear what's happening. + if let ref = toggleHotKeyRef { + UnregisterEventHotKey(ref) + toggleHotKeyRef = nil + } + statusBarItem.button?.title = " ⏺" + shortcutManager.isRecording = true + + recordingMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { [weak self] event in + guard let self = self else { return event } + + if event.keyCode == 53 { // Esc cancels, keeps the previous shortcut + self.finishRecording() + self.reregisterToggleHotKey() + return nil + } + + let flags = event.modifierFlags.intersection([.control, .option, .shift, .command]) + guard !flags.isEmpty else { return nil } // require at least one modifier key + + var carbonModifiers: UInt32 = 0 + if flags.contains(.control) { carbonModifiers |= UInt32(controlKey) } + if flags.contains(.option) { carbonModifiers |= UInt32(optionKey) } + if flags.contains(.shift) { carbonModifiers |= UInt32(shiftKey) } + if flags.contains(.command) { carbonModifiers |= UInt32(cmdKey) } + + self.shortcutManager.set(modifiers: carbonModifiers, keyCode: UInt32(event.keyCode)) + self.finishRecording() + return nil + } } - + + private func finishRecording() { + if let monitor = recordingMonitor { + NSEvent.removeMonitor(monitor) + recordingMonitor = nil + } + statusBarItem.button?.title = "" + shortcutManager.isRecording = false + NSSound.beep() + } + + // MARK: - Esc — close Notebar while it's frontmost + + func registerEscMonitor() { + localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + guard let self = self else { return event } + if event.keyCode == 53, self.popover.isShown { // 53 = Escape + self.closePopover() + return nil // swallow it so it doesn't do anything else + } + return event + } + } + + /// Closes the popover and hands focus back to whatever app was frontmost + /// before Notebar opened. + private func closePopover() { + popover.performClose(nil) + if let app = previousApp { + app.activate(options: []) + } + previousApp = nil + } + + // MARK: - Show/hide + @objc func togglePopover(_ sender: AnyObject?) { if let button = self.statusBarItem.button { if self.popover.isShown { - self.popover.performClose(sender) + closePopover() } else { + // Remember who had focus, so we can give it back when we close. + previousApp = NSWorkspace.shared.frontmostApplication + + NSApp.activate(ignoringOtherApps: true) + self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) self.popover.contentViewController?.view.window?.becomeKey() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + NotificationCenter.default.post(name: AppDelegate.didOpenNotification, object: nil) + } } } } func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application + if let ref = toggleHotKeyRef { UnregisterEventHotKey(ref) } + if let handler = carbonEventHandler { RemoveEventHandler(handler) } + if let monitor = localMonitor { NSEvent.removeMonitor(monitor) } + if let monitor = recordingMonitor { NSEvent.removeMonitor(monitor) } } - } - diff --git a/Notebar/ShortcutManager.swift b/Notebar/ShortcutManager.swift new file mode 100644 index 0000000..f038b61 --- /dev/null +++ b/Notebar/ShortcutManager.swift @@ -0,0 +1,73 @@ +// +// ShortcutManager.swift +// Notebar +// +// Stores the user's chosen open/close shortcut and remembers it between launches. +// +import Foundation +import Carbon.HIToolbox + +final class ShortcutManager: ObservableObject { + + private let modifiersKey = "NotebarShortcutModifiers" + private let keyCodeKey = "NotebarShortcutKeyCode" + + @Published private(set) var modifiers: UInt32 + @Published private(set) var keyCode: UInt32 + + // True while the user is actively pressing keys to set a new shortcut. + @Published var isRecording: Bool = false + + // AppDelegate sets this so it can re-register the system-wide shortcut + // whenever the user picks a new one. + var onShortcutChanged: (() -> Void)? + + init() { + let defaults = UserDefaults.standard + if defaults.object(forKey: modifiersKey) != nil { + modifiers = UInt32(defaults.integer(forKey: modifiersKey)) + keyCode = UInt32(defaults.integer(forKey: keyCodeKey)) + } else { + // Default: ⌃⌥⌘N + modifiers = UInt32(controlKey | optionKey | cmdKey) + keyCode = UInt32(kVK_ANSI_N) + } + } + + func set(modifiers: UInt32, keyCode: UInt32) { + self.modifiers = modifiers + self.keyCode = keyCode + let defaults = UserDefaults.standard + defaults.set(Int(modifiers), forKey: modifiersKey) + defaults.set(Int(keyCode), forKey: keyCodeKey) + onShortcutChanged?() + } + + /// Human-readable version, e.g. "⌃⌥⌘N", shown in the menu. + var displayString: String { + var s = "" + if modifiers & UInt32(controlKey) != 0 { s += "⌃" } + if modifiers & UInt32(optionKey) != 0 { s += "⌥" } + if modifiers & UInt32(shiftKey) != 0 { s += "⇧" } + if modifiers & UInt32(cmdKey) != 0 { s += "⌘" } + s += ShortcutManager.label(forKeyCode: keyCode) + return s + } + + /// Rough key-code → label mapping covering the keys people are likely to pick. + static func label(forKeyCode keyCode: UInt32) -> String { + let map: [UInt32: String] = [ + UInt32(kVK_Space): "Space", UInt32(kVK_Return): "↩︎", UInt32(kVK_Tab): "⇥", + UInt32(kVK_ANSI_A): "A", UInt32(kVK_ANSI_B): "B", UInt32(kVK_ANSI_C): "C", + UInt32(kVK_ANSI_D): "D", UInt32(kVK_ANSI_E): "E", UInt32(kVK_ANSI_F): "F", + UInt32(kVK_ANSI_G): "G", UInt32(kVK_ANSI_H): "H", UInt32(kVK_ANSI_I): "I", + UInt32(kVK_ANSI_J): "J", UInt32(kVK_ANSI_K): "K", UInt32(kVK_ANSI_L): "L", + UInt32(kVK_ANSI_M): "M", UInt32(kVK_ANSI_N): "N", UInt32(kVK_ANSI_O): "O", + UInt32(kVK_ANSI_P): "P", UInt32(kVK_ANSI_Q): "Q", UInt32(kVK_ANSI_R): "R", + UInt32(kVK_ANSI_S): "S", UInt32(kVK_ANSI_T): "T", UInt32(kVK_ANSI_U): "U", + UInt32(kVK_ANSI_V): "V", UInt32(kVK_ANSI_W): "W", UInt32(kVK_ANSI_X): "X", + UInt32(kVK_ANSI_Y): "Y", UInt32(kVK_ANSI_Z): "Z" + ] + return map[keyCode] ?? "Key\(keyCode)" + } +} diff --git a/Notebar/Views/ContentView.swift b/Notebar/Views/ContentView.swift index ff32977..8bea298 100644 --- a/Notebar/Views/ContentView.swift +++ b/Notebar/Views/ContentView.swift @@ -26,11 +26,16 @@ struct ContentView: View { @State var firstResponder: FirstResponders? = FirstResponders.textEditor @ObservedObject var themeManager = ThemeManager() @ObservedObject var textManager = TextManager() + @ObservedObject var shortcutManager: ShortcutManager + + init(shortcutManager: ShortcutManager) { + self.shortcutManager = shortcutManager + } var body: some View { ZStack { VStack(alignment: .leading, spacing: 0) { - HeaderView(themeManager: themeManager) + HeaderView(themeManager: themeManager, shortcutManager: shortcutManager) ZStack(alignment: .topLeading) { TextEditor(text: $textManager.text) .firstResponder(id: FirstResponders.textEditor, firstResponder: $firstResponder) @@ -48,7 +53,6 @@ struct ContentView: View { .background(themeManager.bgColor) } ZStack { -// if themeManager.isThemeEditor { Color(.shadowColor) .opacity(themeManager.isThemeEditor ? 0.5 : 0) .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -61,17 +65,17 @@ struct ContentView: View { .frame(width: 240, height: 240) .offset(y: themeManager.isThemeEditor ? 0 : 400) .animation(.easeOut(duration: 0.25)) -// } - } } .background(Color(.windowBackgroundColor)) + .onReceive(NotificationCenter.default.publisher(for: AppDelegate.didOpenNotification)) { _ in + firstResponder = FirstResponders.textEditor + } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() + ContentView(shortcutManager: ShortcutManager()) } } - diff --git a/Notebar/Views/DropdownMenuView.swift b/Notebar/Views/DropdownMenuView.swift index a767f20..4b9ef1c 100644 --- a/Notebar/Views/DropdownMenuView.swift +++ b/Notebar/Views/DropdownMenuView.swift @@ -4,12 +4,11 @@ // // Created by Jay Stakelon on 1/30/21. // - import SwiftUI - struct DropdownMenuView: NSViewRepresentable { @ObservedObject var themeManager: ThemeManager + @ObservedObject var shortcutManager: ShortcutManager func makeNSView(context: Context) -> NSPopUpButton { @@ -24,26 +23,20 @@ struct DropdownMenuView: NSViewRepresentable { nsView.removeAllItems() let iconItem = NSMenuItem() -// let iconImage = NSImage(named: "GearIcon") -// iconImage?.size = NSSize(width: 12, height: 12) -// iconItem.image = iconImage - - let themeItem = NSMenuItem(title: "Change theme", action: #selector(Coordinator.themeAction), keyEquivalent: "") - themeItem.representedObject = self.themeManager - themeItem.target = context.coordinator + + let shortcutItem = NSMenuItem(title: "Imposta scorciatoia (\(shortcutManager.displayString))…", action: #selector(Coordinator.shortcutAction), keyEquivalent: "") + shortcutItem.target = context.coordinator let quitItem = NSMenuItem(title: "Quit", action: #selector(Coordinator.quitAction), keyEquivalent: "q") quitItem.target = context.coordinator nsView.menu?.insertItem(iconItem, at: 0) -// nsView.menu?.insertItem(themeItem, at: 1) -// nsView.menu?.insertItem(NSMenuItem.separator(), at: 2) - nsView.menu?.insertItem(quitItem, at: 1) + nsView.menu?.insertItem(shortcutItem, at: 1) + nsView.menu?.insertItem(quitItem, at: 2) let cell = nsView.cell as? NSButtonCell cell?.imagePosition = .imageOnly cell?.bezelStyle = .texturedRounded - nsView.wantsLayer = true nsView.layer?.backgroundColor = NSColor.clear.cgColor nsView.isBordered = false @@ -53,7 +46,6 @@ struct DropdownMenuView: NSViewRepresentable { func makeCoordinator() -> Coordinator { Coordinator() } - class Coordinator: NSObject { @objc func quitAction(_ sender: NSMenuItem) { NSApplication.shared.terminate(self) @@ -62,5 +54,8 @@ struct DropdownMenuView: NSViewRepresentable { let tm = sender.representedObject as! ThemeManager tm.showThemeEditor() } + @objc func shortcutAction(_ sender: NSMenuItem) { + (NSApp.delegate as? AppDelegate)?.startRecordingShortcut() + } } } diff --git a/Notebar/Views/HeaderView.swift b/Notebar/Views/HeaderView.swift index 13f7c78..56b79ef 100644 --- a/Notebar/Views/HeaderView.swift +++ b/Notebar/Views/HeaderView.swift @@ -4,17 +4,16 @@ // // Created by Jay Stakelon on 1/30/21. // - import SwiftUI - struct HeaderView: View { @ObservedObject var themeManager: ThemeManager + @ObservedObject var shortcutManager: ShortcutManager var body: some View { VStack(alignment: .leading, spacing: 0) { HStack { Text("Notebar").font(Font.system(size: 12, weight: .bold, design: .rounded)) Spacer() - DropdownMenuView(themeManager: themeManager).frame(width: 24, height: 24) + DropdownMenuView(themeManager: themeManager, shortcutManager: shortcutManager).frame(width: 24, height: 24) } .padding(.vertical, 4) .padding(.horizontal, 12) From 2006976efca4280d0bde84ac5f6148586c3944bb Mon Sep 17 00:00:00 2001 From: RoTokyo Date: Sun, 6 Sep 2026 11:05:10 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Add=20=E2=8C=98K=20clear=20and=20=E2=8C=98S?= =?UTF-8?q?=20save-to-file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notebar.xcodeproj/project.pbxproj | 4 +++- Notebar/Info.plist | 2 ++ Notebar/Notebar.entitlements | 7 +----- Notebar/NotebarRelease.entitlements | 5 ++++ Notebar/Views/ContentView.swift | 37 ++++++++++++++++++++++++++++- Notebar/Views/HeaderView.swift | 22 ++++++++++++++++- README.md | 18 ++++++++++++-- 7 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 Notebar/NotebarRelease.entitlements diff --git a/Notebar.xcodeproj/project.pbxproj b/Notebar.xcodeproj/project.pbxproj index ea09fa4..5bdfd12 100644 --- a/Notebar.xcodeproj/project.pbxproj +++ b/Notebar.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 69749C16304CEB33007AE748 /* NotebarRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotebarRelease.entitlements; sourceTree = ""; }; 69BFB695304B8B1E007EAB18 /* ShortcutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutManager.swift; sourceTree = ""; }; C803A1F125CF98DF00366CEB /* TextManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextManager.swift; sourceTree = ""; }; C82C210625C60B3D00CCCDF7 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; @@ -82,6 +83,7 @@ C836C55725A0171500BEB83F /* Notebar */ = { isa = PBXGroup; children = ( + 69749C16304CEB33007AE748 /* NotebarRelease.entitlements */, C836C55825A0171500BEB83F /* AppDelegate.swift */, C803A1F125CF98DF00366CEB /* TextManager.swift */, C82C211825C771CA00CCCDF7 /* ThemeManager.swift */, @@ -356,7 +358,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Notebar/Notebar.entitlements; + CODE_SIGN_ENTITLEMENTS = Notebar/NotebarRelease.entitlements; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; diff --git a/Notebar/Info.plist b/Notebar/Info.plist index 666d692..c5aa4d5 100644 --- a/Notebar/Info.plist +++ b/Notebar/Info.plist @@ -26,6 +26,8 @@ $(MACOSX_DEPLOYMENT_TARGET) LSUIElement + NSAppleEventsUsageDescription + Notebar ha bisogno di questo permesso per salvare le tue note su Notes! NSMainStoryboardFile Main NSPrincipalClass diff --git a/Notebar/Notebar.entitlements b/Notebar/Notebar.entitlements index f2ef3ae..0c67376 100644 --- a/Notebar/Notebar.entitlements +++ b/Notebar/Notebar.entitlements @@ -1,10 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - + diff --git a/Notebar/NotebarRelease.entitlements b/Notebar/NotebarRelease.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Notebar/NotebarRelease.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/Notebar/Views/ContentView.swift b/Notebar/Views/ContentView.swift index 8bea298..11b9379 100644 --- a/Notebar/Views/ContentView.swift +++ b/Notebar/Views/ContentView.swift @@ -32,10 +32,45 @@ struct ContentView: View { self.shortcutManager = shortcutManager } + func clearText() { + textManager.text = "" + } + + /// Saves the current note as a plain text file inside ~/Documents/Notebar Notes/. + func saveToFile() { + let text = textManager.text + guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } + + guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + showSaveError("Non riesco a trovare la cartella Documenti.") + return + } + let folderURL = documentsURL.appendingPathComponent("Notebar Notes", isDirectory: true) + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH.mm.ss" + let fileURL = folderURL.appendingPathComponent("Nota \(formatter.string(from: Date())).txt") + + do { + try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true) + try text.write(to: fileURL, atomically: true, encoding: .utf8) + NSSound(named: "Glass")?.play() + } catch { + showSaveError(error.localizedDescription) + } + } + + private func showSaveError(_ message: String) { + let alert = NSAlert() + alert.messageText = "Impossibile salvare su Notes" + alert.informativeText = message + alert.runModal() + } + var body: some View { ZStack { VStack(alignment: .leading, spacing: 0) { - HeaderView(themeManager: themeManager, shortcutManager: shortcutManager) + HeaderView(themeManager: themeManager, shortcutManager: shortcutManager, onClear: clearText, onSaveToNotes: saveToFile) ZStack(alignment: .topLeading) { TextEditor(text: $textManager.text) .firstResponder(id: FirstResponders.textEditor, firstResponder: $firstResponder) diff --git a/Notebar/Views/HeaderView.swift b/Notebar/Views/HeaderView.swift index 56b79ef..c132944 100644 --- a/Notebar/Views/HeaderView.swift +++ b/Notebar/Views/HeaderView.swift @@ -8,11 +8,31 @@ import SwiftUI struct HeaderView: View { @ObservedObject var themeManager: ThemeManager @ObservedObject var shortcutManager: ShortcutManager + var onClear: () -> Void + var onSaveToNotes: () -> Void + var body: some View { VStack(alignment: .leading, spacing: 0) { HStack { - Text("Notebar").font(Font.system(size: 12, weight: .bold, design: .rounded)) + Text(shortcutManager.isRecording ? "Premi la combinazione (Esc annulla)" : "Notebar") + .font(Font.system(size: 12, weight: .bold, design: .rounded)) + .foregroundColor(shortcutManager.isRecording ? .orange : .primary) + .lineLimit(1) Spacer() + Button(action: onClear) { + Image(systemName: "trash") + } + .buttonStyle(.plain) + .keyboardShortcut("k", modifiers: .command) + .help("Clear (⌘K)") + + Button(action: onSaveToNotes) { + Image(systemName: "square.and.arrow.up") + } + .buttonStyle(.plain) + .keyboardShortcut("s", modifiers: .command) + .help("Salva come file (⌘S)") + DropdownMenuView(themeManager: themeManager, shortcutManager: shortcutManager).frame(width: 24, height: 24) } .padding(.vertical, 4) diff --git a/README.md b/README.md index 2d2d77d..a9f8efd 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,23 @@ Notebar's ready for quick notetaking whenever inspiration strikes. It's an always-on, stream-of-consciousness notepad; nothing more, nothing less. +> This is a fork of [stakes/Notebar](https://github.com/stakes/Notebar) by Jay Stakelon. +> All credit for the original app goes to him — see `license.md` for the license this project is distributed under. + +### What's new in this fork + +- **Global keyboard shortcut** to open and close Notebar from anywhere (default `⌃⌥⌘N`) +- The shortcut is **customizable**: open the menu inside Notebar and choose "Imposta scorciatoia..." / "Set Shortcut...", then press any combination with at least one modifier key +- **Esc** closes Notebar instantly +- Opening Notebar now correctly focuses the text field, instead of another control +- Closing Notebar hands keyboard focus back to whichever app you were using before + +- **⌘K** clears the note +- **⌘S** saves the note as a text file in `Documents/Notebar Notes/` + ### Get Notebar -[Download](https://github.com/stakes/Notebar/releases/download/1.0-alpha/Notebar.zip) and unzip the app, then put it into your Applications folder. +Download and unzip the app, then put it into your Applications folder. ### What it does @@ -16,4 +30,4 @@ Notebar is the most basic little notepad you could imagine and it lives in your ### Find a bug? -Please [create an issue](https://github.com/stakes/Notebar/issues) for it! +Please create an issue for it! \ No newline at end of file