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
2 changes: 1 addition & 1 deletion Configs/AppVersion.xcconfig
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
MARKETING_VERSION = 1.2.0
MARKETING_VERSION = 1.2.1
CURRENT_PROJECT_VERSION = 69
100 changes: 51 additions & 49 deletions Plugins/WindowLayouts/Sources/WindowCustomCommandSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,33 @@ import MacToolsPluginKit

struct WindowCustomCommandPreviewLayout {
private static let referenceScreenSize = CGSize(width: 1_440, height: 900)
private static let referenceWindowFrame = CGRect(
x: 288,
y: 180,
width: 864,
height: 540
)

let command: WindowCustomCommand
let gap: CGFloat

func windowFrame(in screenSize: CGSize) -> CGRect {
guard screenSize.width > 0, screenSize.height > 0 else { return .zero }

let windowSize = CGSize(
width: screenSize.width * fraction(
for: command.width,
referenceLength: Self.referenceScreenSize.width
),
height: screenSize.height * fraction(
for: command.height,
referenceLength: Self.referenceScreenSize.height
)
let referenceFrame = WindowLayoutCalculator().customFrame(
for: command,
windowFrame: Self.referenceWindowFrame,
visibleFrame: CGRect(origin: .zero, size: Self.referenceScreenSize),
gap: gap
)
let factors = anchorFactors(command.anchor)
let origin = CGPoint(
x: (screenSize.width - windowSize.width) * factors.x
+ screenSize.width * command.offsetX / Self.referenceScreenSize.width,
y: (screenSize.height - windowSize.height) * factors.y
+ screenSize.height * command.offsetY / Self.referenceScreenSize.height
let scaleX = screenSize.width / Self.referenceScreenSize.width
let scaleY = screenSize.height / Self.referenceScreenSize.height
return CGRect(
x: referenceFrame.minX * scaleX,
y: referenceFrame.minY * scaleY,
width: referenceFrame.width * scaleX,
height: referenceFrame.height * scaleY
)
return CGRect(origin: origin, size: windowSize)
}

private func fraction(
for dimension: WindowLayoutDimension,
referenceLength: CGFloat
) -> CGFloat {
let value: CGFloat = switch dimension {
case .current: 0.6
case let .points(points): points / referenceLength
case let .fraction(fraction): fraction
}
return min(max(value, 0.05), 1)
}

private func anchorFactors(_ anchor: WindowLayoutAnchor) -> CGPoint {
switch anchor {
case .topLeft: CGPoint(x: 0, y: 0)
case .top: CGPoint(x: 0.5, y: 0)
case .topRight: CGPoint(x: 1, y: 0)
case .left: CGPoint(x: 0, y: 0.5)
case .center: CGPoint(x: 0.5, y: 0.5)
case .right: CGPoint(x: 1, y: 0.5)
case .bottomLeft: CGPoint(x: 0, y: 1)
case .bottom: CGPoint(x: 0.5, y: 1)
case .bottomRight: CGPoint(x: 1, y: 1)
}
}
}

Expand Down Expand Up @@ -111,6 +88,7 @@ struct WindowCustomCommandSettingsView: View {
let commandID: UUID

@State private var draft: WindowCustomCommand
@State private var shortcutErrorMessage: String?
@FocusState private var isNameFocused: Bool

init(plugin: WindowLayoutsPlugin, command: WindowCustomCommand) {
Expand Down Expand Up @@ -161,7 +139,10 @@ struct WindowCustomCommandSettingsView: View {
)
.font(PluginSettingsTheme.Typography.emphasizedRowTitle)

WindowCustomLayoutPreview(command: draft)
WindowCustomLayoutPreview(
command: draft,
gap: plugin.customCommandPreviewGap
)
.frame(width: 184, height: 112)
.accessibilityLabel(previewSummary)
}
Expand Down Expand Up @@ -189,7 +170,9 @@ struct WindowCustomCommandSettingsView: View {
displayText: shortcutDisplayText,
minWidth: PluginSettingsTheme.Size.shortcutRecorderWidth,
onRecord: { binding in
plugin.recordCustomCommandShortcut(binding, for: commandID)
let result = plugin.recordCustomCommandShortcut(binding, for: commandID)
updateShortcutError(from: result)
return result
}
)
.frame(width: PluginSettingsTheme.Size.shortcutRecorderWidth)
Expand All @@ -199,7 +182,9 @@ struct WindowCustomCommandSettingsView: View {

if shortcutBinding != nil {
Button {
plugin.clearCustomCommandShortcut(for: commandID)
updateShortcutError(
from: plugin.clearCustomCommandShortcut(for: commandID)
)
} label: {
Image(systemName: "xmark.circle.fill")
.pluginSettingsRowIconStyle(.secondary)
Expand All @@ -215,12 +200,31 @@ struct WindowCustomCommandSettingsView: View {
))
}
}

if let shortcutErrorMessage {
Label(
shortcutErrorMessage,
systemImage: "exclamationmark.triangle.fill"
)
.font(PluginSettingsTheme.Typography.rowDescription)
.foregroundStyle(.red)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.pluginSettingsListRowPadding(interactive: true)
}

private func updateShortcutError(from result: PluginShortcutRecordingResult) {
switch result {
case .accepted:
shortcutErrorMessage = nil
case let .rejected(message):
shortcutErrorMessage = message
}
}

private var nameRow: some View {
settingsRow(
title: plugin.localizedKey("settings.custom.name", "名称")
Expand Down Expand Up @@ -518,9 +522,6 @@ struct WindowCustomCommandSettingsView: View {
}

private func commitDraft() {
guard !draft.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
}
guard plugin.updateCustomCommand(draft),
let stored = plugin.customCommand(id: commandID)
else {
Expand All @@ -543,10 +544,11 @@ struct WindowCustomCommandSettingsView: View {

private struct WindowCustomLayoutPreview: View {
let command: WindowCustomCommand
let gap: CGFloat

var body: some View {
GeometryReader { proxy in
let frame = WindowCustomCommandPreviewLayout(command: command)
let frame = WindowCustomCommandPreviewLayout(command: command, gap: gap)
.windowFrame(in: proxy.size)

ZStack(alignment: .topLeading) {
Expand Down
39 changes: 29 additions & 10 deletions Plugins/WindowLayouts/Sources/WindowLayoutCalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ struct WindowLayoutCalculator {
func movedFrame(
_ windowFrame: CGRect,
from sourceVisibleFrame: CGRect,
to destinationVisibleFrame: CGRect
to destinationVisibleFrame: CGRect,
preservingSize: Bool = false
) -> CGRect {
let source = sourceVisibleFrame.standardized
let destination = destinationVisibleFrame.standardized
Expand All @@ -159,14 +160,16 @@ struct WindowLayoutCalculator {

let widthRatio = max(0, windowFrame.width / source.width)
let heightRatio = max(0, windowFrame.height / source.height)
let destinationSize = CGSize(
width: source.width == destination.width
? min(destination.width, max(0, windowFrame.width))
: min(destination.width, destination.width * widthRatio),
height: source.height == destination.height
? min(destination.height, max(0, windowFrame.height))
: min(destination.height, destination.height * heightRatio)
)
let destinationSize = preservingSize
? windowFrame.size
: CGSize(
width: source.width == destination.width
? min(destination.width, max(0, windowFrame.width))
: min(destination.width, destination.width * widthRatio),
height: source.height == destination.height
? min(destination.height, max(0, windowFrame.height))
: min(destination.height, destination.height * heightRatio)
)
let sourceTravelX = source.width - windowFrame.width
let sourceTravelY = source.height - windowFrame.height
let relativeX = relativePosition(
Expand All @@ -185,7 +188,9 @@ struct WindowLayoutCalculator {
width: destinationSize.width,
height: destinationSize.height
)
return clamp(proposed, inside: destination)
return preservingSize
? restoreReachableFrame(proposed, inside: destination)
: clamp(proposed, inside: destination)
}

func clamp(_ frame: CGRect, inside bounds: CGRect) -> CGRect {
Expand All @@ -202,6 +207,20 @@ struct WindowLayoutCalculator {
)
}

/// Keeps the original size while ensuring the window's top edge remains reachable.
func restoreReachableFrame(_ frame: CGRect, inside bounds: CGRect) -> CGRect {
let bounds = bounds.standardized
let minimumX = min(bounds.minX, bounds.maxX - frame.width)
let maximumX = max(bounds.minX, bounds.maxX - frame.width)
let maximumY = max(bounds.minY, bounds.maxY - frame.height)
return CGRect(
x: min(max(frame.minX, minimumX), maximumX),
y: min(max(frame.minY, bounds.minY), maximumY),
width: frame.width,
height: frame.height
)
}

private func insetFrame(_ frame: CGRect, by requestedGap: CGFloat) -> CGRect {
let maximumGap = max(0, min(frame.width, frame.height) / 2 - 0.5)
let gap = min(max(0, requestedGap), maximumGap)
Expand Down
25 changes: 12 additions & 13 deletions Plugins/WindowLayouts/Sources/WindowLayoutService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ final class WindowLayoutService: WindowLayoutExecuting {
targetFrame = calculator.movedFrame(
currentFrame,
from: effectiveCurrentScreen.visibleFrame,
to: screen(destination, respectingStageManager: options.respectsStageManager).visibleFrame
to: screen(destination, respectingStageManager: options.respectsStageManager).visibleFrame,
preservingSize: !window.canResize
)
case .restorePreviousFrame:
guard await frameReader.isValid(window) else {
Expand All @@ -304,22 +305,20 @@ final class WindowLayoutService: WindowLayoutExecuting {
guard let previousFrame = history.previousFrame(for: window) else {
throw WindowLayoutError.noPreviousFrame
}
let safePreviousFrame: CGRect
if screens.contains(where: {
$0.visibleFrame.intersection(previousFrame).area > 0
}) {
safePreviousFrame = previousFrame
} else if let nearestScreen = screenResolver.screen(
guard let nearestScreen = screenResolver.screen(
for: previousFrame,
among: screens
) {
safePreviousFrame = calculator.clamp(
previousFrame,
inside: nearestScreen.visibleFrame
)
} else {
) else {
throw WindowLayoutError.noDisplay
}
let safeVisibleFrame = screen(
nearestScreen,
respectingStageManager: options.respectsStageManager
).visibleFrame
let safePreviousFrame = calculator.restoreReachableFrame(
previousFrame,
inside: safeVisibleFrame
)
if safePreviousFrame.size != currentFrame.size, !window.canResize {
throw WindowLayoutError.windowCannotResize
}
Expand Down
Loading
Loading