Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
982383b
stubbed out Flip menu
pascalpp Jun 13, 2026
8517ecb
Address flip ruler PR comments
pascalpp Jun 13, 2026
e29b60e
Localize flip submenu titles
pascalpp Jun 13, 2026
f1f195c
Restore zero corner test defaults domain
pascalpp Jun 13, 2026
b4028b5
Add defaultZeroCorner and use in prefs
pascalpp Jun 13, 2026
4a12e81
Address PR feedback on flip shortcuts and defaults
pascalpp Jun 13, 2026
7dcb702
Use default zero corner in UI test reset
pascalpp Jun 14, 2026
df5a6a2
Introduce zero corner geometry helpers
pascalpp Jun 13, 2026
5f55e5d
Align left resize handle inset
pascalpp Jun 13, 2026
c9f996c
Preserve screen origin for default rulers
pascalpp Jun 14, 2026
3564825
Update ruler drawing for zero corners
pascalpp Jun 13, 2026
728a4b0
Mirror unit label mouse tick gating
pascalpp Jun 14, 2026
086db77
Revert "Mirror unit label mouse tick gating"
pascalpp Jun 14, 2026
03892c5
Cover direction-aware resizing
pascalpp Jun 13, 2026
0b93004
Handle grouped zero-corner flips
pascalpp Jun 13, 2026
c7e3f6c
Wire and guard grouped ruler flips
pascalpp Jun 13, 2026
db24323
Add flip ruler UI
pascalpp Jun 13, 2026
e84f0d6
Verify flippable ruler integration
pascalpp Jun 13, 2026
031c854
Derive flipped label test scale from screen
pascalpp Jun 13, 2026
00f4c51
Ignore Caps Lock for ruler hotkeys
pascalpp Jun 13, 2026
5aafebb
Address flip ruler review comments
pascalpp Jun 13, 2026
94e01f9
Anchor labels/handles to opposite sides and update tests
pascalpp Jun 13, 2026
67b5a72
Encapsulate resize-handle placement logic
pascalpp Jun 13, 2026
8cad8a2
Add UnitLabelView and integrate unit labels
pascalpp Jun 13, 2026
cf0702c
Simplify unit label positioning and add insets
pascalpp Jun 13, 2026
51ac56c
Adjust unit label insets and update tests
pascalpp Jun 13, 2026
47d3046
Refactor mouse tick layout & handle visibility
pascalpp Jun 13, 2026
5692401
Add mouse tick label preview views
pascalpp Jun 13, 2026
04802ec
Propagate zeroCorner and refactor previews
pascalpp Jun 13, 2026
a806c6d
Remove RuleViewPreview and simplify previews
pascalpp Jun 13, 2026
211e652
Update Free Ruler help screen
pascalpp Jun 13, 2026
e452fdb
Use current app icon in help book
pascalpp Jun 13, 2026
bd917dc
Update help icon when generating app icons
pascalpp Jun 13, 2026
c0cfeab
Stabilize Free Ruler help book caching
pascalpp Jun 14, 2026
640bbee
Use timestamped help icon filenames
pascalpp Jun 14, 2026
028e0c5
Use CSS var for help icon and refresh assets
pascalpp Jun 14, 2026
9a280ed
Update project.pbxproj
pascalpp Jun 14, 2026
903df52
Update project.pbxproj
pascalpp Jun 14, 2026
3e4f065
Add help-index generator and npm script
pascalpp Jun 14, 2026
f761bef
Fix help bundle identifier & version handling
pascalpp Jun 14, 2026
f43c144
Update set-version.js
pascalpp Jun 14, 2026
cb45972
revert help menu handler
pascalpp Jun 14, 2026
0f71340
Update bump-version.js
pascalpp Jun 14, 2026
35f1a8c
Update generate-app-icon.sh
pascalpp Jun 14, 2026
7383b04
Add interactive mouse tick label preview UI
pascalpp Jun 14, 2026
6ca2be4
Centralize resize handle end-region check
pascalpp Jun 14, 2026
9eff253
Avoid mouse tick label overlapping resize handle
pascalpp Jun 14, 2026
62eac58
Propagate zeroCorner and refactor geometry
pascalpp Jun 14, 2026
cf51789
tweak mouse tick layout and update tests
pascalpp Jun 14, 2026
4167666
Address PR feedback on ruler labels and help assets
pascalpp Jun 14, 2026
6b86b0b
Continue resize handle placement assertions
pascalpp Jun 14, 2026
7b42797
Merge pull request #204 from pascalpp/pascal/198-grouped-zero-corner-…
pascalpp Jun 14, 2026
b5375b1
Merge pull request #203 from pascalpp/pascal/194-direction-aware-resi…
pascalpp Jun 14, 2026
701c689
Merge pull request #202 from pascalpp/pascal/196-ruler-drawing-update
pascalpp Jun 14, 2026
8d9b173
Merge pull request #207 from pascalpp/pascal/193-zero-corner-helpers-v2
pascalpp Jun 14, 2026
bd2ed33
Update English.lproj.helpindex
pascalpp Jun 14, 2026
2a42517
Reset group setting to default in resetRulerPositions
pascalpp Jun 14, 2026
537973a
Reset Ruler Positions → Reset Rulers
pascalpp Jun 14, 2026
aa9df14
Add flip rulers screenshot layout
pascalpp Jun 14, 2026
375e516
Update AppStoreScreenshotPreview.swift
pascalpp Jun 14, 2026
5976562
Update AppStoreScreenshotPreview.swift
pascalpp Jun 14, 2026
051dabc
update screenshots
pascalpp Jun 14, 2026
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
10 changes: 8 additions & 2 deletions Free Ruler.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
50B1D3532D05D00100B1D135 /* RulerCursorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3522D05D00100B1D135 /* RulerCursorController.swift */; };
50B1D3552D05E00000B1D138 /* RulerTickLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */; };
50B1D3592D06000100B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; };
50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
50B1D35D2D06000400B1D139 /* AppIconRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3582D06000100B1D139 /* AppIconRenderer.swift */; };
50B1D3602D06000600B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
50B1D3612D06000700B1D139 /* AppIconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */; };
50B1D3652D06010100B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; };
50B1D3662D06010200B1D139 /* AppStoreScreenshotPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */; };
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D3682D06100000B1D13A /* ResizeHandleView.swift */; };
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D36C2D06110000B1D13B /* UnitLabelView.swift */; };
50C6D891228BDBAD0091F19E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50C6D890228BDBAD0091F19E /* Images.xcassets */; };
50D7BEE7227D42FD0008B95E /* RulerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE6227D42FD0008B95E /* RulerController.swift */; };
50D7BEE9227D43270008B95E /* Ruler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEE8227D43270008B95E /* Ruler.swift */; };
Expand Down Expand Up @@ -118,6 +120,7 @@
50B1D35F2D06000600B1D139 /* AppIconGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGenerator.swift; sourceTree = "<group>"; };
50B1D3642D06010100B1D139 /* AppStoreScreenshotPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshotPreview.swift; sourceTree = "<group>"; };
50B1D3682D06100000B1D13A /* ResizeHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeHandleView.swift; sourceTree = "<group>"; };
50B1D36C2D06110000B1D13B /* UnitLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitLabelView.swift; sourceTree = "<group>"; };
50C6D890228BDBAD0091F19E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
50D7BEE6227D42FD0008B95E /* RulerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RulerController.swift; sourceTree = "<group>"; };
50D7BEE8227D43270008B95E /* Ruler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ruler.swift; sourceTree = "<group>"; };
Expand All @@ -139,7 +142,7 @@
8F629823243003EA004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = "<group>"; };
8F629825243003F6004F9099 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreferencesController.xib; sourceTree = "<group>"; };
8F629828243003FF004F9099 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/PreferencesController.strings; sourceTree = "<group>"; };
AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = "Free Ruler.app"; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; };
AB053EE93E1DC9AF341A8D4F /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; };
B894A5002BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = "<group>"; };
B894A5012BBFE61A005A3B6F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/PreferencesController.strings; sourceTree = "<group>"; };
D9DBE8A12C791B1600A42589 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -252,6 +255,7 @@
50D7BEEA227D432E0008B95E /* RulerWindow.swift */,
50D7BEEC227D5C810008B95E /* RuleView.swift */,
50B1D3682D06100000B1D13A /* ResizeHandleView.swift */,
50B1D36C2D06110000B1D13B /* UnitLabelView.swift */,
50B1D3542D05E00000B1D138 /* RulerTickLayout.swift */,
6F41029E22607DC900F06A10 /* HorizontalRule.swift */,
5012CAAC226AB09000BD9565 /* VerticalRule.swift */,
Expand Down Expand Up @@ -479,6 +483,7 @@
6F4102892260712F00F06A10 /* AppDelegate.swift in Sources */,
50D7BEED227D5C810008B95E /* RuleView.swift in Sources */,
50B1D3672D06100000B1D13A /* ResizeHandleView.swift in Sources */,
50B1D36A2D06110000B1D13B /* UnitLabelView.swift in Sources */,
50D7BEE9227D43270008B95E /* Ruler.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -504,6 +509,7 @@
23E2B80975F07733EB6CF5DB /* AppDelegate.swift in Sources */,
9A082BBC4A583513B0858C41 /* RuleView.swift in Sources */,
50B1D3692D06100000B1D13A /* ResizeHandleView.swift in Sources */,
50B1D36B2D06110000B1D13B /* UnitLabelView.swift in Sources */,
E6BC6663BAF0D11D7A9D2195 /* Ruler.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
168 changes: 163 additions & 5 deletions Free Ruler/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ private enum HotkeyBezelLocalizationKey: String {
case rulersUngrouped = "HotkeyBezel.RulersUngrouped"
case shadowEnabled = "HotkeyBezel.ShadowEnabled"
case shadowDisabled = "HotkeyBezel.ShadowDisabled"
case horizontalOriginFormat = "HotkeyBezel.HorizontalOriginFormat"
case verticalOriginFormat = "HotkeyBezel.VerticalOriginFormat"
case originLeft = "HotkeyBezel.OriginLeft"
case originRight = "HotkeyBezel.OriginRight"
case originTop = "HotkeyBezel.OriginTop"
case originBottom = "HotkeyBezel.OriginBottom"
case unitsFormat = "HotkeyBezel.UnitsFormat"
case pixelsUnit = "Unit.Pixels.Abbreviation"
case millimetersUnit = "Unit.Millimeters.Abbreviation"
Expand All @@ -57,6 +63,18 @@ private enum HotkeyBezelLocalizationKey: String {
return "Hotkey status bezel text indicating ruler shadow is enabled"
case .shadowDisabled:
return "Hotkey status bezel text indicating ruler shadow is disabled"
case .horizontalOriginFormat:
return "Hotkey status bezel format for the horizontal ruler origin side"
case .verticalOriginFormat:
return "Hotkey status bezel format for the vertical ruler origin side"
case .originLeft:
return "Hotkey status bezel value for a ruler origin on the left"
case .originRight:
return "Hotkey status bezel value for a ruler origin on the right"
case .originTop:
return "Hotkey status bezel value for a ruler origin at the top"
case .originBottom:
return "Hotkey status bezel value for a ruler origin at the bottom"
case .unitsFormat:
return "Hotkey status bezel format for the selected measurement unit"
case .pixelsUnit:
Expand Down Expand Up @@ -127,7 +145,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
#endif

showRulers()

}

#if DEBUG
Expand Down Expand Up @@ -218,6 +235,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in
self.redrawRulers()
},
prefs.observe(\Prefs.zeroCorner, options: .new) { prefs, changed in
self.redrawRulers()
},
]
}

Expand Down Expand Up @@ -262,6 +282,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
"backgroundOpacity",
"rulerColor",
"unit",
"zeroCorner",
"NSWindow Frame horizontal-ruler",
"NSWindow Frame vertical-ruler",
"NSWindow Frame preferencesWindow",
Expand All @@ -274,6 +295,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
prefs.backgroundOpacity = 50
prefs.rulerColor = Prefs.defaultRulerFillColor
prefs.unit = .pixels
prefs.zeroCorner = Prefs.defaultZeroCorner
}

func createRulersIfNeeded() {
Expand Down Expand Up @@ -479,15 +501,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBAction func resetRulerPositions(_ sender: Any) {
createRulersIfNeeded()

prefs.zeroCorner = Prefs.defaultZeroCorner

Comment thread
pascalpp marked this conversation as resolved.
// ungroup rulers during reset operation
let groupRulers = prefs.groupRulers
prefs.groupRulers = false
for ruler in rulers {
ruler.resetPosition()
showRuler(ruler)
}
// reset groupRulers to previous value
prefs.groupRulers = groupRulers

prefs.groupRulers = Prefs.defaultGroupRulers
updateRulerGrouping()
}

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

func performRulerHotkey(keyCode: Int, sender: Any) -> Bool {
@IBAction func flipHorizontalRuler(_ sender: Any) {
flipRulers(along: .horizontal)
showHorizontalOriginHotkeyBezel(on: bezelScreen(for: sender))
}

@IBAction func flipVerticalRuler(_ sender: Any) {
flipRulers(along: .vertical)
showVerticalOriginHotkeyBezel(on: bezelScreen(for: sender))
}

func flipRulers(along orientation: Orientation) {
createRulersIfNeeded()

let oldGeometry = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner)
let flippedCorner = prefs.zeroCorner.flipped(along: orientation)
let flippedRuler = existingRulerController(orientation: orientation)
let otherOrientation: Orientation = orientation == .horizontal ? .vertical : .horizontal
let otherRuler = existingRulerController(orientation: otherOrientation)
let zeroPointOffset = zeroPointOffset(
from: flippedRuler?.rulerWindow,
to: otherRuler?.rulerWindow,
geometry: oldGeometry
)

prefs.zeroCorner = flippedCorner

guard prefs.groupRulers,
let flippedWindow = flippedRuler?.rulerWindow,
let otherWindow = otherRuler?.rulerWindow,
isRulerWindowShown(otherWindow),
let zeroPointOffset = zeroPointOffset else { return }

let newGeometry = ZeroCornerGeometry(zeroCorner: flippedCorner)
let flippedZeroPoint = newGeometry.zeroPoint(in: flippedWindow.frame, for: orientation)
let targetOtherZeroPoint = NSPoint(
x: flippedZeroPoint.x + zeroPointOffset.width,
y: flippedZeroPoint.y + zeroPointOffset.height
)
let otherFrame = newGeometry.frame(
for: otherOrientation,
zeroPoint: targetOtherZeroPoint,
size: otherWindow.frame.size
)

detachRulerWindows()
otherWindow.setFrame(otherFrame, display: true)
updateRulerGrouping()
}

func isRulerWindowShown(_ window: RulerWindow) -> Bool {
return window.isVisible || window.parent != nil || rulers.contains {
$0.rulerWindow.childWindows?.contains(window) == true
}
}

private func zeroPointOffset(
from sourceWindow: RulerWindow?,
to targetWindow: RulerWindow?,
geometry: ZeroCornerGeometry
) -> NSSize? {
guard let sourceWindow = sourceWindow,
let targetWindow = targetWindow else { return nil }

let sourceZeroPoint = geometry.zeroPoint(
in: sourceWindow.frame,
for: sourceWindow.ruler.orientation
)
let targetZeroPoint = geometry.zeroPoint(
in: targetWindow.frame,
for: targetWindow.ruler.orientation
)

return NSSize(
width: targetZeroPoint.x - sourceZeroPoint.x,
height: targetZeroPoint.y - sourceZeroPoint.y
)
}

func performRulerHotkey(
keyCode: Int,
modifierFlags: NSEvent.ModifierFlags,
sender: Any
) -> Bool {
let keyboardModifiers = modifierFlags
.intersection(.deviceIndependentFlagsMask)
.subtracting(.capsLock)

if keyboardModifiers == .shift {
switch keyCode {
case kVK_ANSI_H:
flipHorizontalRuler(sender)
case kVK_ANSI_V:
flipVerticalRuler(sender)
default:
return false
}

return true
}

guard keyboardModifiers.isEmpty else { return false }

switch keyCode {
case kVK_ANSI_H:
toggleHorizontalRuler(sender)
Expand Down Expand Up @@ -538,6 +662,40 @@ class AppDelegate: NSObject, NSApplicationDelegate {
showHotkeyBezel(prefs.groupRulers ? .rulersGrouped : .rulersUngrouped, on: screen)
}

private func showHorizontalOriginHotkeyBezel(on screen: NSScreen?) {
switch prefs.zeroCorner {
case .topLeft, .bottomLeft:
showHotkeyBezel(
format: .horizontalOriginFormat,
HotkeyBezelLocalizationKey.originLeft.localizedString,
on: screen
)
case .topRight, .bottomRight:
showHotkeyBezel(
format: .horizontalOriginFormat,
HotkeyBezelLocalizationKey.originRight.localizedString,
on: screen
)
}
}

private func showVerticalOriginHotkeyBezel(on screen: NSScreen?) {
switch prefs.zeroCorner {
case .topLeft, .topRight:
showHotkeyBezel(
format: .verticalOriginFormat,
HotkeyBezelLocalizationKey.originTop.localizedString,
on: screen
)
case .bottomLeft, .bottomRight:
showHotkeyBezel(
format: .verticalOriginFormat,
HotkeyBezelLocalizationKey.originBottom.localizedString,
on: screen
)
}
}

private func bezelScreen(for sender: Any) -> NSScreen? {
if let rulerController = sender as? RulerController {
return rulerController.rulerWindow.screen
Expand Down
Loading