Skip to content

Commit

Permalink
✨ Import from Rectangle (+ Pro)
Browse files Browse the repository at this point in the history
  • Loading branch information
SenpaiHunters committed Jul 9, 2024
1 parent 6361718 commit 93996c9
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
0A6DC3EB2BB869DE002AB05F /* WindowAction+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6DC3EA2BB869DE002AB05F /* WindowAction+Image.swift */; };
0AFE802E2BB98E81009CF06F /* WindowDirection+LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFE802D2BB98E81009CF06F /* WindowDirection+LocalizedString.swift */; };
4C0F2ACF2C3CFD09006CB34D /* TranslationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0F2ACE2C3CFD09006CB34D /* TranslationLayer.swift */; };
4C6B93E72C1DCF6E00AFF832 /* TheLoopTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B93E12C1DCF6E00AFF832 /* TheLoopTimes.swift */; };
4C6B93E82C1DCF6E00AFF832 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B93E22C1DCF6E00AFF832 /* Updater.swift */; };
4C6B93E92C1DCF6E00AFF832 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B93E32C1DCF6E00AFF832 /* UpdateView.swift */; };
Expand Down Expand Up @@ -93,6 +94,7 @@
/* Begin PBXFileReference section */
0A6DC3EA2BB869DE002AB05F /* WindowAction+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowAction+Image.swift"; sourceTree = "<group>"; };
0AFE802D2BB98E81009CF06F /* WindowDirection+LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowDirection+LocalizedString.swift"; sourceTree = "<group>"; };
4C0F2ACE2C3CFD09006CB34D /* TranslationLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationLayer.swift; sourceTree = "<group>"; };
4C6B93E12C1DCF6E00AFF832 /* TheLoopTimes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TheLoopTimes.swift; sourceTree = "<group>"; };
4C6B93E22C1DCF6E00AFF832 /* Updater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
4C6B93E32C1DCF6E00AFF832 /* UpdateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -210,6 +212,7 @@
A80900D12AA3F9F20085C63B /* Utilities */ = {
isa = PBXGroup;
children = (
4C0F2ACE2C3CFD09006CB34D /* TranslationLayer.swift */,
A8D4327A2C13ED3C007BE4F2 /* Icon.swift */,
A82DDBDD2AEC736300D7F974 /* AnimationConfiguration.swift */,
A80900D32AA3F9F20085C63B /* VisualEffectView.swift */,
Expand Down Expand Up @@ -571,6 +574,7 @@
A8789F6729805B190040512E /* RadialMenuView.swift in Sources */,
A8330ABD2A3AC0CA00673C8D /* Bundle+Extensions.swift in Sources */,
A80D49BB2BAE479900493B67 /* WindowAction+Port.swift in Sources */,
4C0F2ACF2C3CFD09006CB34D /* TranslationLayer.swift in Sources */,
A8D4327B2C13ED3C007BE4F2 /* Icon.swift in Sources */,
A86B97AD2AB79E2500099D7F /* ShakeEffect.swift in Sources */,
A8D6D3032B6C8D750061B11F /* PaddingPreviewView.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Loop/Extensions/Notification+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ extension Notification.Name {
static let didLoop = Notification.Name("didLoop")
static let activeStateChanged = Notification.Name("activeStateChanged")

static let keybindsUpdated = Notification.Name("keybindsUpdated")

@discardableResult
func onReceive(object: Any? = nil, using: @escaping (Notification) -> ()) -> NSObjectProtocol {
NotificationCenter.default.addObserver(
Expand Down
3 changes: 3 additions & 0 deletions Loop/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3481,6 +3481,9 @@
}
}
}
},
"Import from Rectangle" : {

},
"In a galaxy far, far away... still no updates!" : {

Expand Down
7 changes: 7 additions & 0 deletions Loop/Luminare/Loop/AdvancedConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ struct AdvancedConfigurationView: View {
}
}

LuminareSection {
Button("Import from Rectangle") {
initiateImportProcess()
}
.buttonStyle(LuminareButtonStyle())
}

LuminareSection("Permissions") {
HStack {
if model.isAccessibilityAccessGranted {
Expand Down
26 changes: 11 additions & 15 deletions Loop/Luminare/Settings/Keybindings/KeybindingsConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,30 @@
// Created by Kai Azim on 2024-04-20.
//

import Combine
import Defaults
import Luminare
import SwiftUI

class KeybindingsConfigurationModel: ObservableObject {
@Published var triggerKey = Defaults[.triggerKey] {
didSet {
Defaults[.triggerKey] = triggerKey
}
didSet { Defaults[.triggerKey] = triggerKey }
}

@Published var triggerDelay = Defaults[.triggerDelay] {
didSet {
Defaults[.triggerDelay] = triggerDelay
}
didSet { Defaults[.triggerDelay] = triggerDelay }
}

@Published var doubleClickToTrigger = Defaults[.doubleClickToTrigger] {
didSet {
Defaults[.doubleClickToTrigger] = doubleClickToTrigger
}
didSet { Defaults[.doubleClickToTrigger] = doubleClickToTrigger }
}

@Published var middleClickTriggersLoop = Defaults[.middleClickTriggersLoop] {
didSet {
Defaults[.middleClickTriggersLoop] = middleClickTriggersLoop
}
didSet { Defaults[.middleClickTriggersLoop] = middleClickTriggersLoop }
}

@Published var keybinds = Defaults[.keybinds] {
didSet {
Defaults[.keybinds] = keybinds
}
didSet { Defaults[.keybinds] = keybinds }
}

@Published var currentEventMonitor: NSEventMonitor?
Expand Down Expand Up @@ -75,6 +66,8 @@ struct KeybindingsConfigurationView: View {
selection: $model.selectedKeybinds,
addAction: {
model.keybinds.insert(.init(.noAction), at: 0)
// Post a notification that the keybinds have been updated
NotificationCenter.default.post(name: .keybindsUpdated, object: nil)
},
content: { keybind in
KeybindingItemView(keybind)
Expand All @@ -98,5 +91,8 @@ struct KeybindingsConfigurationView: View {
addText: "Add",
removeText: "Remove"
)
.onReceive(NotificationCenter.default.publisher(for: .keybindsUpdated)) { _ in
model.keybinds = Defaults[.keybinds]
}
}
}
93 changes: 93 additions & 0 deletions Loop/Utilities/TranslationLayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// TranslationLayer.swift
// Loop
//
// Created by Kami on 8/7/2024.
//

import AppKit
import Defaults
import Foundation

/// Represents a keyboard shortcut configuration for a Rectangle action.
struct RectangleShortcut: Codable {
let keyCode: Int
let modifierFlags: Int
}

/// Represents the configuration of Rectangle app shortcuts.
struct RectangleConfig: Codable {
let shortcuts: [String: RectangleShortcut]
}

/// Translates the RectangleConfig to an array of WindowActions for Loop.
/// - Parameter rectangleConfig: The RectangleConfig instance to translate. (this works w/ both normal and pro)
/// - Returns: An array of WindowAction instances corresponding to the RectangleConfig.
func translateRectangleConfigToWindowActions(rectangleConfig: RectangleConfig) -> [WindowAction] {
// Maps Rectangle direction keys to Loop's WindowDirection enum.
let directionMapping: [String: WindowDirection] = [
"bottomHalf": .bottomHalf,
"bottomRight": .bottomRightQuarter,
"center": .center,
"larger": .larger,
"leftHalf": .leftHalf,
"maximize": .maximize,
"nextDisplay": .nextScreen,
"previousDisplay": .previousScreen,
"restore": .undo,
"rightHalf": .rightHalf,
"smaller": .smaller,
"topHalf": .topHalf,
"topLeft": .topLeftQuarter,
"topRight": .topRightQuarter
]

// Converts the Rectangle shortcuts into Loop's WindowActions.
return rectangleConfig.shortcuts.compactMap { direction, shortcut in
guard let loopDirection = directionMapping[direction], !direction.contains("Todo") else { return nil }
return WindowAction(
loopDirection,
keybind: Set([CGKeyCode(shortcut.keyCode)]), // Converts the integer keyCode to CGKeyCode.
name: direction.capitalized.replacingOccurrences(of: " ", with: "") + "Cycle"
)
}
}

/// Initiates the import process for the RectangleConfig.json file.
func importRectangleConfig() {
let openPanel = NSOpenPanel()
openPanel.prompt = "Select Rectangle Config File"
openPanel.allowedContentTypes = [.json]

// Presents a file open panel to the user.
openPanel.begin { response in
guard response == .OK, let selectedFile = openPanel.url else { return }

// Attempts to decode the selected file into a RectangleConfig object.
if let rectangleConfig = try? JSONDecoder().decode(RectangleConfig.self, from: Data(contentsOf: selectedFile)) {
let windowActions = translateRectangleConfigToWindowActions(rectangleConfig: rectangleConfig)
saveWindowActions(windowActions)
} else {
print("Error reading or translating RectangleConfig.json")
}
}
}

/// Saves the translated WindowActions into Loop's configuration and posts a notification.
/// - Parameter windowActions: The array of WindowActions to save.
func saveWindowActions(_ windowActions: [WindowAction]) {
for action in windowActions {
print("Direction: \(action.direction), Keybind: \(action.keybind), Name: \(action.name ?? "")")
}

// Stores the WindowActions into Loop's configuration.
Defaults[.keybinds] = windowActions

// Post a notification after saving the new keybinds
NotificationCenter.default.post(name: .keybindsUpdated, object: nil)
}

/// Starts the import process for Rectangle configuration.
func initiateImportProcess() {
importRectangleConfig()
}

0 comments on commit 93996c9

Please sign in to comment.