diff --git a/DockDoor.xcodeproj/project.pbxproj b/DockDoor.xcodeproj/project.pbxproj index 6269ecf..a0673dd 100644 --- a/DockDoor.xcodeproj/project.pbxproj +++ b/DockDoor.xcodeproj/project.pbxproj @@ -454,7 +454,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.0.15; + CURRENT_PROJECT_VERSION = 1.0.16; DEVELOPMENT_ASSET_PATHS = "\"DockDoor/Preview Content\""; DEVELOPMENT_TEAM = 2Q775S63Q3; ENABLE_HARDENED_RUNTIME = YES; @@ -469,7 +469,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.16; PRODUCT_BUNDLE_IDENTIFIER = com.ethanbills.DockDoor; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -486,7 +486,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.0.15; + CURRENT_PROJECT_VERSION = 1.0.16; DEVELOPMENT_ASSET_PATHS = "\"DockDoor/Preview Content\""; DEVELOPMENT_TEAM = 2Q775S63Q3; ENABLE_HARDENED_RUNTIME = YES; @@ -501,7 +501,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.16; PRODUCT_BUNDLE_IDENTIFIER = com.ethanbills.DockDoor; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/DockDoor.xcodeproj/project.xcworkspace/xcuserdata/ethanbills.xcuserdatad/UserInterfaceState.xcuserstate b/DockDoor.xcodeproj/project.xcworkspace/xcuserdata/ethanbills.xcuserdatad/UserInterfaceState.xcuserstate index fc37a97..29e1dd5 100644 Binary files a/DockDoor.xcodeproj/project.xcworkspace/xcuserdata/ethanbills.xcuserdatad/UserInterfaceState.xcuserstate and b/DockDoor.xcodeproj/project.xcworkspace/xcuserdata/ethanbills.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/DockDoor/AppDelegate.swift b/DockDoor/AppDelegate.swift index 3d710cc..a82ef74 100644 --- a/DockDoor/AppDelegate.swift +++ b/DockDoor/AppDelegate.swift @@ -42,7 +42,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else { dockObserver = DockObserver.shared appClosureObserver = AppClosureObserver.shared - if Defaults[.showWindowSwitcher] { + if Defaults[.enableWindowSwitcher] { keybindHelper = KeybindHelper.shared } } diff --git a/DockDoor/Utilities/KeybindHelper.swift b/DockDoor/Utilities/KeybindHelper.swift index ea783bd..11d0a5b 100644 --- a/DockDoor/Utilities/KeybindHelper.swift +++ b/DockDoor/Utilities/KeybindHelper.swift @@ -7,23 +7,29 @@ import AppKit import Carbon +import Defaults + +struct UserKeyBind: Codable, Defaults.Serializable { + var keyCode: UInt16 + var modifierFlags: Int +} class KeybindHelper { static let shared = KeybindHelper() - - private var isControlKeyPressed = false + private var isModifierKeyPressed = false private var isShiftKeyPressed = false private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? - + private var modifierValue: Int = 0 + private init() { setupEventTap() } - + deinit { removeEventTap() } - + private func setupEventTap() { let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue) | (1 << CGEventType.flagsChanged.rawValue) @@ -47,7 +53,7 @@ class KeybindHelper { CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: eventTap, enable: true) } - + private func removeEventTap() { if let eventTap = eventTap, let runLoopSource = runLoopSource { CGEvent.tapEnable(tap: eventTap, enable: false) @@ -56,32 +62,33 @@ class KeybindHelper { self.runLoopSource = nil } } - + private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged? { let keyCode = event.getIntegerValueField(.keyboardEventKeycode) + let keyBoardShortcutSaved: UserKeyBind = Defaults[.UserKeybind] // UserDefaults.standard.getKeybind()! + let shiftKeyCurrentlyPressed = event.flags.contains(.maskShift) + var userDefinedKeyCurrentlyPressed = false - switch type { - case .flagsChanged: - let modifierFlags = event.flags - let controlKeyCurrentlyPressed = modifierFlags.contains(.maskControl) - let shiftKeyCurrentlyPressed = modifierFlags.contains(.maskShift) - - if controlKeyCurrentlyPressed != isControlKeyPressed { - isControlKeyPressed = controlKeyCurrentlyPressed - } - - // Update the state of Shift key - if shiftKeyCurrentlyPressed != isShiftKeyPressed { - isShiftKeyPressed = shiftKeyCurrentlyPressed + if ((type == .flagsChanged) && (!Defaults[.defaultCMDTABKeybind])){ + // New Keybind that the user has enforced, includes the modifier keys + if event.flags.contains(.maskControl) { + modifierValue = Defaults[.Int64maskControl] + userDefinedKeyCurrentlyPressed = true } - - if !isControlKeyPressed { // If Ctrl was released - HoverWindow.shared.hideWindow() // Hide the HoverWindow - HoverWindow.shared.selectAndBringToFrontCurrentWindow() + else if event.flags.contains(.maskAlternate) { + modifierValue = Defaults[.Int64maskAlternate] + userDefinedKeyCurrentlyPressed = true } - - case .keyDown: - if isControlKeyPressed && keyCode == 48 { // Tab key + handleModifierEvent(modifierKeyPressed: userDefinedKeyCurrentlyPressed, shiftKeyPressed: shiftKeyCurrentlyPressed) + } + + else if ((type == .flagsChanged) && (Defaults[.defaultCMDTABKeybind])){ + // Default MacOS CMD + TAB keybind replaced + handleModifierEvent(modifierKeyPressed: event.flags.contains(.maskCommand), shiftKeyPressed: shiftKeyCurrentlyPressed) + } + + else if (type == .keyDown){ + if (isModifierKeyPressed && keyCode == keyBoardShortcutSaved.keyCode && modifierValue == keyBoardShortcutSaved.modifierFlags) || (Defaults[.defaultCMDTABKeybind] && keyCode == 48) { // Tab key if HoverWindow.shared.isVisible { // Check if HoverWindow is already shown HoverWindow.shared.cycleWindows(goBackwards: isShiftKeyPressed) // Cycle windows based on Shift key state } else { @@ -89,14 +96,26 @@ class KeybindHelper { } return nil // Suppress the Tab key event } - - default: - break } - + return Unmanaged.passUnretained(event) } - + + private func handleModifierEvent(modifierKeyPressed : Bool, shiftKeyPressed : Bool){ + if modifierKeyPressed != isModifierKeyPressed { + isModifierKeyPressed = modifierKeyPressed + } + // Update the state of Shift key + if shiftKeyPressed != isShiftKeyPressed { + isShiftKeyPressed = shiftKeyPressed + } + + if !isModifierKeyPressed { + HoverWindow.shared.hideWindow() // Hide the HoverWindow + HoverWindow.shared.selectAndBringToFrontCurrentWindow() + } + } + private func showHoverWindow() { Task { [weak self] in do { @@ -104,7 +123,7 @@ class KeybindHelper { let windows = try await WindowUtil.activeWindows(for: "") await MainActor.run { [weak self] in guard let self = self else { return } - if self.isControlKeyPressed { + if self.isModifierKeyPressed { HoverWindow.shared.showWindow(appName: "Alt-Tab", windows: windows, overrideDelay: true, onWindowTap: { HoverWindow.shared.hideWindow() }) } } diff --git a/DockDoor/Utilities/Misc Utils.swift b/DockDoor/Utilities/Misc Utils.swift index d9d8b09..5d3d38e 100644 --- a/DockDoor/Utilities/Misc Utils.swift +++ b/DockDoor/Utilities/Misc Utils.swift @@ -7,6 +7,7 @@ import Cocoa import Defaults +import Carbon func quitApp() { // Terminate the current application @@ -41,3 +42,67 @@ func measureString(_ string: String, fontSize: CGFloat, fontWeight: NSFont.Weigh let size = attributedString.size() return size } + +struct modifierConverter { + static func toString(_ modifierIntValue: Int) -> String { + if modifierIntValue == Defaults[.Int64maskCommand] { + return "⌘" + } + else if modifierIntValue == Defaults[.Int64maskAlternate] { + return "⌥" + } + else if modifierIntValue == Defaults[.Int64maskControl] { + return "⌃" + } + else { + return " " + } + } +} + +struct KeyCodeConverter { + static func toString(_ keyCode: UInt16) -> String { + switch keyCode { + case 48: + return "⇥" // Tab symbol + case 51: + return "⌫" // Delete symbol + case 53: + return "⎋" // Escape symbol + case 36: + return "↩︎" // Return symbol + default: + + let source = TISCopyCurrentKeyboardInputSource().takeUnretainedValue() + let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) + + guard let data = layoutData else { + return "?" + } + + let layout = unsafeBitCast(data, to: CFData.self) + let keyboardLayout = unsafeBitCast(CFDataGetBytePtr(layout), to: UnsafePointer.self) + + var keysDown: UInt32 = 0 + var chars = [UniChar](repeating: 0, count: 4) + var realLength: Int = 0 + + let result = UCKeyTranslate(keyboardLayout, + keyCode, + UInt16(kUCKeyActionDisplay), + 0, + UInt32(LMGetKbdType()), + UInt32(kUCKeyTranslateNoDeadKeysBit), + &keysDown, + chars.count, + &realLength, + &chars) + + if result == noErr { + return String(utf16CodeUnits: chars, count: realLength) + } else { + return "?" + } + } + } +} diff --git a/DockDoor/Views/Hover Window/WindowPreview.swift b/DockDoor/Views/Hover Window/WindowPreview.swift index 615f490..da3b6bd 100644 --- a/DockDoor/Views/Hover Window/WindowPreview.swift +++ b/DockDoor/Views/Hover Window/WindowPreview.swift @@ -110,13 +110,13 @@ struct WindowPreview: View { let stringMeasurementWidth = measureString(windowTitle, fontSize: 12).width + 5 let width = maxLabelWidth > stringMeasurementWidth ? stringMeasurementWidth : maxLabelWidth - TheMarquee(width: width, secsBeforeLooping: 3, speedPtsPerSec: 20, nonMovingAlignment: .leading) { + TheMarquee(width: width, secsBeforeLooping: 1, speedPtsPerSec: 20, nonMovingAlignment: .leading) { Text(windowInfo.windowName ?? "Hidden window") .font(.system(size: 12, weight: .medium)) .foregroundStyle(.primary) } .padding(4) - .dockStyle(cornerRadius: 6) + .background(RoundedRectangle(cornerRadius: 6, style: .continuous).fill(.ultraThinMaterial)) .padding(4) } } diff --git a/DockDoor/Views/Settings/WindowSwitcher.swift b/DockDoor/Views/Settings/WindowSwitcher.swift index cd280be..47823bf 100644 --- a/DockDoor/Views/Settings/WindowSwitcher.swift +++ b/DockDoor/Views/Settings/WindowSwitcher.swift @@ -8,21 +8,127 @@ import SwiftUI import Defaults +import Carbon + +class KeybindModel: ObservableObject { + @Published var modifierKey: Int + @Published var isRecording: Bool = false + @Published var currentKeybind: UserKeyBind? + + init() { + self.modifierKey = Defaults[.UserKeybind].modifierFlags + self.currentKeybind = Defaults[.UserKeybind] + } + +} struct WindowSwitcherSettingsView: View { - @Default(.showWindowSwitcher) var showWindowSwitcher - + @Default(.enableWindowSwitcher) var enableWindowSwitcher + @Default(.defaultCMDTABKeybind) var defaultCMDTABKeybind var body: some View { VStack(alignment: .leading, spacing: 10) { - Toggle(isOn: $showWindowSwitcher, label: { + Toggle(isOn: $enableWindowSwitcher, label: { Text("Enable Window Switcher") - }).onChange(of: showWindowSwitcher){ + }).onChange(of: enableWindowSwitcher){ _, newValue in restartApplication() } - + // Default CMD + TAB implementation checkbox + if Defaults[.enableWindowSwitcher] { + Toggle(isOn: $defaultCMDTABKeybind, label: { + Text("Use default MacOS keybind ⌘ + ⇥") + }) + // If default CMD Tab is not enabled + if !Defaults[.defaultCMDTABKeybind] { + InitializationKeyPickerView() + } + } } .padding(20) .frame(minWidth: 600) } } + +struct InitializationKeyPickerView: View { + @ObservedObject var viewModel = KeybindModel() + + var body: some View { + VStack(spacing: 20) { + Text("Set Initialization Key and Keybind") + .font(.headline) + .padding(.top, 20) + + Picker("Initialization Key", selection: $viewModel.modifierKey) { + Text("Control (⌃)").tag(Defaults[.Int64maskControl]) + Text("Option (⌥)").tag(Defaults[.Int64maskAlternate]) + Text("Command (⌘)").tag(Defaults[.Int64maskCommand]) + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.horizontal) + + Text("Press any key combination after holding the initialization key to set the keybind.") + .multilineTextAlignment(.center) + .padding(.horizontal) + + Button(action: { viewModel.isRecording = true }) { + Text(viewModel.isRecording ? "Press the key combination..." : "Start Recording Keybind") + } + .keyboardShortcut(.defaultAction) + .padding(.bottom, 20) + + if let keybind = viewModel.currentKeybind { + Text("Current Keybind: \(printCurrentKeybind(keybind))") + .padding() + } + } + .background( + ShortcutCaptureView( + currentKeybind: $viewModel.currentKeybind, + isRecording: $viewModel.isRecording, + modifierKey: $viewModel.modifierKey + ) + ) + .onAppear { + viewModel.currentKeybind = Defaults[.UserKeybind] + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + + func printCurrentKeybind(_ shortcut: UserKeyBind) -> String { + var parts: [String] = [] + parts.append(modifierConverter.toString(shortcut.modifierFlags)) + parts.append(KeyCodeConverter.toString(shortcut.keyCode)) + return parts.joined(separator: " ") + } +} + +struct ShortcutCaptureView: NSViewRepresentable { + @Binding var currentKeybind: UserKeyBind? + @Binding var isRecording: Bool + @Binding var modifierKey: Int + + func makeNSView(context: Context) -> NSView { + let view = NSView() + NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + guard self.isRecording else { + return event + } + self.isRecording = false + if event.keyCode == 48 && modifierKey == Defaults[.Int64maskCommand] { // User has chosen the default Mac OS window switcher keybind + // Set the default CMDTAB + Defaults[.defaultCMDTABKeybind] = true + Defaults[.UserKeybind] = UserKeyBind(keyCode: 48, modifierFlags: Defaults[.Int64maskControl]) + self.currentKeybind = Defaults[.UserKeybind] + return event + } + Defaults[.UserKeybind] = UserKeyBind(keyCode: event.keyCode, modifierFlags: modifierKey) + self.currentKeybind = Defaults[.UserKeybind] + return nil + } + return view + } + + + func updateNSView(_ nsView: NSView, context: Context) {} +} diff --git a/DockDoor/consts.swift b/DockDoor/consts.swift index 3b4673b..62e434d 100644 --- a/DockDoor/consts.swift +++ b/DockDoor/consts.swift @@ -7,6 +7,7 @@ import Cocoa import Defaults +//import Carbon let optimisticScreenSizeWidth = NSScreen.main!.frame.width let optimisticScreenSizeHeight = NSScreen.main!.frame.height @@ -19,8 +20,12 @@ extension Defaults.Keys { static let openDelay = Key("openDelay") { 0 } static let screenCaptureCacheLifespan = Key("screenCaptureCacheLifespan") { 60 } static let showAnimations = Key("showAnimations") { true } - static let showWindowSwitcher = Key("showWindowSwitcher"){ true } + static let enableWindowSwitcher = Key("enableWindowSwitcher"){ true } static let showMenuBarIcon = Key("showMenuBarIcon", default: true) - + static let defaultCMDTABKeybind = Key("defaultCMDTABKeybind") { true } static let launched = Key("launched") { false } + static let Int64maskCommand = Key("Int64maskCommand") { 1048840 } + static let Int64maskControl = Key("Int64maskControl") { 262401 } + static let Int64maskAlternate = Key("Int64maskAlternate") { 524576 } + static let UserKeybind = Key("UserKeybind", default: UserKeyBind(keyCode: 48, modifierFlags: Defaults[.Int64maskControl])) } diff --git a/README.md b/README.md index 1c20aea..8899369 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,14 @@ DockDoor is a macOS application developed with Swift and SwiftUI that allows use ## Usage - **How do I use the alt-tab functionality?** - - Ctrl + Tab to open the menu, continue pressing tab to increment forwards, shift + tab to go back. Letting go of control will select the window. + - By default, use Cmd + Tab to open the window switcher, continue pressing Tab to increment forwards, Shift + Tab to go back. Letting go of command will select the window. + - Disabling the default Cmd + Tab keybind, will allow a user to set a custom keybind + - User selects one of the modifiers presented on the screen + - User presses `Record Keybind` button + - User presses a singular key on the keyboard + - Keybind is now set! + + ![Set keybind](./resources/setKeybind.gif) - **How do I use the dock peeking functionality?** - Simply hover over any application with active windows in the dock. - **What are the traffic light buttons that appear in the preview window?** diff --git a/Sparkle/generate_appcast b/Sparkle/generate_appcast new file mode 100644 index 0000000..1e2df3f Binary files /dev/null and b/Sparkle/generate_appcast differ diff --git a/resources/setKeybind.gif b/resources/setKeybind.gif new file mode 100644 index 0000000..5fea612 Binary files /dev/null and b/resources/setKeybind.gif differ