Skip to content
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
16 changes: 16 additions & 0 deletions Rectangle/CooperativeCornerResize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ struct CooperativeCornerResize {
}
}

/// Both edges along the axis moved: the window jumped to the other side (the `switchSides`
/// repeated-command mode) rather than resizing against a shared edge, so no neighbor gives way.
static func isSideSwitch(oldFocusedFrame: CGRect,
newFocusedFrame: CGRect,
axis: CornerCycleExpansionAxis,
tolerance: CGFloat) -> Bool {
switch axis {
case .horizontal:
return abs(oldFocusedFrame.minX - newFocusedFrame.minX) > tolerance
&& abs(oldFocusedFrame.maxX - newFocusedFrame.maxX) > tolerance
case .vertical:
return abs(oldFocusedFrame.minY - newFocusedFrame.minY) > tolerance
&& abs(oldFocusedFrame.maxY - newFocusedFrame.maxY) > tolerance
}
}

static func movedEdge(from oldFocusedFrame: CGRect,
to newFocusedFrame: CGRect,
axis: CornerCycleExpansionAxis,
Expand Down
10 changes: 10 additions & 0 deletions Rectangle/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -30265,6 +30265,16 @@
}
}
},
"Switch to the opposite side on half actions" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "半屏操作切换到对侧"
}
}
}
},
"Third Fourth" : {
"localizations" : {
"ar" : {
Expand Down
4 changes: 3 additions & 1 deletion Rectangle/SubsequentExecutionMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum SubsequentExecutionMode: Int {
case acrossAndResize = 3 // across monitor for right/left, spectacle resize for all else
case cycleMonitor = 4
case resizeAndCycleQuadrants = 5
case switchSides = 6 // repeated half actions move to the opposite side

var title: String {
switch self {
Expand All @@ -18,6 +19,7 @@ enum SubsequentExecutionMode: Int {
case .acrossAndResize: return String(localized: "move to adjacent on left/right, or cycle size on side")
case .cycleMonitor: return String(localized: "cycle through displays")
case .resizeAndCycleQuadrants: return String(localized: "cycle positions on quadrants, cycle sizes on side")
case .switchSides: return String(localized: "Switch to the opposite side on half actions")
}
}

Expand All @@ -28,7 +30,7 @@ enum SubsequentExecutionMode: Int {
}
}

static var ordered: [SubsequentExecutionMode] = [.none, .cycleMonitor, .resize, .acrossMonitor, .acrossAndResize, .resizeAndCycleQuadrants]
static var ordered: [SubsequentExecutionMode] = [.none, .cycleMonitor, .resize, .acrossMonitor, .acrossAndResize, .resizeAndCycleQuadrants, .switchSides]
}

class SubsequentExecutionDefault: Default {
Expand Down
9 changes: 9 additions & 0 deletions Rectangle/WindowCalculation/BottomHalfCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ class BottomHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalcul
return tiledRect
}

if Defaults.subsequentExecutionMode.value == .switchSides {
// Repeating Bottom Half while the window still sits on the bottom half moves it to the top
// half; see LeftRightHalfCalculation.calculateSwitchSides.
guard isRepeatedCommand(params) else { return calculateFirstRect(params) }
let opposite = RectCalculationParameters(window: params.window, visibleFrameOfScreen: params.visibleFrameOfScreen,
action: .topHalf, lastAction: params.lastAction)
return RectResult(WindowCalculationFactory.topHalfCalculation.calculateFirstRect(opposite).rect, resultingAction: .topHalf)
}

if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes {
return calculateFirstRect(params)
}
Expand Down
14 changes: 13 additions & 1 deletion Rectangle/WindowCalculation/LeftRightHalfCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal
return calculateAcrossDisplays(params)
case .resize, .resizeAndCycleQuadrants:
return calculateResize(params)
case .none, .cycleMonitor:
case .switchSides where isRepeatedCommand(params):
return calculateSwitchSides(params)
case .none, .cycleMonitor, .switchSides:
let screen = usableScreens.currentScreen
let oneHalfRect = calculateFirstRect(params.asRectParams())
return WindowCalculationResult(rect: oneHalfRect.rect, screen: screen, resultingAction: params.action)
Expand Down Expand Up @@ -56,6 +58,16 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal
return WindowCalculationResult(rect: rectResult.rect, screen: screen, resultingAction: params.action)
}

/// Repeating a half action while the window still sits on that half moves it to the opposite
/// half (Left Half → Right Half → Left Half). Reporting the opposite half as the resulting action
/// is what makes the next repeat flip back; a first execution, or a window that has moved since,
/// gets a plain half above.
func calculateSwitchSides(_ params: WindowCalculationParameters) -> WindowCalculationResult? {
let opposite: WindowAction = params.action == .rightHalf ? .leftHalf : .rightHalf
let oppositeRect = calculateFirstRect(params.asRectParams(differentAction: opposite))
return WindowCalculationResult(rect: oppositeRect.rect, screen: params.usableScreens.currentScreen, resultingAction: opposite)
}

func calculateAcrossDisplays(_ params: WindowCalculationParameters) -> WindowCalculationResult? {
let screen = params.usableScreens.currentScreen
return params.action == .rightHalf
Expand Down
9 changes: 9 additions & 0 deletions Rectangle/WindowCalculation/TopHalfCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ class TopHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculati
return tiledRect
}

if Defaults.subsequentExecutionMode.value == .switchSides {
// Repeating Top Half while the window still sits on the top half moves it to the bottom
// half; see LeftRightHalfCalculation.calculateSwitchSides.
guard isRepeatedCommand(params) else { return calculateFirstRect(params) }
let opposite = RectCalculationParameters(window: params.window, visibleFrameOfScreen: params.visibleFrameOfScreen,
action: .bottomHalf, lastAction: params.lastAction)
return RectResult(WindowCalculationFactory.bottomHalfCalculation.calculateFirstRect(opposite).rect, resultingAction: .bottomHalf)
}

if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes {
return calculateFirstRect(params)
}
Expand Down
17 changes: 12 additions & 5 deletions Rectangle/WindowCalculation/WindowCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@ class WindowCalculation: Calculation {
}

func isRepeatedCommand(_ params: WindowCalculationParameters) -> Bool {
if let lastAction = params.lastAction, lastAction.action == params.action {
let normalizedLastRect = lastAction.rect.screenFlipped
return normalizedLastRect == params.window.rect
}
return false
isRepeatedCommand(lastAction: params.lastAction, action: params.action, windowRect: params.window.rect)
}

func isRepeatedCommand(_ params: RectCalculationParameters) -> Bool {
isRepeatedCommand(lastAction: params.lastAction, action: params.action, windowRect: params.window.rect)
}

/// The same action was just executed on this window and the window has not moved since.
private func isRepeatedCommand(lastAction: RectangleAction?, action: WindowAction, windowRect: CGRect) -> Bool {
guard let lastAction, lastAction.action == action else { return false }
let normalizedLastRect = lastAction.rect.screenFlipped
return normalizedLastRect == windowRect
}

}
Expand Down
10 changes: 10 additions & 0 deletions Rectangle/WindowManager+CooperativeCornerResize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ extension WindowManager {
let tolerance = CooperativeCornerResize.detectionTolerance(screenFrame: screenFrame, configuredGap: gapSize)
let captureTolerance = CooperativeCornerResize.captureTolerance(screenFrame: screenFrame, axis: cooperativeAxis)
let isRepeatedCooperativeAction = action.isCompatibleRepeatedResizeAction(with: lastRectangleAction?.action)
if isRepeatedCooperativeAction, !action.isCooperativeCornerAction,
Defaults.subsequentExecutionMode.value == .switchSides,
CooperativeCornerResize.isSideSwitch(oldFocusedFrame: oldFocusedFrame, newFocusedFrame: newFocusedFrame,
axis: cooperativeAxis, tolerance: tolerance) {
// A repeated half action that switched sides is a move, not a resize of the shared edge:
// planned as one, the action's moved edge would be pushed across the screen, squeezing the
// neighbor and recording a wrong split ratio. Corners cycle quadrant positions through their
// own cooperative path and keep it.
return nil
}
let actionDescription = isRepeatedCooperativeAction ? "repeated cooperative resize" : "initial corner/side cooperative placement"
let screenFrameAX = screenFrame.screenFlipped
let elementsById = AccessibilityElement.getAllWindowElements().reduce(into: [CGWindowID: AccessibilityElement]()) { elements, element in
Expand Down
Loading
Loading