Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing the usage of custom keybinds set by user to switch windows #88

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DockDoor/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

dockObserver = DockObserver.shared
appClosureObserver = AppClosureObserver.shared
if Defaults[.showWindowSwitcher] {
if Defaults[.enableWindowSwitcher] {
keybindHelper = KeybindHelper.shared
}
}
Expand Down
85 changes: 52 additions & 33 deletions DockDoor/Utilities/KeybindHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -56,55 +62,68 @@ class KeybindHelper {
self.runLoopSource = nil
}
}

private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
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 {
showHoverWindow() // Initialize HoverWindow if it's not open
}
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 {
guard let self = self else { return }
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() })
}
}
Expand Down
65 changes: 65 additions & 0 deletions DockDoor/Utilities/Misc Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Cocoa
import Defaults
import Carbon

func quitApp() {
// Terminate the current application
Expand Down Expand Up @@ -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<UCKeyboardLayout>.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 "?"
}
}
}
}
97 changes: 92 additions & 5 deletions DockDoor/Views/Settings/WindowSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,108 @@

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] {
ModifierKeyPickerView()
}
}
}
.padding(20)
.frame(minWidth: 600)
}
}

struct ModifierKeyPickerView : View {
@ObservedObject var viewModel = KeybindModel()

var body: some View {
VStack(spacing: 20) {
Picker("Modifier Key", selection: $viewModel.modifierKey) {
Text("Control (⌃)").tag(Defaults[.Int64maskControl])
Text("Option (⌥)").tag(Defaults[.Int64maskAlternate])
Text("Command (⌘)").tag(Defaults[.Int64maskCommand])
}
.pickerStyle(SegmentedPickerStyle())
Text("Press any key combination to set the keybind").padding()
Button(action: {viewModel.isRecording = true}){
Text(viewModel.isRecording ? "Press key ..." : "Record keybind")
}.keyboardShortcut(.defaultAction)
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(alignment: .leading)
}

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) {}
}
9 changes: 7 additions & 2 deletions DockDoor/consts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Cocoa
import Defaults
//import Carbon

let optimisticScreenSizeWidth = NSScreen.main!.frame.width
let optimisticScreenSizeHeight = NSScreen.main!.frame.height
Expand All @@ -19,8 +20,12 @@ extension Defaults.Keys {
static let openDelay = Key<CGFloat>("openDelay") { 0 }
static let screenCaptureCacheLifespan = Key<CGFloat>("screenCaptureCacheLifespan") { 60 }
static let showAnimations = Key<Bool>("showAnimations") { true }
static let showWindowSwitcher = Key<Bool>("showWindowSwitcher"){ true }
static let enableWindowSwitcher = Key<Bool>("enableWindowSwitcher"){ true }
static let showMenuBarIcon = Key<Bool>("showMenuBarIcon", default: true)

static let defaultCMDTABKeybind = Key<Bool>("defaultCMDTABKeybind") { true }
static let launched = Key<Bool>("launched") { false }
static let Int64maskCommand = Key<Int>("Int64maskCommand") { 1048840 }
static let Int64maskControl = Key<Int>("Int64maskControl") { 262401 }
static let Int64maskAlternate = Key<Int>("Int64maskAlternate") { 524576 }
static let UserKeybind = Key<UserKeyBind>("UserKeybind", default: UserKeyBind(keyCode: 48, modifierFlags: Defaults[.Int64maskControl]))
}
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?**
Expand Down
Binary file added Sparkle/generate_appcast
Binary file not shown.
Binary file added resources/setKeybind.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.