Skip to content
Open
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
9 changes: 4 additions & 5 deletions Rectangle/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,12 @@ extension AppDelegate: NSMenuDelegate {
if frontmostWindow == nil {
menuItem.isEnabled = false
}
if screenCount == 1
&& (windowAction == .nextDisplay || windowAction == .previousDisplay) {
menuItem.isEnabled = false
if windowAction == .nextDisplay || windowAction == .previousDisplay {
menuItem.isHidden = screenCount == 1 || Defaults.combinedDisplayMode.enabled
}
}
}

func menuDidClose(_ menu: NSMenu) {
for menuItem in menu.items {

Expand All @@ -397,7 +396,7 @@ extension AppDelegate: NSMenuDelegate {
guard let windowAction = sender.representedObject as? WindowAction else { return }
windowAction.postMenu()
}

func addWindowActionMenuItems() {
let additionalSizeCategories: Set<WindowActionCategory> = [.eighths, .ninths, .twelfths, .sixteenths]
let submenuOnlyWhenAdditional: Set<WindowActionCategory> = [.thirds, .size]
Expand Down
1 change: 1 addition & 0 deletions Rectangle/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class Defaults {
static let systemWideMouseDownApps = JSONDefault<Set<String>>(key:"systemWideMouseDownApps", defaultValue: Set<String>(["org.languagetool.desktop", "com.microsoft.teams2"]))
static let internalTilingNotified = BoolDefault(key: "internalTilingNotified")
static let screensOrderedByX = OptionalBoolDefault(key: "screensOrderedByX")
static let combinedDisplayMode = BoolDefault(key: "combinedDisplayMode")
static var array: [Default] = [
launchOnLogin,
disabledApps,
Expand Down
43 changes: 41 additions & 2 deletions Rectangle/PrefsWindow/SettingsViewController.swift
Copy link
Copy Markdown
Author

@dooyeoung dooyeoung May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preview image
Image

Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ class SettingsViewController: NSViewController {

private var aboutTodoWindowController: NSWindowController?
private var extraSettingsPopover: NSPopover?

private var cycleSizeCheckboxes = [NSButton]()
private var combinedDisplayModeCheckbox: NSButton?

@IBAction func toggleLaunchOnLogin(_ sender: NSButton) {
let newSetting: Bool = sender.state == .on
Expand Down Expand Up @@ -145,6 +146,10 @@ class SettingsViewController: NSViewController {
Notification.Name.windowTitleBar.post()
}

@objc func toggleCombinedDisplayMode(_ sender: NSButton) {
Defaults.combinedDisplayMode.enabled = sender.state == .on
}

@IBAction func toggleTodoMode(_ sender: NSButton) {
let newSetting: Bool = sender.state == .on
Defaults.todo.enabled = newSetting
Expand Down Expand Up @@ -948,7 +953,39 @@ class SettingsViewController: NSViewController {
self.cycleSizeCheckboxes = cycleSizeCheckboxes

initializeCycleSizesView(animated: false)


if combinedDisplayModeCheckbox == nil,
let parentStack = doubleClickTitleBarCheckbox.superview as? NSStackView,
let insertIdx = parentStack.arrangedSubviews.firstIndex(of: doubleClickTitleBarCheckbox) {
let checkbox = NSButton(checkboxWithTitle: NSLocalizedString("Treat multiple monitors as one", tableName: "Main", value: "", comment: ""), target: self, action: #selector(toggleCombinedDisplayMode(_:)))
checkbox.state = Defaults.combinedDisplayMode.enabled ? .on : .off
// Match storyboard checkbox content priorities to prevent vertical compression
checkbox.setContentCompressionResistancePriority(.required, for: .vertical)
checkbox.setContentHuggingPriority(.defaultHigh, for: .vertical)

// Must be set before inserting: NSStackView queries intrinsicContentSize once on
// insertion, so preferredMaxLayoutWidth=0 would give zero height permanently.
let descLabel = NSTextField(wrappingLabelWithString: NSLocalizedString("When using multiple monitors, treats them as a single display. Requires System Settings > Desktop & Dock > Displays have separate Spaces to be OFF.", tableName: "Main", value: "", comment: ""))
descLabel.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
descLabel.textColor = .secondaryLabelColor
descLabel.translatesAutoresizingMaskIntoConstraints = false
descLabel.preferredMaxLayoutWidth = 500
descLabel.setContentCompressionResistancePriority(.required, for: .vertical)
descLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)

let separator = NSBox()
separator.boxType = .separator
separator.translatesAutoresizingMaskIntoConstraints = false
separator.setContentHuggingPriority(.defaultHigh, for: .vertical)

parentStack.insertArrangedSubview(separator, at: insertIdx + 1)
parentStack.insertArrangedSubview(checkbox, at: insertIdx + 2)
parentStack.insertArrangedSubview(descLabel, at: insertIdx + 3)
separator.widthAnchor.constraint(equalTo: doubleClickTitleBarCheckbox.widthAnchor).isActive = true
separator.heightAnchor.constraint(equalToConstant: 20).isActive = true
combinedDisplayModeCheckbox = checkbox
}

Notification.Name.configImported.onPost(using: {_ in
self.initializeTodoModeSettings()
self.initializeToggles()
Expand Down Expand Up @@ -1011,6 +1048,8 @@ class SettingsViewController: NSViewController {

doubleClickTitleBarCheckbox.state = WindowAction(rawValue: Defaults.doubleClickTitleBar.value - 1) != nil ? .on : .off

combinedDisplayModeCheckbox?.state = Defaults.combinedDisplayMode.enabled ? .on : .off

if StageUtil.stageCapable {
stageSlider.intValue = Int32(Defaults.stageSize.value)
stageSlider.isContinuous = true
Expand Down
6 changes: 5 additions & 1 deletion Rectangle/ScreenDetection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ extension NSScreen {

func adjustedVisibleFrame(_ ignoreTodo: Bool = false, _ ignoreStage: Bool = false) -> CGRect {
var newFrame = visibleFrame


if !NSScreen.screensHaveSeparateSpaces && Defaults.combinedDisplayMode.enabled {
return NSScreen.screens.reduce(CGRect.null) { $0.union($1.visibleFrame) }
}

if !ignoreStage && Defaults.stageSize.value > 0 {
if StageUtil.stageCapable && StageUtil.stageEnabled && StageUtil.stageStripShow && StageUtil.isStageStripVisible(self) {
let stageSize = Defaults.stageSize.value < 1
Expand Down
4 changes: 2 additions & 2 deletions Rectangle/WindowCalculation/CenterCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class CenterCalculation: WindowCalculation {
if !Defaults.alwaysAccountForStage.userEnabled {
screenFrame = params.usableScreens.currentScreen.adjustedVisibleFrame(params.ignoreTodo, true)
}

let rectResult = calculateRect(params.asRectParams(visibleFrame: screenFrame))

let resultingAction: WindowAction = rectResult.resultingAction ?? params.action

return WindowCalculationResult(rect: rectResult.rect,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import Foundation
class CenterProminentlyCalculation: WindowCalculation {

override func calculate(_ params: WindowCalculationParameters) -> WindowCalculationResult? {

var screenFrame: CGRect?
if !Defaults.alwaysAccountForStage.userEnabled {
screenFrame = params.usableScreens.currentScreen.adjustedVisibleFrame(params.ignoreTodo, true)
}

let rectResult = calculateRect(params.asRectParams(visibleFrame: screenFrame))

let resultingAction: WindowAction = rectResult.resultingAction ?? params.action

return WindowCalculationResult(rect: rectResult.rect,
Expand Down
10 changes: 5 additions & 5 deletions Rectangle/WindowCalculation/LeftRightHalfCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,21 @@ class LeftRightHalfCalculation: WindowCalculation, RepeatedExecutionsInThirdsCal
let oneHalfRect = calculateFirstRect(params.asRectParams(visibleFrame: screen.adjustedVisibleFrame(params.ignoreTodo), differentAction: .leftHalf))
return WindowCalculationResult(rect: oneHalfRect.rect, screen: screen, resultingAction: .leftHalf)
}


func calculateRightAcrossDisplays(_ params: WindowCalculationParameters, screen: NSScreen) -> WindowCalculationResult? {

if isRepeatedCommand(params) {
if let nextScreen = params.usableScreens.adjacentScreens?.next {

if Defaults.subsequentExecutionMode.value == .acrossAndResize && nextScreen == params.usableScreens.screensOrdered.first {
return calculateResize(params)
}

return calculateLeftAcrossDisplays(params.withDifferentAction(.leftHalf), screen: nextScreen)
}
}

let oneHalfRect = calculateFirstRect(params.asRectParams(visibleFrame: screen.adjustedVisibleFrame(params.ignoreTodo), differentAction: .rightHalf))
return WindowCalculationResult(rect: oneHalfRect.rect, screen: screen, resultingAction: .rightHalf)
}
Expand Down
4 changes: 2 additions & 2 deletions Rectangle/WindowCalculation/WindowCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ struct WindowCalculationParameters {
let action: WindowAction
let lastAction: RectangleAction?
let ignoreTodo: Bool

func asRectParams(visibleFrame: CGRect? = nil, differentAction: WindowAction? = nil) -> RectCalculationParameters {
RectCalculationParameters(window: window,
visibleFrameOfScreen: visibleFrame ?? usableScreens.currentScreen.adjustedVisibleFrame(ignoreTodo),
action: differentAction ?? action,
lastAction: lastAction)
}

func withDifferentAction(_ differentAction: WindowAction) -> WindowCalculationParameters {
.init(window: window,
usableScreens: usableScreens,
Expand Down
20 changes: 20 additions & 0 deletions Rectangle/mul.lproj/Main.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -38945,6 +38945,26 @@
}
}
},
"Treat multiple monitors as one" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "์—ฌ๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ฅผ ํ•˜๋‚˜๋กœ ์ธ์‹"
}
}
}
},
"When using multiple monitors, treats them as a single display. Requires System Settings > Desktop & Dock > Displays have separate Spaces to be OFF." : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "๋ชจ๋‹ˆํ„ฐ ์—ฌ๋Ÿฌ๊ฐœ ์‚ฌ์šฉ์‹œ ํ•˜๋‚˜๋กœ ๋ชจ๋‹ˆํ„ฐ๋กœ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค\n์„ค์ • > ๋ฐ์Šคํฌํƒ‘ ๋ฐ dock > ๊ฐ๊ฐ์˜ spaces๊ฐ€ ์žˆ๋Š” ๋””์Šคํ”Œ๋ ˆ์ด ์„ค์ •์ด off ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
}
}
}
},
"Sixteenths" : {
"localizations" : {
"ko" : {
Expand Down