Skip to content

Commit a6288f9

Browse files
authored
Make rulers flippable (#199)
* stubbed out Flip menu * Address flip ruler PR comments * Localize flip submenu titles * Restore zero corner test defaults domain * Add defaultZeroCorner and use in prefs Introduce Prefs.defaultZeroCorner and use it as the canonical default for zeroCorner. Update the prefs defaults dictionary and the zeroCorner(fromRawValue:) fallback to reference defaultZeroCorner. Ensure resetRulerPositions restores zeroCorner to the canonical default. Add a unit test to assert the default is .topLeft. * Address PR feedback on flip shortcuts and defaults * Use default zero corner in UI test reset * Introduce zero corner geometry helpers * Align left resize handle inset * Preserve screen origin for default rulers * Update ruler drawing for zero corners * Mirror unit label mouse tick gating * Revert "Mirror unit label mouse tick gating" This reverts commit 728a4b0. * Cover direction-aware resizing * Handle grouped zero-corner flips * Wire and guard grouped ruler flips * Add flip ruler UI * Verify flippable ruler integration * Derive flipped label test scale from screen * Ignore Caps Lock for ruler hotkeys * Address flip ruler review comments * Anchor labels/handles to opposite sides and update tests Add RulerSide.opposite and update label/handle geometry to anchor unit labels and resize handles relative to the far/opposite corner derived from ZeroCornerGeometry. HorizontalRule and VerticalRule now compute label X/Y anchors using geometry.opposite sides (and use a fixed textHeight for top labels). ResizeHandleView uses tickSide.opposite to place vertical/horizontal handles correctly. Update RulerCoreTests: adjust expected rects, add a unit-label positioning test, rename/expand resize-handle assertions to check X and Y anchoring, and wrap tests to set/restore zeroCorner preference where needed. * Encapsulate resize-handle placement logic Add RulerCornerPlacement and a new ZeroCornerGeometry.resizeHandlePlacement(for:) helper to compute the x/y sides for resize handles based on orientation and zero corner. Refactor ResizeHandleView to use placement.xSide/placement.ySide instead of separately computing tickSide, resizeSide and their opposites, simplifying control flow and removing duplicated logic. Changes in Ruler.swift and ResizeHandleView.swift; no intended behavioral change. * Add UnitLabelView and integrate unit labels Introduce UnitLabelView to centralize unit label sizing, framing and drawing. Move unit label placement logic into UnitLabelView.labelFrame and ZeroCornerGeometry.unitLabelPlacement, and wire a unitLabelView subview into RuleView with install/update methods. HorizontalRule and VerticalRule now install the subview and override visibility logic to hide the label when the mouse tick overlaps it. Update project file to include the new source and adjust/extend unit tests to match the new layout behavior and verify subview positioning and baseline consistency. * Simplify unit label positioning and add insets Replace the orientation-heavy coordinate logic with a simpler corner-based switch and explicit insets. Introduces top/bottom/left/right inset constants and computes x/y from placement.xSide and placement.ySide (four corner cases), removes redundant orientation branches, and consolidates the assertion/fallback behavior. * Adjust unit label insets and update tests Tweak UnitLabelView insets (bottom: 10→9, left/right: 7→8) to correct unit label positioning. Update RulerCoreTests expected CGRects to match the adjusted layout. Files changed: Free Ruler/UnitLabelView.swift and FreeRulerTests/RulerCoreTests.swift. * Refactor mouse tick layout & handle visibility Introduce MouseTickLabelLayout to centralize mouse-tick label placement logic for both horizontal and vertical rulers, replacing per-rule mouseNumberLabelRect implementations. Add resize-handle obscuring support via setResizeHandleObscured and per-rule updateResizeHandleVisibility overrides so the resize handle fades when a mouse tick overlaps it; wire visibility updates into layout/preference changes and mouse-tick/showMouseTick setters. Update tests to reflect the new layout API and visual-obscuring behavior and remove the older mouseTickLabelResizeHandleSpacing constant. * Add mouse tick label preview views Introduce debug-only preview tooling to visualize mouse tick label layout and offsets. Adds MouseTickLabelLayoutPreview (NSViewRepresentable) and MouseTickLabelLayoutPreviewView to draw ruler ticks, mouse tick line/label, unit label, and resize handle; plus SwiftUI helpers MouseTickLabelOffsetPreview and MouseTickLabelCornerPreview to show combinations of zero corners and orientations. Update RuleView_Previews to group horizontal/vertical ruler previews and include the new Mouse Tick Labels preview. * Propagate zeroCorner and refactor previews Replace direct prefs.zeroCorner usages with a RuleView.zeroCorner property and propagate that value to child views (UnitLabelView, ResizeHandleView, HorizontalRule, VerticalRule). Add frame(in:bounds, zeroCorner:) overloads and update layout code to use the passed zeroCorner. Introduce RuleView.installWindowBorder() and improve window size fallbacks. Add nil-checks for NSGraphicsContext before drawing label rectangles. Refactor DEBUG preview code: replace the old MouseTickLabelLayoutPreview with a compact MouseTickRulePreview that hosts new MouseTickPreviewHorizontalRule/VerticalRule subclasses, and wire preview configuration (previewZeroCorner, mouse positions, colors). Also add a new localized string key to Localizable.xcstrings. * Remove RuleViewPreview and simplify previews Delete the NSViewRepresentable RuleViewPreview and the HStack of horizontal/vertical rule previews. Replace the previous preview group with a single MouseTickLabelOffsetPreview and adjust the preview layout/name. This simplifies SwiftUI previews by removing the AppKit bridging preview helper and duplicate preview entries. * Update Free Ruler help screen * Use current app icon in help book * Update help icon when generating app icons * Stabilize Free Ruler help book caching * Use timestamped help icon filenames Switch help icon naming from a build-number-based token to a UTC timestamp+hash token. Update FreeRuler HTML and CSS to reference the new freeruler-help-icon-<timestamp>-<hash>.png name, rename the icon file accordingly, and remove the old freeruler.png legacy copy. Update scripts/generate-app-icon.sh to generate a timestamped help_cache_token, drop the Info.plist build_number dependency, remove legacy copy behavior, and propagate the new token into styles.css and FreeRuler.html while cleaning up stale icons. * Use CSS var for help icon and refresh assets Introduce a CSS variable for the help icon and refresh related assets. - FreeRuler.html: add :root style setting --free-ruler-help-icon, update styles.css query token, tidy metadata and reformat the keyboard shortcuts/feature sections. - styles.css: switch .title-page-header background-image to use var(--free-ruler-help-icon). - Replace old help icon with a new freeruler-help-icon PNG and remove the old file. - Update multiple AppIcon image assets (various sizes) — regenerated icons. - scripts/generate-app-icon.sh: combine perl substitutions to inject the cache token and set the --free-ruler-help-icon URL in the HTML, and remove an unused variable. These changes allow the help page to reference the generated icon dynamically and support cache-busting when regenerating icons. * Update project.pbxproj * Update project.pbxproj * Add help-index generator and npm script Add scripts/generate-help-index.sh which runs hiutil to build English.lproj.helpindex from Free Ruler/FreeRuler.help/Contents/Resources/English.lproj (script uses strict bash flags and executes from repo root). Wire the script into package.json as the "generate:help" npm script and update the packaged help index file accordingly so the help index can be regenerated as part of build tasks. * Fix help bundle identifier & version handling Use a fixed help bundle identifier and stop appending the build number. Updated FreeRuler.help/Contents/Info.plist to set CFBundleIdentifier to com.pascal.freeruler.help and adjusted CFBundleVersion to 491. Replaced the templated CFBundleHelpBookName in Free Ruler/Info.github.plist and Free Ruler/Info.plist with the fixed help ID. Removed the code in scripts/release/set-version.js that modified the help CFBundleIdentifier per-build so the help bundle ID remains stable across builds. * Update set-version.js * revert help menu handler * Update bump-version.js * Update generate-app-icon.sh * Add interactive mouse tick label preview UI Introduce an adjustable preview for mouse tick label offsets used in DEBUG builds. Adds a mouseTickLabelPreviewRulerLength constant and refactors MouseTickLabelOffsetPreview into smaller views: heading, grid, controls, slider panel, and individual sliders with a numeric TextField (clamped and rounded). Replaces hardcoded 260 ruler length usage, extracts a cornerPreview helper, and updates the PreviewProvider to use the interactive controls for easier testing of label offsets. * Centralize resize handle end-region check Introduce RuleView.resizeHandleEndRegionContains(...) to encapsulate logic that determines whether the mouse tick overlaps the resize-handle end region based on ruler orientation and the current zeroCorner/resize side. Replace the inline min/max checks in HorizontalRule and VerticalRule with calls to this helper. Expand and rename unit tests to cover obscuring behavior from the leading edge to the ruler end across different zeroCorner configurations. Includes an assertionFailure fallback for unexpected cases. * Avoid mouse tick label overlapping resize handle Prevent mouse-tick/unit labels from overlapping the ruler resize handle and from incorrectly hiding near the zero edge. MouseTickLabelLayout now accepts an optional resizeHandleFrame and tickGap was renamed to tickLabelSpacing; layout logic was extended with horizontal/vertical helper checks to prefer the side that doesn't collide with the resize handle. RuleView gained unitLabelZeroRegionContains and HorizontalRule/VerticalRule use it to compute unit-label visibility based on growth direction and bounds. Debug previews and sliders were adjusted to exercise the new behavior, and unit tests were added to cover zero-edge hiding and label flipping around the resize handle. * Propagate zeroCorner and refactor geometry Pass zeroCorner into child views and clean up geometry/layout logic. ResizeHandleView now computes grip and slot frames (gripRect/gripSize/slotFrame) and draws grip/bg relative to the grip; it accepts a zeroCorner override. UnitLabelView gets explicit padding, safer draw rect to leave room for descenders, and accepts zeroCorner for placement. MouseTickLabelLayout was extended to handle unitLabelFrame, vertical lanes, and revised offsets; added labelBackgroundFrame helper. HorizontalRule/VerticalRule use growthDirection-aware mouse tick line helpers and simplified unit label / resize-handle visibility checks. Tests updated and new unit tests added to cover the new layout behavior and zeroCorner override. * tweak mouse tick layout and update tests * Address PR feedback on ruler labels and help assets * Continue resize handle placement assertions * Update English.lproj.helpindex * Reset group setting to default in resetRulerPositions * Reset Ruler Positions → Reset Rulers * Add flip rulers screenshot layout Adds the flip rulers App Store screenshot in output position 3, removes Screen prefixes from preview labels, splits screenshot constants by layout, and models the flip rulers with corner-relative inset positioning, grouped copy placement, and distinct non-yellow colors. * Update AppStoreScreenshotPreview.swift * Update AppStoreScreenshotPreview.swift * update screenshots
1 parent 13c5c53 commit a6288f9

52 files changed

Lines changed: 4488 additions & 762 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Free Ruler.xcodeproj/project.pbxproj

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
50B1D3532D05D00100B1D135 /* RulerCursorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3522D05D00100B1D135 /* RulerCursorController.swift */; };
3333
50B1D3552D05E00000B1D138 /* RulerTickLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */; };
3434
50B1D3592D06000100B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; };
35-
50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
3635
50B1D35D2D06000400B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; };
36+
50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
3737
50B1D3612D06000700B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
3838
50B1D3652D06010100B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; };
3939
50B1D3662D06010200B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; };
4040
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
4141
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
42+
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
43+
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
4244
50C6D891228BDBAD0091F19E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50C6D890228BDBAD0091F19E /* Images.xcassets */; };
4345
50D7BEE7227D42FD0008B95E /* RulerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE6227D42FD0008B95E /* RulerController.swift */; };
4446
50D7BEE9227D43270008B95E /* Ruler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE8227D43270008B95E /* Ruler.swift */; };
@@ -118,6 +120,7 @@
118120
50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGenerator.swift; sourceTree = "<group>"; };
119121
50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshotPreview.swift; sourceTree = "<group>"; };
120122
50B1D3682D06100000B1D13A /* ResizeHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeHandleView.swift; sourceTree = "<group>"; };
123+
50B1D36C2D06110000B1D13B /* UnitLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitLabelView.swift; sourceTree = "<group>"; };
121124
50C6D890228BDBAD0091F19E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
122125
50D7BEE6227D42FD0008B95E /* RulerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RulerController.swift; sourceTree = "<group>"; };
123126
50D7BEE8227D43270008B95E /* Ruler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ruler.swift; sourceTree = "<group>"; };
@@ -139,7 +142,7 @@
139142
8F629823243003EA004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = "<group>"; };
140143
8F629825243003F6004F9099 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreferencesController.xib; sourceTree = "<group>"; };
141144
8F629828243003FF004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/PreferencesController.strings; sourceTree = "<group>"; };
142-
AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = "Free Ruler.app"; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; };
145+
AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; };
143146
B894A5002BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = "<group>"; };
144147
B894A5012BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/PreferencesController.strings; sourceTree = "<group>"; };
145148
D9DBE8A12C791B1600A42589 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = "<group>"; };
@@ -252,6 +255,7 @@
252255
50D7BEEA227D432E0008B95E /* RulerWindow.swift */,
253256
50D7BEEC227D5C810008B95E /* RuleView.swift */,
254257
50B1D3682D06100000B1D13A /* ResizeHandleView.swift */,
258+
50B1D36C2D06110000B1D13B /* UnitLabelView.swift */,
255259
50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */,
256260
6F41029E22607DC900F06A10 /* HorizontalRule.swift */,
257261
5012CAAC226AB09000BD9565 /* VerticalRule.swift */,
@@ -479,6 +483,7 @@
479483
6F4102892260712F00F06A10 /* AppDelegate.swift in Sources */,
480484
50D7BEED227D5C810008B95E /* RuleView.swift in Sources */,
481485
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */,
486+
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */,
482487
50D7BEE9227D43270008B95E /* Ruler.swift in Sources */,
483488
);
484489
runOnlyForDeploymentPostprocessing = 0;
@@ -504,6 +509,7 @@
504509
23E2B80975F07733EB6CF5DB /* AppDelegate.swift in Sources */,
505510
9A082BBC4A583513B0858C41 /* RuleView.swift in Sources */,
506511
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */,
512+
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */,
507513
E6BC6663BAF0D11D7A9D2195 /* Ruler.swift in Sources */,
508514
);
509515
runOnlyForDeploymentPostprocessing = 0;

Free Ruler/AppDelegate.swift

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ private enum HotkeyBezelLocalizationKey: String {
3434
case rulersUngrouped = "HotkeyBezel.RulersUngrouped"
3535
case shadowEnabled = "HotkeyBezel.ShadowEnabled"
3636
case shadowDisabled = "HotkeyBezel.ShadowDisabled"
37+
case horizontalOriginFormat = "HotkeyBezel.HorizontalOriginFormat"
38+
case verticalOriginFormat = "HotkeyBezel.VerticalOriginFormat"
39+
case originLeft = "HotkeyBezel.OriginLeft"
40+
case originRight = "HotkeyBezel.OriginRight"
41+
case originTop = "HotkeyBezel.OriginTop"
42+
case originBottom = "HotkeyBezel.OriginBottom"
3743
case unitsFormat = "HotkeyBezel.UnitsFormat"
3844
case pixelsUnit = "Unit.Pixels.Abbreviation"
3945
case millimetersUnit = "Unit.Millimeters.Abbreviation"
@@ -57,6 +63,18 @@ private enum HotkeyBezelLocalizationKey: String {
5763
return "Hotkey status bezel text indicating ruler shadow is enabled"
5864
case .shadowDisabled:
5965
return "Hotkey status bezel text indicating ruler shadow is disabled"
66+
case .horizontalOriginFormat:
67+
return "Hotkey status bezel format for the horizontal ruler origin side"
68+
case .verticalOriginFormat:
69+
return "Hotkey status bezel format for the vertical ruler origin side"
70+
case .originLeft:
71+
return "Hotkey status bezel value for a ruler origin on the left"
72+
case .originRight:
73+
return "Hotkey status bezel value for a ruler origin on the right"
74+
case .originTop:
75+
return "Hotkey status bezel value for a ruler origin at the top"
76+
case .originBottom:
77+
return "Hotkey status bezel value for a ruler origin at the bottom"
6078
case .unitsFormat:
6179
return "Hotkey status bezel format for the selected measurement unit"
6280
case .pixelsUnit:
@@ -127,7 +145,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
127145
#endif
128146

129147
showRulers()
130-
131148
}
132149

133150
#if DEBUG
@@ -218,6 +235,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
218235
prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in
219236
self.redrawRulers()
220237
},
238+
prefs.observe(\Prefs.zeroCorner, options: .new) { prefs, changed in
239+
self.redrawRulers()
240+
},
221241
]
222242
}
223243

@@ -262,6 +282,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
262282
"backgroundOpacity",
263283
"rulerColor",
264284
"unit",
285+
"zeroCorner",
265286
"NSWindow Frame horizontal-ruler",
266287
"NSWindow Frame vertical-ruler",
267288
"NSWindow Frame preferencesWindow",
@@ -274,6 +295,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
274295
prefs.backgroundOpacity = 50
275296
prefs.rulerColor = Prefs.defaultRulerFillColor
276297
prefs.unit = .pixels
298+
prefs.zeroCorner = Prefs.defaultZeroCorner
277299
}
278300

279301
func createRulersIfNeeded() {
@@ -479,15 +501,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
479501
@IBAction func resetRulerPositions(_ sender: Any) {
480502
createRulersIfNeeded()
481503

504+
prefs.zeroCorner = Prefs.defaultZeroCorner
505+
482506
// ungroup rulers during reset operation
483-
let groupRulers = prefs.groupRulers
484507
prefs.groupRulers = false
485508
for ruler in rulers {
486509
ruler.resetPosition()
487510
showRuler(ruler)
488511
}
489-
// reset groupRulers to previous value
490-
prefs.groupRulers = groupRulers
512+
513+
prefs.groupRulers = Prefs.defaultGroupRulers
491514
updateRulerGrouping()
492515
}
493516

@@ -503,7 +526,108 @@ class AppDelegate: NSObject, NSApplicationDelegate {
503526
toggleRuler(orientation: .vertical)
504527
}
505528

506-
func performRulerHotkey(keyCode: Int, sender: Any) -> Bool {
529+
@IBAction func flipHorizontalRuler(_ sender: Any) {
530+
flipRulers(along: .horizontal)
531+
showHorizontalOriginHotkeyBezel(on: bezelScreen(for: sender))
532+
}
533+
534+
@IBAction func flipVerticalRuler(_ sender: Any) {
535+
flipRulers(along: .vertical)
536+
showVerticalOriginHotkeyBezel(on: bezelScreen(for: sender))
537+
}
538+
539+
func flipRulers(along orientation: Orientation) {
540+
createRulersIfNeeded()
541+
542+
let oldGeometry = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner)
543+
let flippedCorner = prefs.zeroCorner.flipped(along: orientation)
544+
let flippedRuler = existingRulerController(orientation: orientation)
545+
let otherOrientation: Orientation = orientation == .horizontal ? .vertical : .horizontal
546+
let otherRuler = existingRulerController(orientation: otherOrientation)
547+
let zeroPointOffset = zeroPointOffset(
548+
from: flippedRuler?.rulerWindow,
549+
to: otherRuler?.rulerWindow,
550+
geometry: oldGeometry
551+
)
552+
553+
prefs.zeroCorner = flippedCorner
554+
555+
guard prefs.groupRulers,
556+
let flippedWindow = flippedRuler?.rulerWindow,
557+
let otherWindow = otherRuler?.rulerWindow,
558+
isRulerWindowShown(otherWindow),
559+
let zeroPointOffset = zeroPointOffset else { return }
560+
561+
let newGeometry = ZeroCornerGeometry(zeroCorner: flippedCorner)
562+
let flippedZeroPoint = newGeometry.zeroPoint(in: flippedWindow.frame, for: orientation)
563+
let targetOtherZeroPoint = NSPoint(
564+
x: flippedZeroPoint.x + zeroPointOffset.width,
565+
y: flippedZeroPoint.y + zeroPointOffset.height
566+
)
567+
let otherFrame = newGeometry.frame(
568+
for: otherOrientation,
569+
zeroPoint: targetOtherZeroPoint,
570+
size: otherWindow.frame.size
571+
)
572+
573+
detachRulerWindows()
574+
otherWindow.setFrame(otherFrame, display: true)
575+
updateRulerGrouping()
576+
}
577+
578+
func isRulerWindowShown(_ window: RulerWindow) -> Bool {
579+
return window.isVisible || window.parent != nil || rulers.contains {
580+
$0.rulerWindow.childWindows?.contains(window) == true
581+
}
582+
}
583+
584+
private func zeroPointOffset(
585+
from sourceWindow: RulerWindow?,
586+
to targetWindow: RulerWindow?,
587+
geometry: ZeroCornerGeometry
588+
) -> NSSize? {
589+
guard let sourceWindow = sourceWindow,
590+
let targetWindow = targetWindow else { return nil }
591+
592+
let sourceZeroPoint = geometry.zeroPoint(
593+
in: sourceWindow.frame,
594+
for: sourceWindow.ruler.orientation
595+
)
596+
let targetZeroPoint = geometry.zeroPoint(
597+
in: targetWindow.frame,
598+
for: targetWindow.ruler.orientation
599+
)
600+
601+
return NSSize(
602+
width: targetZeroPoint.x - sourceZeroPoint.x,
603+
height: targetZeroPoint.y - sourceZeroPoint.y
604+
)
605+
}
606+
607+
func performRulerHotkey(
608+
keyCode: Int,
609+
modifierFlags: NSEvent.ModifierFlags,
610+
sender: Any
611+
) -> Bool {
612+
let keyboardModifiers = modifierFlags
613+
.intersection(.deviceIndependentFlagsMask)
614+
.subtracting(.capsLock)
615+
616+
if keyboardModifiers == .shift {
617+
switch keyCode {
618+
case kVK_ANSI_H:
619+
flipHorizontalRuler(sender)
620+
case kVK_ANSI_V:
621+
flipVerticalRuler(sender)
622+
default:
623+
return false
624+
}
625+
626+
return true
627+
}
628+
629+
guard keyboardModifiers.isEmpty else { return false }
630+
507631
switch keyCode {
508632
case kVK_ANSI_H:
509633
toggleHorizontalRuler(sender)
@@ -538,6 +662,40 @@ class AppDelegate: NSObject, NSApplicationDelegate {
538662
showHotkeyBezel(prefs.groupRulers ? .rulersGrouped : .rulersUngrouped, on: screen)
539663
}
540664

665+
private func showHorizontalOriginHotkeyBezel(on screen: NSScreen?) {
666+
switch prefs.zeroCorner {
667+
case .topLeft, .bottomLeft:
668+
showHotkeyBezel(
669+
format: .horizontalOriginFormat,
670+
HotkeyBezelLocalizationKey.originLeft.localizedString,
671+
on: screen
672+
)
673+
case .topRight, .bottomRight:
674+
showHotkeyBezel(
675+
format: .horizontalOriginFormat,
676+
HotkeyBezelLocalizationKey.originRight.localizedString,
677+
on: screen
678+
)
679+
}
680+
}
681+
682+
private func showVerticalOriginHotkeyBezel(on screen: NSScreen?) {
683+
switch prefs.zeroCorner {
684+
case .topLeft, .topRight:
685+
showHotkeyBezel(
686+
format: .verticalOriginFormat,
687+
HotkeyBezelLocalizationKey.originTop.localizedString,
688+
on: screen
689+
)
690+
case .bottomLeft, .bottomRight:
691+
showHotkeyBezel(
692+
format: .verticalOriginFormat,
693+
HotkeyBezelLocalizationKey.originBottom.localizedString,
694+
on: screen
695+
)
696+
}
697+
}
698+
541699
private func bezelScreen(for sender: Any) -> NSScreen? {
542700
if let rulerController = sender as? RulerController {
543701
return rulerController.rulerWindow.screen

0 commit comments

Comments
 (0)