Skip to content

Commit

Permalink
🔀 Merge pull request #646 from MrKai77/mrkai77/loop-685-resetting-key…
Browse files Browse the repository at this point in the history
…binds-and-importing-from-rectangle-should-have

✨ Keybind import/export/reset improvements
  • Loading branch information
MrKai77 authored Dec 28, 2024
2 parents 0abc2ae + 7a7a758 commit 706a942
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 72 deletions.
3 changes: 3 additions & 0 deletions Loop/Extensions/Notification+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ extension Notification.Name {

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

static let didImportKeybindsSuccessfully = Notification.Name("didImportKeybindsSuccessfully")
static let didExportKeybindsSuccessfully = Notification.Name("didExportKeybindsSuccessfully")

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

},
"Select Loop keybinds file" : {
"extractionState" : "manual",
Expand Down Expand Up @@ -20426,6 +20429,9 @@
}
}
}
},
"There are other keybinds that conflict with this key combination." : {

},
"Thickness" : {
"extractionState" : "manual",
Expand Down
107 changes: 97 additions & 10 deletions Loop/Luminare/Loop/AdvancedConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,57 @@ class AdvancedConfigurationModel: ObservableObject {
didSet { Defaults[.sizeIncrement] = sizeIncrement }
}

@Published var didImportSuccessfullyAlert = false
@Published var didExportSuccessfullyAlert = false
@Published var didResetSuccessfullyAlert = false

@Published var isAccessibilityAccessGranted = AccessibilityManager.getStatus()
@Published var isScreenCaptureAccessGranted = ScreenCaptureManager.getStatus()
@Published var accessibilityChecker: Publishers.Autoconnect<Timer.TimerPublisher> = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@Published var accessibilityChecks: Int = 0

func importedSuccessfully() {
DispatchQueue.main.async { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didImportSuccessfullyAlert = true
}
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didImportSuccessfullyAlert = false
}
}
}

func exportedSuccessfully() {
DispatchQueue.main.async { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didExportSuccessfullyAlert = true
}
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didExportSuccessfullyAlert = false
}
}
}

func resetSuccessfully() {
DispatchQueue.main.async { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didResetSuccessfullyAlert = true
}
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
withAnimation(.smooth(duration: 0.5)) {
self?.didResetSuccessfullyAlert = false
}
}
}

func beginAccessibilityAccessRequest() {
accessibilityChecker = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
accessibilityChecks = 0
Expand Down Expand Up @@ -73,6 +119,12 @@ struct AdvancedConfigurationView: View {
let elementHeight: CGFloat = 34

var body: some View {
generalSection()
keybindsSection()
permissionsSection()
}

func generalSection() -> some View {
LuminareSection("General") {
if #available(macOS 15.0, *) {
LuminareToggle("Use macOS window manager when available", isOn: $model.useSystemWindowManagerWhenAvailable)
Expand All @@ -96,31 +148,66 @@ struct AdvancedConfigurationView: View {
lowerClamp: true
)
}
}

func keybindsSection() -> some View {
LuminareSection("Keybinds") {
HStack(spacing: 2) {
Button("Import") {
Button {
WindowAction.importPrompt()
} label: {
HStack {
Text("Import")

if model.didImportSuccessfullyAlert {
Image(systemName: "checkmark")
.foregroundStyle(tintColor())
.bold()
}
}
}
.onReceive(.didImportKeybindsSuccessfully) { _ in
model.importedSuccessfully()
}

Button("Export") {
Button {
WindowAction.exportPrompt()
} label: {
HStack {
Text("Export")

if model.didExportSuccessfullyAlert {
Image(systemName: "checkmark")
.foregroundStyle(tintColor())
.bold()
}
}
}
.onReceive(.didExportKeybindsSuccessfully) { _ in
model.exportedSuccessfully()
}

Button("Reset") {
Button {
Defaults.reset(.keybinds)
model.resetSuccessfully()
Notification.Name.keybindsUpdated.post()
} label: {
HStack {
Text("Reset")

if model.didResetSuccessfullyAlert {
Image(systemName: "checkmark")
.foregroundStyle(tintColor())
.bold()
}
}
}
.buttonStyle(LuminareDestructiveButtonStyle())
}
}
}

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

func permissionsSection() -> some View {
LuminareSection("Permissions") {
accessibilityComponent()
screenCaptureComponent()
Expand Down
17 changes: 17 additions & 0 deletions Loop/Luminare/Settings/Keybindings/KeybindItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ struct KeybindItemView: View {
.modifier(LuminareBordered())
} else {
HStack(spacing: 6) {
let hasConflicts = hasDuplicateKeybinds()

if hasConflicts {
LuminareInfoView(
"There are other keybinds that conflict with this key combination.",
.red
)
}

HStack {
ForEach(triggerKey.sorted().compactMap(\.systemImage), id: \.self) { image in
Text("\(Image(systemName: image))")
Expand All @@ -129,6 +138,7 @@ struct KeybindItemView: View {
Image(systemName: "plus")

Keycorder($keybind)
.opacity(hasConflicts ? 0.5 : 1)
}
.fixedSize()
}
Expand Down Expand Up @@ -182,6 +192,13 @@ struct KeybindItemView: View {
.help("Customize this keybind's action.")
}

/// Checks if there are any existing keybinds with the same key combination
func hasDuplicateKeybinds() -> Bool {
Defaults[.keybinds]
.filter { $0.keybind == keybind.keybind }
.count > 1
}

func directionPicker() -> some View {
VStack {
Button {
Expand Down
67 changes: 27 additions & 40 deletions Loop/Utilities/RectangleTranslationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import AppKit
import Defaults
import Foundation

/// Represents an error that can occur in the RectangleTranslationLayer.
enum RectangleTranslationLayerError: Error {
case dataLoadFailed

var localizedString: String {
switch self {
case .dataLoadFailed:
"Failed to convert string to data."
}
}
}

/// Represents a keyboard shortcut configuration for a Rectangle action.
struct RectangleShortcut: Codable {
let keyCode: Int
Expand Down Expand Up @@ -42,10 +54,24 @@ enum RectangleTranslationLayer {
"topRight": .topRightQuarter
]

/// Imports the keybinds from a JSON string.
/// - Parameter jsonString: The JSON string to import the keybinds from.
/// - Returns: An array of WindowAction instances corresponding to the keybinds.
static func importKeybinds(from jsonString: String) throws -> [WindowAction] {
guard let data = jsonString.data(using: .utf8) else {
throw RectangleTranslationLayerError.dataLoadFailed
}

let rectangleConfig = try JSONDecoder().decode(RectangleConfig.self, from: data)
let windowActions = translateRectangleConfigToWindowActions(rectangleConfig: rectangleConfig)

return windowActions
}

/// Translates the RectangleConfig to an array of WindowActions for Loop.
/// - Parameter rectangleConfig: The RectangleConfig instance to translate.
/// - Returns: An array of WindowAction instances corresponding to the RectangleConfig.
static func translateRectangleConfigToWindowActions(rectangleConfig: RectangleConfig) -> [WindowAction] {
private static func translateRectangleConfigToWindowActions(rectangleConfig: RectangleConfig) -> [WindowAction] {
// Converts the Rectangle shortcuts into Loop's WindowActions.
rectangleConfig.shortcuts.compactMap { direction, shortcut in
guard let loopDirection = directionMapping[direction], !direction.contains("Todo") else { return nil }
Expand All @@ -56,43 +82,4 @@ enum RectangleTranslationLayer {
)
}
}

/// Initiates the import process for the RectangleConfig.json file.
static func importRectangleConfig() {
let openPanel = NSOpenPanel()
openPanel.prompt = .init(localized: "Import from Rectangle", defaultValue: "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.
static 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.
static func initiateImportProcess() {
importRectangleConfig()
}
}
Loading

0 comments on commit 706a942

Please sign in to comment.