Skip to content

Commit 33ed1e1

Browse files
MyronKochclaude
andcommitted
Add quarter position cycling, revert prefs UI, add restart warning
Quarter actions (Top Left/Right, Bottom Left/Right) now cycle through all four corners on repeated presses, matching sixths/eighths/ninths pattern. Reverts PrefsViewController to original per maintainer feedback. Adds "(restart required)" hint to additional sizes menu checkbox. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a06f491 commit 33ed1e1

9 files changed

Lines changed: 196 additions & 155 deletions

Rectangle.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
98A009BD251253A000CFBF0C /* BottomCenterSixthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A009BC251253A000CFBF0C /* BottomCenterSixthCalculation.swift */; };
135135
98A009BF251253AB00CFBF0C /* BottomRightSixthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A009BE251253AB00CFBF0C /* BottomRightSixthCalculation.swift */; };
136136
98A6EDDD251F3F4A00F74B10 /* SixthsRepeated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A6EDDC251F3F4A00F74B10 /* SixthsRepeated.swift */; };
137+
BB0000000000000000000001 /* QuartersRepeated.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* QuartersRepeated.swift */; };
137138
98A6EDEC2528FFC100F74B10 /* WindowActionCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A6EDEB2528FFC100F74B10 /* WindowActionCategory.swift */; };
138139
98B3559823CE025700E410E0 /* CenteringFixedSizedWindowMover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98B3559723CE025700E410E0 /* CenteringFixedSizedWindowMover.swift */; };
139140
98BEFA482620DEDD00D9D54F /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BEFA472620DEDC00D9D54F /* NSImageExtension.swift */; };
@@ -349,6 +350,7 @@
349350
98A009BC251253A000CFBF0C /* BottomCenterSixthCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomCenterSixthCalculation.swift; sourceTree = "<group>"; };
350351
98A009BE251253AB00CFBF0C /* BottomRightSixthCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRightSixthCalculation.swift; sourceTree = "<group>"; };
351352
98A6EDDC251F3F4A00F74B10 /* SixthsRepeated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SixthsRepeated.swift; sourceTree = "<group>"; };
353+
BB0000000000000000000002 /* QuartersRepeated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuartersRepeated.swift; sourceTree = "<group>"; };
352354
98A6EDEB2528FFC100F74B10 /* WindowActionCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowActionCategory.swift; sourceTree = "<group>"; };
353355
98B3559723CE025700E410E0 /* CenteringFixedSizedWindowMover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteringFixedSizedWindowMover.swift; sourceTree = "<group>"; };
354356
98BEFA472620DEDC00D9D54F /* NSImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = "<group>"; };
@@ -526,6 +528,7 @@
526528
98C6DEEF23CE191700CC0C1E /* GapCalculation.swift */,
527529
9851A5C2251BEBA300ECF78C /* OrientationAware.swift */,
528530
98A6EDDC251F3F4A00F74B10 /* SixthsRepeated.swift */,
531+
BB0000000000000000000002 /* QuartersRepeated.swift */,
529532
866661F1257D248A00A9CD2D /* RepeatedExecutionsInThirdsCalculation.swift */,
530533
30166BCF24F27D6A00A38608 /* SpecifiedCalculation.swift */,
531534
D04CE31127817C9B00BD47B3 /* NinthsRepeated.swift */,
@@ -1081,6 +1084,7 @@
10811084
BB0B804D2EFB0AF900A9B165 /* TopVerticalThirdCalculation.swift in Sources */,
10821085
6490B39D27BF984D0056C220 /* BottomCenterLeftEighthCalculation.swift in Sources */,
10831086
98A6EDDD251F3F4A00F74B10 /* SixthsRepeated.swift in Sources */,
1087+
BB0000000000000000000001 /* QuartersRepeated.swift in Sources */,
10841088
AAADE1AF28CBAB0000036331 /* WindowUtil.swift in Sources */,
10851089
98A009B72512538200CFBF0C /* TopCenterSixthCalculation.swift in Sources */,
10861090
AA69F8402992DCB1001A81AF /* LeftTodoCalculation.swift in Sources */,

Rectangle/PrefsWindow/PrefsViewController.swift

Lines changed: 34 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ class PrefsViewController: NSViewController {
6868

6969
@IBOutlet weak var showMoreButton: NSButton!
7070
@IBOutlet weak var additionalShortcutsStackView: NSStackView!
71-
71+
7272
// Settings
7373
override func awakeFromNib() {
74-
75-
// Main section shortcuts (storyboard-connected)
74+
7675
actionsToViews = [
7776
.leftHalf: leftHalfShortcutView,
7877
.rightHalf: rightHalfShortcutView,
@@ -92,135 +91,49 @@ class PrefsViewController: NSViewController {
9291
.larger: makeLargerShortcutView,
9392
.smaller: makeSmallerShortcutView,
9493
.restore: restoreShortcutView,
94+
.firstThird: firstThirdShortcutView,
95+
.firstTwoThirds: firstTwoThirdsShortcutView,
96+
.centerThird: centerThirdShortcutView,
97+
.centerTwoThirds: centerTwoThirdsShortcutView,
98+
.lastTwoThirds: lastTwoThirdsShortcutView,
99+
.lastThird: lastThirdShortcutView,
100+
.moveLeft: moveLeftShortcutView,
101+
.moveRight: moveRightShortcutView,
102+
.moveUp: moveUpShortcutView,
103+
.moveDown: moveDownShortcutView,
104+
.firstFourth: firstFourthShortcutView,
105+
.secondFourth: secondFourthShortcutView,
106+
.thirdFourth: thirdFourthShortcutView,
107+
.lastFourth: lastFourthShortcutView,
108+
.firstThreeFourths: firstThreeFourthsShortcutView,
109+
.centerThreeFourths: centerThreeFourthsShortcutView,
110+
.lastThreeFourths: lastThreeFourthsShortcutView,
111+
.topLeftSixth: topLeftSixthShortcutView,
112+
.topCenterSixth: topCenterSixthShortcutView,
113+
.topRightSixth: topRightSixthShortcutView,
114+
.bottomLeftSixth: bottomLeftSixthShortcutView,
115+
.bottomCenterSixth: bottomCenterSixthShortcutView,
116+
.bottomRightSixth: bottomRightSixthShortcutView
95117
]
96-
97-
// Replace the storyboard additional section with simplified category rows
98-
setupCategoryShortcuts()
99-
118+
100119
for (action, view) in actionsToViews {
101120
view.setAssociatedUserDefaultsKey(action.name, withTransformerName: MASDictionaryTransformerName)
102121
}
103-
122+
104123
if Defaults.allowAnyShortcut.enabled {
105124
let passThroughValidator = PassthroughShortcutValidator()
106125
actionsToViews.values.forEach { $0.shortcutValidator = passThroughValidator }
107126
}
108-
127+
109128
subscribeToAllowAnyShortcutToggle()
110-
111-
// Default the extra shortcuts section to open
112-
additionalShortcutsStackView.isHidden = false
113-
showMoreButton.title = ""
114-
}
115-
116-
private var extraSectionsAdded = false
117-
118-
/// Replaces the storyboard's individual shortcut rows with one row per category.
119-
/// Each category shortcut cycles through all positions on repeated presses.
120-
private func setupCategoryShortcuts() {
121-
guard !extraSectionsAdded else { return }
122-
extraSectionsAdded = true
123-
124-
guard let leftColumn = additionalShortcutsStackView.arrangedSubviews.first as? NSStackView,
125-
let rightColumn = additionalShortcutsStackView.arrangedSubviews.last as? NSStackView else { return }
126-
127-
// Clear all existing storyboard rows (individual thirds, fourths, sixths, move)
128-
for view in leftColumn.arrangedSubviews {
129-
leftColumn.removeArrangedSubview(view)
130-
view.removeFromSuperview()
131-
}
132-
for view in rightColumn.arrangedSubviews {
133-
rightColumn.removeArrangedSubview(view)
134-
view.removeFromSuperview()
135-
}
136-
137-
// Grid Layout categories - one shortcut each, cycles through all positions
138-
leftColumn.addArrangedSubview(createShortcutRow(for: .firstThird, label: "Thirds"))
139-
leftColumn.addArrangedSubview(createShortcutRow(for: .firstFourth, label: "Fourths"))
140-
leftColumn.addArrangedSubview(createShortcutRow(for: .topLeftSixth, label: "Sixths"))
141-
142-
rightColumn.addArrangedSubview(createShortcutRow(for: .topLeftEighth, label: "Eighths"))
143-
rightColumn.addArrangedSubview(createShortcutRow(for: .topLeftTwelfth, label: "Twelfths"))
144-
rightColumn.addArrangedSubview(createShortcutRow(for: .topLeftSixteenth, label: "Sixteenths"))
145-
129+
130+
additionalShortcutsStackView.isHidden = true
146131
}
147-
148-
private func createSectionSpacer() -> NSView {
149-
let spacer = NSView()
150-
spacer.translatesAutoresizingMaskIntoConstraints = false
151-
spacer.heightAnchor.constraint(equalToConstant: 5).isActive = true
152-
return spacer
153-
}
154-
155-
/// Creates a left-aligned bold label used as a section header in the left column.
156-
/// The matching right column gets only a separator via createSectionSeparator().
157-
private func createSectionHeader(title: String) -> NSView {
158-
let separator = createSectionSeparator()
159-
160-
let label = NSTextField(labelWithString: title)
161-
label.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
162-
label.textColor = .secondaryLabelColor
163-
label.alignment = .left
164-
label.translatesAutoresizingMaskIntoConstraints = false
165-
166-
let container = NSStackView(views: [separator, label])
167-
container.orientation = .vertical
168-
container.alignment = .leading
169-
container.spacing = 4
170-
container.translatesAutoresizingMaskIntoConstraints = false
171-
return container
172-
}
173-
174-
/// Creates a full-width NSBox horizontal separator line.
175-
private func createSectionSeparator() -> NSBox {
176-
let box = NSBox()
177-
box.boxType = .separator
178-
box.translatesAutoresizingMaskIntoConstraints = false
179-
return box
180-
}
181-
182-
private func createShortcutRow(for action: WindowAction, label customLabel: String? = nil) -> NSStackView {
183-
let shortcutView = MASShortcutView()
184-
shortcutView.translatesAutoresizingMaskIntoConstraints = false
185-
shortcutView.widthAnchor.constraint(equalToConstant: 160).isActive = true
186-
shortcutView.heightAnchor.constraint(equalToConstant: 19).isActive = true
187-
188-
let label = customLabel ?? action.displayName ?? action.name
189-
let textField = NSTextField(labelWithString: label)
190-
textField.alignment = .right
191-
textField.lineBreakMode = .byClipping
192-
textField.translatesAutoresizingMaskIntoConstraints = false
193-
textField.setContentHuggingPriority(.init(251), for: .horizontal)
194-
textField.setContentHuggingPriority(.init(750), for: .vertical)
195-
196-
let imageView = NSImageView()
197-
imageView.translatesAutoresizingMaskIntoConstraints = false
198-
imageView.widthAnchor.constraint(equalToConstant: 21).isActive = true
199-
imageView.heightAnchor.constraint(equalToConstant: 14).isActive = true
200-
imageView.image = action.image
201-
imageView.imageScaling = .scaleProportionallyDown
202-
imageView.setContentHuggingPriority(.init(251), for: .horizontal)
203-
imageView.setContentHuggingPriority(.init(251), for: .vertical)
204-
205-
let labelStack = NSStackView(views: [textField, imageView])
206-
labelStack.orientation = .horizontal
207-
labelStack.alignment = .centerY
208-
labelStack.distribution = .fill
209-
210-
let row = NSStackView(views: [labelStack, shortcutView])
211-
row.orientation = .horizontal
212-
row.alignment = .centerY
213-
row.distribution = .fill
214-
row.spacing = 18
215-
216-
actionsToViews[action] = shortcutView
217-
return row
218-
}
219-
132+
220133
@IBAction func toggleShowMore(_ sender: NSButton) {
221-
let hide = !additionalShortcutsStackView.isHidden
222-
additionalShortcutsStackView.isHidden = hide
223-
showMoreButton.title = hide ? "▶︎ ⋯" : ""
134+
additionalShortcutsStackView.isHidden = !additionalShortcutsStackView.isHidden
135+
showMoreButton.title = additionalShortcutsStackView.isHidden
136+
? "▶︎ ⋯" : ""
224137
}
225138

226139
private func subscribeToAllowAnyShortcutToggle() {

Rectangle/PrefsWindow/SettingsViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ class SettingsViewController: NSViewController {
740740
mainStackView.addArrangedSubview(bottomCenterRightEighthRow)
741741
mainStackView.addArrangedSubview(bottomRightEighthRow)
742742

743-
let showAdditionalSizesCheckbox = NSButton(checkboxWithTitle: NSLocalizedString("Show additional sizes in menu", tableName: "Main", value: "", comment: ""), target: self, action: #selector(toggleShowAdditionalSizesInMenu(_:)))
743+
let showAdditionalSizesCheckbox = NSButton(checkboxWithTitle: NSLocalizedString("Show additional sizes in menu (restart required)", tableName: "Main", value: "", comment: ""), target: self, action: #selector(toggleShowAdditionalSizesInMenu(_:)))
744744
showAdditionalSizesCheckbox.state = Defaults.showAdditionalSizesInMenu.userEnabled ? .on : .off
745745
showAdditionalSizesCheckbox.translatesAutoresizingMaskIntoConstraints = false
746746
showAdditionalSizesCheckbox.alignment = .right

Rectangle/WindowAction.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,12 @@ enum SubWindowAction {
931931
topRightThird,
932932
bottomLeftThird,
933933
bottomRightThird,
934-
934+
935+
topLeftQuarter,
936+
topRightQuarter,
937+
bottomLeftQuarter,
938+
bottomRightQuarter,
939+
935940
topLeftEighth,
936941
topCenterLeftEighth,
937942
topCenterRightEighth,
@@ -1037,6 +1042,10 @@ enum SubWindowAction {
10371042
case .topRightThird: return [.left, .bottom]
10381043
case .bottomLeftThird: return [.right, .top]
10391044
case .bottomRightThird: return [.left, .top]
1045+
case .topLeftQuarter: return [.right, .bottom]
1046+
case .topRightQuarter: return [.left, .bottom]
1047+
case .bottomLeftQuarter: return [.right, .top]
1048+
case .bottomRightQuarter: return [.left, .top]
10401049
case .topLeftEighth: return [.right, .bottom]
10411050
case .topCenterLeftEighth: return [.right, .left, .bottom]
10421051
case .topCenterRightEighth: return [.right, .left, .bottom]

Rectangle/WindowCalculation/LowerLeftCalculation.swift

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,41 @@
88

99
import Foundation
1010

11-
class LowerLeftCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculation {
11+
class LowerLeftCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculation, QuartersRepeated {
1212

1313
override func calculateRect(_ params: RectCalculationParameters) -> RectResult {
1414

15-
if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes {
16-
return calculateFirstRect(params)
15+
guard Defaults.subsequentExecutionMode.value != .none,
16+
let last = params.lastAction,
17+
let lastSubAction = last.subAction
18+
else {
19+
return quarterRect(params.visibleFrameOfScreen)
1720
}
18-
19-
return calculateRepeatedRect(params)
21+
22+
if last.action != .bottomLeft && lastSubAction != .bottomLeftQuarter {
23+
return quarterRect(params.visibleFrameOfScreen)
24+
}
25+
26+
if let calculation = self.nextCalculation(subAction: lastSubAction, direction: .right) {
27+
return calculation(params.visibleFrameOfScreen)
28+
}
29+
30+
return quarterRect(params.visibleFrameOfScreen)
2031
}
21-
32+
33+
func quarterRect(_ visibleFrameOfScreen: CGRect) -> RectResult {
34+
var rect = visibleFrameOfScreen
35+
rect.size.width = floor(visibleFrameOfScreen.width / 2.0)
36+
rect.size.height = floor(visibleFrameOfScreen.height / 2.0)
37+
return RectResult(rect, subAction: .bottomLeftQuarter)
38+
}
39+
2240
func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult {
2341
let visibleFrameOfScreen = params.visibleFrameOfScreen
2442

2543
var rect = visibleFrameOfScreen
26-
2744
rect.size.width = floor(visibleFrameOfScreen.width * CGFloat(fraction))
28-
2945
rect.size.height = floor(visibleFrameOfScreen.height / 2.0)
30-
3146
return RectResult(rect)
3247
}
3348
}

Rectangle/WindowCalculation/LowerRightCalculation.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,43 @@
88

99
import Foundation
1010

11-
class LowerRightCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculation {
11+
class LowerRightCalculation: WindowCalculation, RepeatedExecutionsInThirdsCalculation, QuartersRepeated {
1212

1313
override func calculateRect(_ params: RectCalculationParameters) -> RectResult {
1414

15-
if params.lastAction == nil || !Defaults.subsequentExecutionMode.resizes {
16-
return calculateFirstRect(params)
15+
guard Defaults.subsequentExecutionMode.value != .none,
16+
let last = params.lastAction,
17+
let lastSubAction = last.subAction
18+
else {
19+
return quarterRect(params.visibleFrameOfScreen)
1720
}
18-
19-
return calculateRepeatedRect(params)
21+
22+
if last.action != .bottomRight && lastSubAction != .bottomRightQuarter {
23+
return quarterRect(params.visibleFrameOfScreen)
24+
}
25+
26+
if let calculation = self.nextCalculation(subAction: lastSubAction, direction: .right) {
27+
return calculation(params.visibleFrameOfScreen)
28+
}
29+
30+
return quarterRect(params.visibleFrameOfScreen)
2031
}
21-
32+
33+
func quarterRect(_ visibleFrameOfScreen: CGRect) -> RectResult {
34+
var rect = visibleFrameOfScreen
35+
rect.size.width = floor(visibleFrameOfScreen.width / 2.0)
36+
rect.origin.x = visibleFrameOfScreen.maxX - rect.width
37+
rect.size.height = floor(visibleFrameOfScreen.height / 2.0)
38+
return RectResult(rect, subAction: .bottomRightQuarter)
39+
}
40+
2241
func calculateFractionalRect(_ params: RectCalculationParameters, fraction: Float) -> RectResult {
2342
let visibleFrameOfScreen = params.visibleFrameOfScreen
2443

2544
var rect = visibleFrameOfScreen
26-
2745
rect.size.width = floor(visibleFrameOfScreen.width * CGFloat(fraction))
2846
rect.origin.x = visibleFrameOfScreen.maxX - rect.width
2947
rect.size.height = floor(visibleFrameOfScreen.height / 2.0)
30-
3148
return RectResult(rect)
3249
}
3350
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// QuartersRepeated.swift
3+
// Rectangle
4+
//
5+
// Copyright © 2026 Ryan Hanson. All rights reserved.
6+
//
7+
8+
import Foundation
9+
10+
protocol QuartersRepeated {
11+
func nextCalculation(subAction: SubWindowAction, direction: Direction) -> SimpleCalc?
12+
}
13+
14+
extension QuartersRepeated {
15+
func nextCalculation(subAction: SubWindowAction, direction: Direction) -> SimpleCalc? {
16+
17+
if direction == .left {
18+
switch subAction {
19+
case .topLeftQuarter:
20+
return WindowCalculationFactory.lowerRightCalculation.quarterRect
21+
case .topRightQuarter:
22+
return WindowCalculationFactory.upperLeftCalculation.quarterRect
23+
case .bottomLeftQuarter:
24+
return WindowCalculationFactory.upperRightCalculation.quarterRect
25+
case .bottomRightQuarter:
26+
return WindowCalculationFactory.lowerLeftCalculation.quarterRect
27+
default: break
28+
}
29+
}
30+
31+
else if direction == .right {
32+
switch subAction {
33+
case .topLeftQuarter:
34+
return WindowCalculationFactory.upperRightCalculation.quarterRect
35+
case .topRightQuarter:
36+
return WindowCalculationFactory.lowerLeftCalculation.quarterRect
37+
case .bottomLeftQuarter:
38+
return WindowCalculationFactory.lowerRightCalculation.quarterRect
39+
case .bottomRightQuarter:
40+
return WindowCalculationFactory.upperLeftCalculation.quarterRect
41+
default: break
42+
}
43+
}
44+
45+
return nil
46+
}
47+
}

0 commit comments

Comments
 (0)