Skip to content

Commit

Permalink
🔀 Merge pull request #566 from MrKai77/system-resize
Browse files Browse the repository at this point in the history
  • Loading branch information
SenpaiHunters authored Sep 20, 2024
2 parents d4e40cc + b15a1e0 commit 3343ee3
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 43 deletions.
8 changes: 4 additions & 4 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
A864F4682AA660CD00579738 /* WindowDragManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A864F4672AA660CD00579738 /* WindowDragManager.swift */; };
A867C20E2C26522B005831BC /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A867C20D2C26522B005831BC /* Observer.swift */; };
A86949862A8F2BB70051AAAF /* CGKeyCode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */; };
A869C1A12B38C6E600AD1A84 /* StageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A869C1A02B38C6E600AD1A84 /* StageManager.swift */; };
A869C1A12B38C6E600AD1A84 /* SystemWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A869C1A02B38C6E600AD1A84 /* SystemWindowManager.swift */; };
A86B97AD2AB79E2500099D7F /* ShakeEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */; };
A86CB7332A3D22E7006A78F2 /* WindowEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */; };
A8789F6729805B190040512E /* RadialMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8789F6629805B190040512E /* RadialMenuView.swift */; };
Expand Down Expand Up @@ -137,7 +137,7 @@
A864F4672AA660CD00579738 /* WindowDragManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDragManager.swift; sourceTree = "<group>"; };
A867C20D2C26522B005831BC /* Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGKeyCode+Extensions.swift"; sourceTree = "<group>"; };
A869C1A02B38C6E600AD1A84 /* StageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageManager.swift; sourceTree = "<group>"; };
A869C1A02B38C6E600AD1A84 /* SystemWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemWindowManager.swift; sourceTree = "<group>"; };
A86AFD7529888B29008F4892 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShakeEffect.swift; sourceTree = "<group>"; };
A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowEngine.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -263,7 +263,7 @@
A864F4672AA660CD00579738 /* WindowDragManager.swift */,
A82521ED29E235AC00139654 /* PermissionsManager.swift */,
A8EF1F08299C87DF00633440 /* IconManager.swift */,
A869C1A02B38C6E600AD1A84 /* StageManager.swift */,
A869C1A02B38C6E600AD1A84 /* SystemWindowManager.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -597,7 +597,7 @@
A867C20E2C26522B005831BC /* Observer.swift in Sources */,
A82740982AB00FCE00B9BDC5 /* Color+Extensions.swift in Sources */,
A82B1AF62BD35C8500E2F3F9 /* BehaviorConfiguration.swift in Sources */,
A869C1A12B38C6E600AD1A84 /* StageManager.swift in Sources */,
A869C1A12B38C6E600AD1A84 /* SystemWindowManager.swift in Sources */,
A8330ACD2A3AC1D100673C8D /* CGGeometry+Extensions.swift in Sources */,
A82B1AF22BD35A3800E2F3F9 /* PreviewConfiguration.swift in Sources */,
A8330AC72A3AC19500673C8D /* NSScreen+Extensions.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions Loop/Extensions/AXUIElement+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ extension AXUIElement {

return id
}

func performAction(_ action: NSAccessibility.Action) throws {
let error = AXUIElementPerformAction(self, action as CFString)

guard error == .success else {
throw error
}
}

var children: [AXUIElement] {
let children: [AXUIElement]? = try? getValue(.children)
return children ?? []
}
}

extension AXError: Swift.Error {}
Expand Down
3 changes: 2 additions & 1 deletion Loop/Extensions/Defaults+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ extension Defaults.Keys {
)

// Advanced
static let animateWindowResizes = Key<Bool>("animateWindowResizes", default: false, iCloud: true) // BETA
static let useSystemWindowManagerWhenAvailable = Key<Bool>("useSystemWindowManagerWhenAvailable", default: false, iCloud: true)
static let animateWindowResizes = Key<Bool>("animateWindowResizes", default: false, iCloud: true)
static let disableCursorInteraction = Key<Bool>("disableCursorInteraction", default: false, iCloud: true)
static let ignoreFullscreen = Key<Bool>("ignoreFullscreen", default: false, iCloud: true)
static let hideUntilDirectionIsChosen = Key<Bool>("hideUntilDirectionIsChosen", default: false, iCloud: true)
Expand Down
4 changes: 2 additions & 2 deletions Loop/Extensions/NSScreen+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ extension NSScreen {
var stageStripFreeFrame: NSRect {
var frame = visibleFrame

if Defaults[.respectStageManager], StageManager.enabled, StageManager.shown {
if StageManager.position == .leading {
if Defaults[.respectStageManager], SystemWindowManager.StageManager.enabled, SystemWindowManager.StageManager.enabled {
if SystemWindowManager.StageManager.position == .leading {
frame.origin.x += Defaults[.stageStripSize]
}

Expand Down
9 changes: 9 additions & 0 deletions Loop/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -4118,6 +4118,9 @@
}
}
}
},
"Go Back" : {

},
"Gradient" : {
"extractionState" : "manual",
Expand Down Expand Up @@ -9807,6 +9810,9 @@
}
}
}
},
"macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality." : {

},
"Measurement unit: percentage" : {
"extractionState" : "manual",
Expand Down Expand Up @@ -18495,6 +18501,9 @@
}
}
}
},
"Use macOS window manager when available" : {

},
"Use pixels" : {
"extractionState" : "manual",
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 @@ -11,6 +11,10 @@ import Luminare
import SwiftUI

class AdvancedConfigurationModel: ObservableObject {
@Published var useSystemWindowManagerWhenAvailable = Defaults[.useSystemWindowManagerWhenAvailable] {
didSet { Defaults[.useSystemWindowManagerWhenAvailable] = useSystemWindowManagerWhenAvailable }
}

@Published var animateWindowResizes = Defaults[.animateWindowResizes] {
didSet { Defaults[.animateWindowResizes] = animateWindowResizes }
}
Expand Down Expand Up @@ -70,6 +74,9 @@ struct AdvancedConfigurationView: View {

var body: some View {
LuminareSection("General") {
if #available(macOS 15.0, *) {
LuminareToggle("Use macOS window manager when available", isOn: $model.useSystemWindowManagerWhenAvailable)
}
LuminareToggle(
"Animate window resize",
info: .init("This feature is still under development.", .orange),
Expand Down
37 changes: 27 additions & 10 deletions Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class BehaviorConfigurationModel: ObservableObject {
didSet { Defaults[.restoreWindowFrameOnDrag] = restoreWindowFrameOnDrag }
}

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

@Published var enablePadding = Defaults[.enablePadding] {
didSet { Defaults[.enablePadding] = enablePadding }
}
Expand Down Expand Up @@ -74,6 +78,7 @@ class BehaviorConfigurationModel: ObservableObject {
@Published var isPaddingConfigurationViewPresented = false

let previewVisibility = Defaults[.previewVisibility]
let systemSnappingWarning: LuminareInfoView = .init("macOS's \"Tile by dragging windows to screen edges\" feature is currently\nenabled, which will conflict with Loop's window snapping functionality.")
}

struct BehaviorConfigurationView: View {
Expand All @@ -93,17 +98,29 @@ struct BehaviorConfigurationView: View {
}

LuminareSection("Window") {
LuminareToggle("Window snapping", isOn: $model.windowSnapping)
LuminareToggle("Restore window frame on drag", isOn: $model.restoreWindowFrameOnDrag)
LuminareToggle("Include padding", isOn: $model.enablePadding)
if #available(macOS 15, *) {
LuminareToggle(
"Window snapping",
info: SystemWindowManager.MoveAndResize.snappingEnabled ? model.systemSnappingWarning : nil,
isOn: $model.windowSnapping
)
} else {
LuminareToggle("Window snapping", isOn: $model.windowSnapping)
}

if model.enablePadding {
Button("Configure padding…") {
model.isPaddingConfigurationViewPresented = true
}
.luminareModal(isPresented: $model.isPaddingConfigurationViewPresented) {
PaddingConfigurationView(isPresented: $model.isPaddingConfigurationViewPresented)
.frame(width: 400)
// Enabling the system window manager will override these options anyway, so hide them
if !model.useSystemWindowManagerWhenAvailable {
LuminareToggle("Restore window frame on drag", isOn: $model.restoreWindowFrameOnDrag)
LuminareToggle("Include padding", isOn: $model.enablePadding)

if model.enablePadding {
Button("Configure padding…") {
model.isPaddingConfigurationViewPresented = true
}
.luminareModal(isPresented: $model.isPaddingConfigurationViewPresented) {
PaddingConfigurationView(isPresented: $model.isPaddingConfigurationViewPresented)
.frame(width: 400)
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion Loop/Luminare/Settings/Keybindings/KeybindingItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ struct KeybindingItemView: View {
.init(.init(localized: "Size Adjustment"), WindowDirection.sizeAdjustment),
.init(.init(localized: "Shrink"), WindowDirection.shrink),
.init(.init(localized: "Grow"), WindowDirection.grow),
.init(.init(localized: "Move"), WindowDirection.move)
.init(.init(localized: "Move"), WindowDirection.move),
.init(.init(localized: "Go Back"), [WindowDirection.initialFrame, WindowDirection.undo])
]

var moreSection: PickerSection<WindowDirection> {
Expand Down
25 changes: 0 additions & 25 deletions Loop/Managers/StageManager.swift

This file was deleted.

169 changes: 169 additions & 0 deletions Loop/Managers/SystemWindowManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// SystemWindowManager.swift
// Loop
//
// Created by Kai Azim on 2023-12-24.
//

import Defaults
import SwiftUI

class SystemWindowManager {
private static let windowManagerDefaults = UserDefaults(suiteName: "com.apple.WindowManager")
private static let dockDefaults = UserDefaults(suiteName: "com.apple.dock")

// MARK: - Stage Manager

enum StageManager {
static var enabled: Bool {
windowManagerDefaults?.bool(forKey: "GloballyEnabled") ?? false
}

static var shown: Bool {
!(windowManagerDefaults?.bool(forKey: "AutoHide") ?? true)
}

static var position: Edge {
dockDefaults?.string(forKey: "orientation") == "left" ? .trailing : .leading
}
}

// MARK: - Move & Resize

// This is a direct mapping of the menu items in the "Move & Resize" menu
@available(macOS 15, *)
enum MoveAndResize: String {
// General
case minimize = "_performMiniaturize:"
case zoom = "_performZoom:"
case fill = "_zoomFill:"
case center = "_zoomCenter:"

// Halves
case left = "_zoomLeft:"
case right = "_zoomRight:"
case top = "_zoomTop:"
case bottom = "_zoomBottom:"

// Quarters
case topLeft = "_zoomTopLeft:"
case topRight = "_zoomTopRight:"
case bottomLeft = "_zoomBottomLeft:"
case bottomRight = "_zoomBottomRight:"

// Arrange
case leftAndRight = "_zoomLeftAndRight:"
case rightAndLeft = "_zoomRightAndLeft:"
case topAndBottom = "_zoomTopAndBottom:"
case bottomAndTop = "_zoomBottomAndTop:"
case quarters = "_zoomQuarters:"

case returnToPreviousSize = "_zoomUntile:"

static var generalActions: [MoveAndResize] {
[.minimize, .zoom, .fill, .center]
}

static var halvesActions: [MoveAndResize] {
[.left, .right, .top, .bottom]
}

static var quartersActions: [MoveAndResize] {
[.topLeft, .topRight, .bottomLeft, .bottomRight]
}

static var arrangeActions: [MoveAndResize] {
[.leftAndRight, .rightAndLeft, .topAndBottom, .bottomAndTop, .quarters]
}

func getItem(for app: NSRunningApplication) throws -> AXUIElement? {
let pid = app.processIdentifier

// Scan menubar items
let element = AXUIElementCreateApplication(pid)
let menubar = try (element.getValue(.menuBar) as CFTypeRef?) as! AXUIElement
let menubarItems = menubar.children.reversed() // Help menu will be last

for menubarItem in menubarItems {
guard let windowMenuItems = menubarItem.children.first?.children else {
continue
}

if MoveAndResize.generalActions.contains(self),
let menuItem = try windowMenuItems.first(where: { try $0.getValue(.identifier) == rawValue }) {
return menuItem
} else {
let menuItemsWithSubmenu = windowMenuItems.filter { $0.children.first?.children != nil }.map(\.children.first)

for item in menuItemsWithSubmenu {
if let menuItem = try item?.children.first(where: { try $0.getValue(.identifier) as String? == rawValue }) {
return menuItem
}
}
}
}

return nil
}

static var snappingEnabled: Bool {
windowManagerDefaults?.bool(forKey: "EnableTilingByEdgeDrag") ?? false
}

static var padding: CGFloat {
windowManagerDefaults?.bool(forKey: "EnableTiledWindowMargins") ?? false ? 9 : 0
}

static func syncPadding() {
let newPadding = padding

Defaults[.enablePadding] = newPadding != 0

if newPadding != 0 {
Defaults[.padding] = PaddingModel(
window: newPadding,
externalBar: 0,
top: newPadding,
bottom: newPadding,
right: newPadding,
left: newPadding,
configureScreenPadding: false
)
}
}
}
}

@available(macOS 15, *)
extension WindowDirection {
var systemEquivalent: SystemWindowManager.MoveAndResize? {
switch self {
case .minimize:
.minimize
case .maximize:
.fill
case .center:
.center
case .leftHalf:
.left
case .rightHalf:
.right
case .topHalf:
.top
case .bottomHalf:
.bottom
case .topLeftQuarter:
.topLeft
case .topRightQuarter:
.topRight
case .bottomLeftQuarter:
.bottomLeft
case .bottomRightQuarter:
.bottomRight
case .initialFrame:
.returnToPreviousSize
default:
nil
}
}
}
Loading

0 comments on commit 3343ee3

Please sign in to comment.