Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Center Two Screens action #1314

Closed
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
4 changes: 4 additions & 0 deletions Rectangle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
12D896200936C0DE9FF41FB4 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
292B2B982B36A57100F7B6B0 /* CenterTwoScreensCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292B2B972B36A57100F7B6B0 /* CenterTwoScreensCalculation.swift */; };
30166BD024F27D6A00A38608 /* SpecifiedCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30166BCF24F27D6A00A38608 /* SpecifiedCalculation.swift */; };
6490B39127BF907A0056C220 /* BottomLeftEighthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6490B39027BF907A0056C220 /* BottomLeftEighthCalculation.swift */; };
6490B39327BF90F90056C220 /* EighthsRepeated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6490B39227BF90F90056C220 /* EighthsRepeated.swift */; };
Expand Down Expand Up @@ -169,6 +170,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
292B2B972B36A57100F7B6B0 /* CenterTwoScreensCalculation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenterTwoScreensCalculation.swift; sourceTree = "<group>"; };
30166BCF24F27D6A00A38608 /* SpecifiedCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecifiedCalculation.swift; sourceTree = "<group>"; };
423DC1992AE681F900C98564 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = "<group>"; };
42627A972ADA03D200D047C6 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -440,6 +442,7 @@
D04CE30927817A9F00BD47B3 /* MiddleRightNinthCalculation.swift */,
D04CE30B27817AA900BD47B3 /* BottomLeftNinthCalculation.swift */,
D04CE30D27817AB500BD47B3 /* BottomCenterNinthCalculation.swift */,
292B2B972B36A57100F7B6B0 /* CenterTwoScreensCalculation.swift */,
D04CE30F27817ABE00BD47B3 /* BottomRightNinthCalculation.swift */,
D0CFE33027A8CAED004DA47B /* TopLeftThirdCalculation.swift */,
D0CFE33227A8CCB1004DA47B /* TopRightThirdCalculation.swift */,
Expand Down Expand Up @@ -931,6 +934,7 @@
9824704F22B189250037B409 /* BestEffortWindowMover.swift in Sources */,
30166BD024F27D6A00A38608 /* SpecifiedCalculation.swift in Sources */,
9824703922B0F37C0037B409 /* ShortcutManager.swift in Sources */,
292B2B982B36A57100F7B6B0 /* CenterTwoScreensCalculation.swift in Sources */,
98D16A492592B460005228CB /* NotificationExtension.swift in Sources */,
AA0AC000291C1B5E00D125D2 /* CGExtension.swift in Sources */,
6490B39D27BF984D0056C220 /* BottomCenterLeftEighthCalculation.swift in Sources */,
Expand Down
13 changes: 10 additions & 3 deletions Rectangle/WindowAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ enum WindowAction: Int, Codable {
leftTodo = 68,
rightTodo = 69,
cascadeActiveApp = 70,
centerProminently = 71
centerProminently = 71,
centerTwoScreens = 72

// Order matters here - it's used in the menu
static let active = [leftHalf, rightHalf, centerHalf, topHalf, bottomHalf,
Expand All @@ -104,7 +105,8 @@ enum WindowAction: Int, Codable {
bottomLeftEighth, bottomCenterLeftEighth, bottomCenterRightEighth, bottomRightEighth,
tileAll, cascadeAll,
leftTodo, rightTodo,
cascadeActiveApp
cascadeActiveApp,
centerTwoScreens
]

func post() {
Expand Down Expand Up @@ -207,6 +209,7 @@ enum WindowAction: Int, Codable {
case .rightTodo: return "rightTodo"
case .cascadeActiveApp: return "cascadeActiveApp"
case .centerProminently: return "centerProminently"
case .centerTwoScreens: return "centerTwoScreens"
}
}

Expand Down Expand Up @@ -332,6 +335,9 @@ enum WindowAction: Int, Codable {
case .bottomRightSixth:
key = "m2F-eA-g7w.title"
value = "Bottom Right Sixth"
case .centerTwoScreens:
key = "rcY-NS-HlJ.title"
value = "Center Two Screens"
case .topLeftNinth, .topCenterNinth, .topRightNinth, .middleLeftNinth, .middleCenterNinth, .middleRightNinth, .bottomLeftNinth, .bottomCenterNinth, .bottomRightNinth:
return nil
case .topLeftThird, .topRightThird, .bottomLeftThird, .bottomRightThird:
Expand Down Expand Up @@ -500,6 +506,7 @@ enum WindowAction: Int, Codable {
case .rightTodo: return NSImage()
case .cascadeActiveApp: return NSImage()
case .centerProminently: return NSImage()
case .centerTwoScreens: return NSImage()
}
}

Expand Down Expand Up @@ -540,7 +547,7 @@ enum WindowAction: Int, Codable {
return Defaults.applyGapsToMaximize.userDisabled ? .none : .both;
case .maximizeHeight:
return Defaults.applyGapsToMaximizeHeight.userDisabled ? .none : .vertical;
case .almostMaximize, .previousDisplay, .nextDisplay, .larger, .smaller, .center, .centerProminently, .restore, .specified, .reverseAll, .tileAll, .cascadeAll, .cascadeActiveApp:
case .almostMaximize, .previousDisplay, .nextDisplay, .larger, .smaller, .center, .centerProminently, .restore, .specified, .reverseAll, .tileAll, .cascadeAll, .cascadeActiveApp, .centerTwoScreens:
return .none
}
}
Expand Down
156 changes: 156 additions & 0 deletions Rectangle/WindowCalculation/CenterTwoScreensCalculation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import Foundation

enum ScreenDirection {
case up
case down
case left
case right
}

/// Centers a window in between two horizontally or vertically stacked displays.
///
/// # Assumptions
/// - The user only has two screens.
/// - The screens may be different sizes.
/// - The screens can be stacked vertically or horizontally, with the line connecting their centers either vertical or horizontal.
///
/// # Algorithm
/// 1. Get the display that the window started on.
/// 2. Get the next display.
/// 3. Figure out whether the next display is to the left, right, top or bottom of the starting display.
/// 4. Figure out which display is smaller.
/// 5. Position the window in the middle of the two screens.
///
/// # Notes
/// This does not work for vertically stacked monitors and I don't know why.
class CenterTwoScreensCalculation: WindowCalculation {

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

// Step 1
let initialScreen = params.usableScreens.currentScreen.frame

// Step 2
let secondScreen = params.usableScreens.adjacentScreens?.next.frame

guard let secondScreen else {
// If there is no second screen then just maximize the window
return MaximizeCalculation().calculate(params)
}

// Step 3
var directionOfSecondScreen = ScreenDirection.right

// Check if initial screen is above second screen
if initialScreen.midY < secondScreen.midY {
directionOfSecondScreen = ScreenDirection.up
}
// Check if initial screen is below second screen
if initialScreen.midY > secondScreen.midY {
directionOfSecondScreen = ScreenDirection.down
}
// Check if initial screen is left of second screen
if initialScreen.midX < secondScreen.midX {
directionOfSecondScreen = ScreenDirection.right
}
// Check if initial screen is right of second screen
if initialScreen.midX > secondScreen.midX {
directionOfSecondScreen = ScreenDirection.left
}

// Step 4
var smallerScreen = initialScreen

// If the windows are horizontally stacked, we define the smaller screen as the one that has the smaller width
if directionOfSecondScreen == ScreenDirection.left
|| directionOfSecondScreen == ScreenDirection.right
{
if initialScreen.width < secondScreen.width {
smallerScreen = initialScreen
} else {
smallerScreen = secondScreen
}
}
// If the windows are verticalled stacked, we define the smaller screen as the one that has the smaller height
if directionOfSecondScreen == ScreenDirection.up
|| directionOfSecondScreen == ScreenDirection.down
{
if initialScreen.height < secondScreen.height {
smallerScreen = initialScreen
} else {
smallerScreen = secondScreen
}
}

// Step 5
// We need to know if the inital screen is the main screen or not because the coordinate system originates on the main screen
let initialScreenIsMain = initialScreen.origin.equalTo(CGPoint(x: 0, y: 0))

let rectResult = calculateRect(
params.asRectParams(visibleFrame: smallerScreen),
directionOfSecondScreen: directionOfSecondScreen, initialScreenIsMain: initialScreenIsMain)

let screenFrame = initialScreen.union(secondScreen)

return WindowCalculationResult(
rect: rectResult.rect,
screen: params.usableScreens.currentScreen,
resultingAction: params.action,
resultingScreenFrame: screenFrame)
}

func calculateRect(
_ params: RectCalculationParameters, directionOfSecondScreen: ScreenDirection,
initialScreenIsMain: Bool
) -> RectResult {
var calculatedWindowRect = params.window.rect

// Make the window as tall as the screen
calculatedWindowRect.size.height = params.visibleFrameOfScreen.height

// Make the window as wide as the screen
calculatedWindowRect.size.width = params.visibleFrameOfScreen.width

let halfScreenHeight = params.visibleFrameOfScreen.height / 2
let halfScreenWidth = params.visibleFrameOfScreen.width / 2

switch directionOfSecondScreen {
case ScreenDirection.down:
// Position the bottom of the window at the midpoint of the bottom screen
calculatedWindowRect.origin.y = initialScreenIsMain ? -1 * halfScreenHeight : halfScreenHeight

// Position the left side of the window at the left of the screen
calculatedWindowRect.origin.x = 0

break
case ScreenDirection.up:
// Position the bottom of the window at the midpoint of the bottom screen
calculatedWindowRect.origin.y = initialScreenIsMain ? halfScreenHeight : -1 * halfScreenHeight

// Position the left side of the window at the left of the screen
calculatedWindowRect.origin.x = 0

break
case ScreenDirection.left:
// Position the bottom of the window at the bottom of the screen
calculatedWindowRect.origin.y = params.visibleFrameOfScreen.origin.y

// Position the left side of the window at the midpoint of the left screen
let midpointOfLeftScreen = initialScreenIsMain ? -1 * halfScreenWidth : halfScreenWidth
calculatedWindowRect.origin.x = midpointOfLeftScreen

break
case ScreenDirection.right:
// Position the bottom of the window at the bottom of the screen
calculatedWindowRect.origin.y = params.visibleFrameOfScreen.origin.y

// Position the left side of the window at the midpoint of the left screen
let midpointOfLeftScreen = initialScreenIsMain ? halfScreenWidth : -1 * halfScreenWidth
calculatedWindowRect.origin.x = midpointOfLeftScreen

break
}

return RectResult(calculatedWindowRect)
}
}
4 changes: 3 additions & 1 deletion Rectangle/WindowCalculation/WindowCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class WindowCalculationFactory {
static let specifiedCalculation = SpecifiedCalculation()
static let leftTodoCalculation = LeftTodoCalculation()
static let rightTodoCalculation = RightTodoCalculation()
static let centerTwoScreensCalculation = CenterTwoScreensCalculation()

static let calculationsByAction: [WindowAction: WindowCalculation] = [
.leftHalf: leftHalfCalculation,
Expand Down Expand Up @@ -246,7 +247,8 @@ class WindowCalculationFactory {
.bottomRightEighth: bottomRightEighthCalculation,
.specified: specifiedCalculation,
.leftTodo: leftTodoCalculation,
.rightTodo: rightTodoCalculation
.rightTodo: rightTodoCalculation,
.centerTwoScreens: centerTwoScreensCalculation
// .restore: nil
]
}
17 changes: 17 additions & 0 deletions TerminalCommands.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The preferences window is purposefully slim, but there's a lot that can be modif
- [Only allow drag-to-snap when modifier keys are pressed](#only-allow-drag-to-snap-when-modifier-keys-are-pressed)
- [Almost Maximize](#almost-maximize)
- [Add an extra centering command with custom size](#add-an-extra-centering-command-with-custom-size)
- [Add an extra center between two screens command](#add-an-extra-center-between-two-screens-command)
- [Add extra "ninths" sizing commands](#add-extra-ninths-sizing-commands)
- [Add extra "eighths" sizing commands](#add-extra-eighths-sizing-commands)
- [Add additional "thirds" sizing commands](#add-additional-thirds-sizing-commands)
Expand Down Expand Up @@ -159,6 +160,22 @@ defaults write com.knollsoft.Rectangle centerProminently -dict-add keyCode -floa
```


## Add an extra center between two screens command

There is an extra command that horizontally centers the window between two horizontally adjacent screens.
This is especially useful for users who are running ultrawide monitors in picture by picture mode, or for users with two adjacent monitors with no bezels.

The key code is:

- centerTwoScreens

For example, the command for setting the shortcut to `ctrl option command C` would be:

```bash
defaults write com.knollsoft.Rectangle centerProminently -dict-add keyCode -float 8 modifierFlags -float 1835305
```


## Add extra "ninths" sizing commands

Commands for resizing to screen ninths are not available in the UI.
Expand Down
Loading