diff --git a/Free Ruler/AppDelegate.swift b/Free Ruler/AppDelegate.swift
index f5ce1a4..ba5aed8 100644
--- a/Free Ruler/AppDelegate.swift
+++ b/Free Ruler/AppDelegate.swift
@@ -65,7 +65,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
lazy var rulerManager: RulerManager = {
let manager = RulerManager()
manager.onActiveControllerChanged = { [weak self] controller in
- self?.groupedRulerController = controller
+ guard let self = self else { return }
+
+ self.groupedRulerController = controller
+
+ guard let settingsController = self.rulerSettingsController,
+ settingsController.window?.isVisible == true else { return }
+
+ if let controller = controller {
+ settingsController.show(attachedTo: controller, sender: self)
+ } else {
+ settingsController.close()
+ }
}
return manager
}()
@@ -93,6 +104,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var alignRulersMenuItem: NSMenuItem!
var preferencesController: PreferencesController? = nil
+ var rulerSettingsController: RulerSettingsController? = nil
private let hotkeyBezel = HotkeyBezel()
private var uiTestSupport: UITestSupport?
private var interfaceStyleObserver: NSObjectProtocol?
@@ -271,14 +283,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
observers = [
prefs.observe(\Prefs.unit, options: .new) { prefs, changed in
self.updateUnitMenu()
- self.redrawRulers()
+ self.redrawDefaultBackedRulers()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.floatRulers, options: .new) { prefs, changed in
self.updateFloatRulersMenuItem()
- for controller in self.rulerManager.controllers {
- controller.updateIsFloatingPanel()
- }
self.legacyGroupedRulerController?.updateIsFloatingPanel()
self.uiTestSupport?.writePreferencesState()
},
@@ -289,17 +298,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
},
prefs.observe(\Prefs.rulerShadow, options: .new) { prefs, changed in
self.updateRulerShadowMenuItem()
- for controller in self.rulerManager.controllers {
- controller.updateHasShadow()
- }
self.legacyGroupedRulerController?.updateHasShadow()
self.uiTestSupport?.writePreferencesState()
},
prefs.observe(\Prefs.rulerColor, options: .new) { prefs, changed in
- self.redrawRulers()
+ self.redrawDefaultBackedRulers()
},
prefs.observe(\Prefs.zeroCorner, options: .new) { prefs, changed in
- self.redrawRulers()
+ self.redrawDefaultBackedRulers()
},
]
}
@@ -327,6 +333,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
legacyGroupedRulerController?.redrawForPreferenceChange()
}
+ func redrawDefaultBackedRulers() {
+ for ruler in rulers {
+ ruler.rulerWindow.rule.redrawForPreferenceChange()
+ }
+ legacyGroupedRulerController?.redrawForPreferenceChange()
+ }
+
func updateFloatRulersMenuItem() {
floatRulersMenuItem?.state = prefs.floatRulers ? .on : .off
}
@@ -662,6 +675,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
+ @IBAction func openRulerSettings(_ sender: Any) {
+ guard let controller = rulerManager.activeController else { return }
+
+ if rulerSettingsController == nil {
+ rulerSettingsController = RulerSettingsController(rulerController: controller)
+ }
+
+ rulerSettingsController?.show(attachedTo: controller, sender: sender)
+ }
+
@IBAction func newRuler(_ sender: Any) {
let controller = rulerManager.createRuler()
controller.show()
@@ -983,6 +1006,8 @@ extension AppDelegate: NSMenuItemValidation {
switch menuItem.action {
case #selector(newRuler(_:)):
return true
+ case #selector(openRulerSettings(_:)):
+ return rulerManager.activeController != nil
case #selector(closeKeyWindow(_:)):
return NSApp.keyWindow?.isVisible == true
case #selector(toggleGroupRulers(_:)):
diff --git a/Free Ruler/Base.lproj/MainMenu.xib b/Free Ruler/Base.lproj/MainMenu.xib
index 9813031..aa26542 100644
--- a/Free Ruler/Base.lproj/MainMenu.xib
+++ b/Free Ruler/Base.lproj/MainMenu.xib
@@ -98,6 +98,12 @@
+
diff --git a/Free Ruler/GroupedRulerWindow.swift b/Free Ruler/GroupedRulerWindow.swift
index d85f3fc..6466054 100644
--- a/Free Ruler/GroupedRulerWindow.swift
+++ b/Free Ruler/GroupedRulerWindow.swift
@@ -267,8 +267,10 @@ final class GroupedRulerWindow: NSPanel {
let verticalRule: VerticalRule
private let groupedContentView: GroupedRulerContentView
+ private(set) var settings: RulerSettings
- init(frame: NSRect) {
+ init(frame: NSRect, settings: RulerSettings = RulerSettings(defaults: prefs)) {
+ self.settings = settings
horizontalRule = GroupedHorizontalRule(
frame: NSRect(x: 0, y: 0, width: 300, height: Ruler.thickness)
)
@@ -294,22 +296,22 @@ final class GroupedRulerWindow: NSPanel {
defer: false
)
- alphaValue = windowAlphaValue(prefs.foregroundOpacity)
+ alphaValue = windowAlphaValue(settings.foregroundOpacity)
title = NSLocalizedString(
"Ruler",
comment: "Window title for a ruler window"
)
identifier = NSUserInterfaceItemIdentifier("grouped-ruler-window")
setAccessibilityIdentifier("grouped-ruler-window")
- minSize = GroupedRulerLayout.minSize(zeroCorner: prefs.zeroCorner)
- maxSize = GroupedRulerLayout.maxSize(zeroCorner: prefs.zeroCorner)
+ minSize = GroupedRulerLayout.minSize(zeroCorner: settings.zeroCorner)
+ maxSize = GroupedRulerLayout.maxSize(zeroCorner: settings.zeroCorner)
isOpaque = false
backgroundColor = .clear
- isFloatingPanel = prefs.floatRulers
+ isFloatingPanel = settings.floatRulers
hidesOnDeactivate = false
isMovableByWindowBackground = true
- hasShadow = prefs.rulerShadow
+ hasShadow = settings.rulerShadow
horizontalRule.setAccessibilityElement(true)
verticalRule.setAccessibilityElement(true)
@@ -320,6 +322,7 @@ final class GroupedRulerWindow: NSPanel {
groupedContentView.nextResponder = self
contentView = groupedContentView
+ apply(settings: settings)
updateLayoutForCurrentZeroCorner()
}
@@ -371,12 +374,23 @@ final class GroupedRulerWindow: NSPanel {
func updateLayoutForCurrentZeroCorner() {
updateSizeConstraintsForVisibleRules()
updateGroupedContentFrame()
- groupedContentView.zeroCorner = prefs.zeroCorner
+ groupedContentView.zeroCorner = settings.zeroCorner
groupedContentView.needsLayout = true
groupedContentView.layoutSubtreeIfNeeded()
groupedContentView.needsDisplay = true
}
+ func apply(settings: RulerSettings) {
+ self.settings = settings
+ alphaValue = windowAlphaValue(settings.foregroundOpacity)
+ isFloatingPanel = settings.floatRulers
+ hasShadow = settings.rulerShadow
+ horizontalRule.settingsOverride = settings
+ verticalRule.settingsOverride = settings
+ groupedContentView.color = RulerColors(customFill: settings.rulerColor)
+ updateLayoutForCurrentZeroCorner()
+ }
+
func redrawForPreferenceChange() {
updateLayoutForCurrentZeroCorner()
horizontalRule.redrawForPreferenceChange()
@@ -417,7 +431,7 @@ final class GroupedRulerWindow: NSPanel {
}
func zeroPoint() -> NSPoint {
- let geometry = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner)
+ let geometry = ZeroCornerGeometry(zeroCorner: settings.zeroCorner)
if isRuleVisible(.horizontal) {
return geometry.zeroPoint(
@@ -450,12 +464,12 @@ final class GroupedRulerWindow: NSPanel {
private func updateSizeConstraintsForVisibleRules() {
minSize = GroupedRulerLayout.minSize(
- zeroCorner: prefs.zeroCorner,
+ zeroCorner: settings.zeroCorner,
showsHorizontalRule: groupedContentView.showsHorizontalRule,
showsVerticalRule: groupedContentView.showsVerticalRule
)
maxSize = GroupedRulerLayout.maxSize(
- zeroCorner: prefs.zeroCorner,
+ zeroCorner: settings.zeroCorner,
showsHorizontalRule: groupedContentView.showsHorizontalRule,
showsVerticalRule: groupedContentView.showsVerticalRule
)
@@ -1076,6 +1090,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
private var keyListener: Any?
private var mouseInteraction: RulerMouseInteractionState!
private var isMouseTickDrawingEnabled = true
+ private let followsDefaultPreferences: Bool
var isLeftMouseButtonPressed = {
return NSEvent.pressedMouseButtons & 1 == 1
@@ -1085,12 +1100,12 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
didSet {
updateIsFloatingPanel()
if !preferencesWindowOpen {
- opacity = prefs.foregroundOpacity
+ opacity = state.settings.foregroundOpacity
}
}
}
- var opacity = prefs.foregroundOpacity {
+ var opacity = 0 {
didSet {
groupedWindow.alphaValue = windowAlphaValue(opacity)
}
@@ -1107,20 +1122,27 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
)
)
- self.init(state: state)
+ self.init(state: state, followsDefaultPreferences: true)
+ }
+
+ convenience init(state: RulerInstanceState) {
+ self.init(state: state, followsDefaultPreferences: false)
}
- init(state: RulerInstanceState) {
+ private init(state: RulerInstanceState, followsDefaultPreferences: Bool) {
self.state = state
+ self.followsDefaultPreferences = followsDefaultPreferences
let layout = state.layout.layout(zeroCorner: state.settings.zeroCorner)
groupedWindow = GroupedRulerWindow(
frame: layout.visibleFrame(
showsHorizontalRule: state.visibility.showsHorizontal,
showsVerticalRule: state.visibility.showsVertical
- )
+ ),
+ settings: state.settings
)
super.init(window: groupedWindow)
+ opacity = state.settings.foregroundOpacity
createObservers()
subscribeToPrefs()
@@ -1216,7 +1238,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
horizontalLength: horizontalLength,
verticalLength: verticalLength,
zeroPoint: point,
- zeroCorner: prefs.zeroCorner
+ zeroCorner: state.settings.zeroCorner
)
groupedWindow.setFrame(groupedWindow.visibleFrame(in: layout), display: true)
@@ -1235,31 +1257,41 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
zeroCorner: zeroCorner
)
- groupedWindow.setFrame(groupedWindow.visibleFrame(in: layout), display: true)
state.settings.zeroCorner = zeroCorner
+ groupedWindow.apply(settings: state.settings)
+ groupedWindow.alphaValue = windowAlphaValue(opacity)
+ updateIsFloatingPanel()
+ updateHasShadow()
+ groupedWindow.setFrame(groupedWindow.visibleFrame(in: layout), display: true)
captureStateFromWindow()
}
func foreground() {
- opacity = prefs.foregroundOpacity
+ opacity = state.settings.foregroundOpacity
}
func background() {
- opacity = prefs.backgroundOpacity
+ opacity = state.settings.backgroundOpacity
}
func updateIsFloatingPanel() {
- groupedWindow.isFloatingPanel = preferencesWindowOpen ? false : prefs.floatRulers
+ groupedWindow.isFloatingPanel = preferencesWindowOpen ? false : state.settings.floatRulers
}
func updateHasShadow() {
- groupedWindow.hasShadow = prefs.rulerShadow
+ groupedWindow.hasShadow = state.settings.rulerShadow
}
func redrawForPreferenceChange() {
groupedWindow.redrawForPreferenceChange()
}
+ func updateSettings(_ update: (inout RulerSettings) -> Void) {
+ update(&state.settings)
+ applyStateToWindow(display: true)
+ notifyStateChanged()
+ }
+
func drawMouseTick(at mouseLoc: NSPoint) {
if groupedWindow.isRuleVisible(.horizontal) {
groupedWindow.horizontalRule.drawMouseTick(at: mouseLoc)
@@ -1284,6 +1316,10 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
private func applyStateToWindow(display: Bool) {
let zeroCorner = state.settings.zeroCorner
let layout = state.layout.layout(zeroCorner: zeroCorner)
+ groupedWindow.apply(settings: state.settings)
+ groupedWindow.alphaValue = windowAlphaValue(opacity)
+ updateIsFloatingPanel()
+ updateHasShadow()
groupedWindow.setVisibleRules(
horizontal: state.visibility.showsHorizontal,
vertical: state.visibility.showsVertical
@@ -1341,7 +1377,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
)
case (true, false):
let horizontalFrame = groupedWindow.screenFrame(for: .horizontal)
- let zeroPoint = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner)
+ let zeroPoint = ZeroCornerGeometry(zeroCorner: state.settings.zeroCorner)
.zeroPoint(in: horizontalFrame, for: .horizontal)
return (
horizontalFrame,
@@ -1353,7 +1389,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
)
case (false, true):
let verticalFrame = groupedWindow.screenFrame(for: .vertical)
- let zeroPoint = ZeroCornerGeometry(zeroCorner: prefs.zeroCorner)
+ let zeroPoint = ZeroCornerGeometry(zeroCorner: state.settings.zeroCorner)
.zeroPoint(in: verticalFrame, for: .vertical)
return (
hiddenRuleFrame(
@@ -1373,7 +1409,7 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
zeroPoint: NSPoint,
size: NSSize
) -> NSRect {
- return ZeroCornerGeometry(zeroCorner: prefs.zeroCorner).frame(
+ return ZeroCornerGeometry(zeroCorner: state.settings.zeroCorner).frame(
for: orientation,
zeroPoint: zeroPoint,
size: size
@@ -1497,6 +1533,11 @@ final class GroupedRulerController: NSWindowController, NSWindowDelegate, Notifi
}
private func subscribeToPrefs() {
+ guard followsDefaultPreferences else {
+ observers = []
+ return
+ }
+
observers = [
prefs.observe(\Prefs.foregroundOpacity, options: .new) { [weak self] prefs, changed in
self?.opacity = prefs.foregroundOpacity
diff --git a/Free Ruler/Localizable.xcstrings b/Free Ruler/Localizable.xcstrings
index 0d6e296..7d32227 100644
--- a/Free Ruler/Localizable.xcstrings
+++ b/Free Ruler/Localizable.xcstrings
@@ -43,6 +43,60 @@
}
}
},
+ "Close" : {
+ "comment" : "Button title for closing the active ruler settings panel",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Schließen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Close"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cerrar"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Sulje"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "閉じる"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "关闭"
+ }
+ }
+ }
+ },
+ "Color" : {
+ "comment" : "Label for the active ruler color setting",
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Color"
+ }
+ }
+ }
+ },
"Ruler" : {
"comment" : "Window title for a ruler window",
"extractionState" : "manual",
@@ -55,6 +109,228 @@
}
}
},
+ "Ruler Settings" : {
+ "comment" : "Window title for the active ruler settings panel",
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ruler Settings"
+ }
+ }
+ }
+ },
+ "Ruler Color" : {
+ "comment" : "Label for the active ruler color setting",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Linealfarbe"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Ruler Color"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Color de la regla"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Viivaimen väri"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "定規の色"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "尺子颜色"
+ }
+ }
+ }
+ },
+ "Foreground Opacity" : {
+ "comment" : "Label for the active ruler foreground opacity setting",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Deckkraft im Vordergrund"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Foreground Opacity"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Opacidad del primer plano"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Peittävyys edustalla"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "前景の不透明度"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "前景不透明度"
+ }
+ }
+ }
+ },
+ "Background Opacity" : {
+ "comment" : "Label for the active ruler background opacity setting",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Deckkraft im Hintergrund"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Background Opacity"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Opacidad del fondo"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Peittävyys taustalla"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "背景の不透明度"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "背景不透明度"
+ }
+ }
+ }
+ },
+ "Float rulers above other applications" : {
+ "comment" : "Checkbox title for whether the active ruler floats above other apps",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Lineale schweben über anderen Programmen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Float rulers above other applications"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reglas flotantes sobre otras aplicaciones"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Kelluta viivaimia muiden sovellusten päällä"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "定規を常に手前に表示"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "让标尺窗口悬浮在其他应用程序之上"
+ }
+ }
+ }
+ },
+ "Show ruler shadow" : {
+ "comment" : "Checkbox title for whether the active ruler draws a window shadow",
+ "extractionState" : "manual",
+ "localizations" : {
+ "de" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Linealschatten anzeigen"
+ }
+ },
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Show ruler shadow"
+ }
+ },
+ "es" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Mostrar sombra de la regla"
+ }
+ },
+ "fi" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Näytä viivainten varjot"
+ }
+ },
+ "ja" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "定規の影を表示"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "显示尺子阴影"
+ }
+ }
+ }
+ },
"Hide Horizontal Ruler" : {
"comment" : "Menu item title to hide the horizontal ruler",
"extractionState" : "manual",
diff --git a/Free Ruler/PreferencesController.swift b/Free Ruler/PreferencesController.swift
index feb9d3c..d7e8dff 100644
--- a/Free Ruler/PreferencesController.swift
+++ b/Free Ruler/PreferencesController.swift
@@ -3,6 +3,7 @@ import Cocoa
private let colorPanelOpaqueConfigurationRetryDelays: [TimeInterval] = [0.1, 0.3]
private let rulerColorPanelIdentifier = NSUserInterfaceItemIdentifier("ruler-color-panel")
private let rulerColorPanelOpaqueAccessibilityValue = "ruler-color-panel-alpha-hidden"
+private weak var activeRulerColorWell: RulerColorWell?
func configureOpaqueColorPicking() {
let colorPanel = NSColorPanel.shared
@@ -57,6 +58,7 @@ class RulerColorWell: NSColorWell {
return
}
+ activeRulerColorWell = self
colorPanel.animationBehavior = .none
colorPanel.color = color
colorPanel.setTarget(self)
@@ -88,6 +90,30 @@ class RulerColorWell: NSColorWell {
}
+private func configureResetRulerColorButtonAppearance(_ button: NSButton, identifier: String) {
+ let resetRulerColorLabel = NSLocalizedString(
+ "Reset ruler color",
+ comment: "Tooltip and accessibility label for the button that restores the default ruler color"
+ )
+ let symbol = NSImage(
+ systemSymbolName: "arrow.counterclockwise",
+ accessibilityDescription: resetRulerColorLabel
+ )?.withSymbolConfiguration(
+ NSImage.SymbolConfiguration(pointSize: 14, weight: .regular)
+ ) ?? NSImage()
+ symbol.isTemplate = true
+
+ button.image = symbol
+ button.isBordered = false
+ button.imagePosition = .imageOnly
+ button.imageScaling = .scaleProportionallyDown
+ button.contentTintColor = .secondaryLabelColor
+ button.toolTip = resetRulerColorLabel
+ button.identifier = NSUserInterfaceItemIdentifier(identifier)
+ button.setAccessibilityIdentifier(identifier)
+ button.setAccessibilityLabel(resetRulerColorLabel)
+}
+
class PreferencesController: NSWindowController, NSWindowDelegate, NotificationPoster {
var observers: [NSKeyValueObservation] = []
@@ -243,27 +269,7 @@ class PreferencesController: NSWindowController, NSWindowDelegate, NotificationP
}
private func configureResetRulerColorButton() {
- let resetRulerColorLabel = NSLocalizedString(
- "Reset ruler color",
- comment: "Tooltip and accessibility label for the button that restores the default ruler color"
- )
- let symbol = NSImage(
- systemSymbolName: "arrow.counterclockwise",
- accessibilityDescription: resetRulerColorLabel
- )?.withSymbolConfiguration(
- NSImage.SymbolConfiguration(pointSize: 14, weight: .regular)
- ) ?? NSImage()
- symbol.isTemplate = true
-
- resetRulerColorButton.image = symbol
- resetRulerColorButton.isBordered = false
- resetRulerColorButton.imagePosition = .imageOnly
- resetRulerColorButton.imageScaling = .scaleProportionallyDown
- resetRulerColorButton.contentTintColor = .secondaryLabelColor
- resetRulerColorButton.toolTip = resetRulerColorLabel
- resetRulerColorButton.identifier = NSUserInterfaceItemIdentifier("reset-ruler-color-button")
- resetRulerColorButton.setAccessibilityIdentifier("reset-ruler-color-button")
- resetRulerColorButton.setAccessibilityLabel(resetRulerColorLabel)
+ configureResetRulerColorButtonAppearance(resetRulerColorButton, identifier: "reset-ruler-color-button")
}
private func subscribeToColorPanel() {
@@ -279,14 +285,503 @@ class PreferencesController: NSWindowController, NSWindowDelegate, NotificationP
private func updateRulerColorFromColorPanel(_ notification: Notification) {
guard window?.isVisible == true,
let colorPanel = notification.object as? NSColorPanel,
- colorPanel.isVisible else { return }
+ colorPanel.isVisible,
+ activeRulerColorWell === rulerColorWell else { return }
prefs.rulerColor = colorPanel.color
}
}
+final class RulerSettingsController: NSWindowController, NSWindowDelegate {
+
+ private weak var rulerController: GroupedRulerController?
+ private var colorPanelObserver: NSObjectProtocol?
+
+ let rulerColorWell: RulerColorWell
+ let resetRulerColorButton: NSButton
+ let foregroundOpacitySlider: NSSlider
+ let backgroundOpacitySlider: NSSlider
+ let foregroundOpacityLabel: NSTextField
+ let backgroundOpacityLabel: NSTextField
+ let floatRulersCheckbox: NSButton
+ let rulerShadowCheckbox: NSButton
+ let closeButton: NSButton
+
+ var currentRulerController: GroupedRulerController? {
+ return rulerController
+ }
+
+ init(rulerController: GroupedRulerController) {
+ self.rulerController = rulerController
+
+ rulerColorWell = RulerColorWell(frame: NSRect(x: 0, y: 0, width: 64, height: 30))
+ resetRulerColorButton = NSButton(frame: .zero)
+ foregroundOpacitySlider = RulerSettingsController.makeOpacitySlider()
+ backgroundOpacitySlider = RulerSettingsController.makeOpacitySlider()
+ foregroundOpacityLabel = RulerSettingsController.makeValueLabel()
+ backgroundOpacityLabel = RulerSettingsController.makeValueLabel()
+ floatRulersCheckbox = NSButton(
+ checkboxWithTitle: NSLocalizedString(
+ "Float rulers above other applications",
+ comment: "Checkbox title for whether the active ruler floats above other apps"
+ ),
+ target: nil,
+ action: nil
+ )
+ rulerShadowCheckbox = NSButton(
+ checkboxWithTitle: NSLocalizedString(
+ "Show ruler shadow",
+ comment: "Checkbox title for whether the active ruler draws a window shadow"
+ ),
+ target: nil,
+ action: nil
+ )
+ closeButton = NSButton(
+ title: NSLocalizedString(
+ "Close",
+ comment: "Button title for closing the active ruler settings panel"
+ ),
+ target: nil,
+ action: nil
+ )
+
+ let window = RulerSettingsController.makeWindow(
+ rulerColorWell: rulerColorWell,
+ resetRulerColorButton: resetRulerColorButton,
+ foregroundOpacitySlider: foregroundOpacitySlider,
+ backgroundOpacitySlider: backgroundOpacitySlider,
+ foregroundOpacityLabel: foregroundOpacityLabel,
+ backgroundOpacityLabel: backgroundOpacityLabel,
+ floatRulersCheckbox: floatRulersCheckbox,
+ rulerShadowCheckbox: rulerShadowCheckbox,
+ closeButton: closeButton
+ )
+
+ super.init(window: window)
+
+ window.delegate = self
+ window.initialFirstResponder = rulerColorWell
+ configureControls()
+ subscribeToColorPanel()
+ updateView()
+ }
+
+ required init?(coder: NSCoder) {
+ nil
+ }
+
+ deinit {
+ if let colorPanelObserver = colorPanelObserver {
+ NotificationCenter.default.removeObserver(colorPanelObserver)
+ }
+ }
+
+ override func showWindow(_ sender: Any?) {
+ detachWindowIfNeeded()
+ configureOpaqueColorPicking()
+ updateView()
+ window?.makeKeyAndOrderFront(sender)
+ window?.makeFirstResponder(rulerColorWell)
+ window?.center()
+ }
+
+ func show(attachedTo controller: GroupedRulerController, sender: Any?) {
+ updateRulerController(controller)
+ guard let settingsWindow = window else { return }
+
+ configureOpaqueColorPicking()
+
+ if settingsWindow.parent === controller.groupedWindow {
+ position(settingsWindow, attachedTo: controller)
+ settingsWindow.orderFront(sender)
+ settingsWindow.makeKey()
+ settingsWindow.makeFirstResponder(rulerColorWell)
+ return
+ }
+
+ detachWindowIfNeeded()
+
+ guard controller.groupedWindow.isVisible else {
+ showWindow(sender)
+ return
+ }
+
+ if settingsWindow.isVisible {
+ settingsWindow.orderOut(sender)
+ }
+
+ position(settingsWindow, attachedTo: controller)
+ controller.groupedWindow.addChildWindow(settingsWindow, ordered: .above)
+ settingsWindow.orderFront(sender)
+ settingsWindow.makeKey()
+ settingsWindow.makeFirstResponder(rulerColorWell)
+ }
+
+ override func close() {
+ detachWindowIfNeeded()
+ super.close()
+ }
+
+ func windowShouldClose(_ sender: NSWindow) -> Bool {
+ true
+ }
+
+ func windowWillClose(_ notification: Notification) {
+ detachWindowIfNeeded()
+ closeSheetColorControls()
+ }
+
+ func updateRulerController(_ controller: GroupedRulerController) {
+ rulerController = controller
+ updateView()
+ }
+
+ @objc func setForegroundOpacity(_ sender: Any) {
+ applySettings { settings in
+ settings.foregroundOpacity = foregroundOpacitySlider.integerValue
+ }
+ rulerController?.opacity = foregroundOpacitySlider.integerValue
+ updateView()
+ }
+
+ @objc func setBackgroundOpacity(_ sender: Any) {
+ applySettings { settings in
+ settings.backgroundOpacity = backgroundOpacitySlider.integerValue
+ }
+ rulerController?.opacity = backgroundOpacitySlider.integerValue
+ updateView()
+ }
+
+ @objc func setFloatRulers(_ sender: Any) {
+ applySettings { settings in
+ settings.floatRulers = floatRulersCheckbox.state == .on
+ }
+ updateView()
+ }
+
+ @objc func setRulerShadow(_ sender: Any) {
+ applySettings { settings in
+ settings.rulerShadow = rulerShadowCheckbox.state == .on
+ }
+ updateView()
+ }
+
+ @objc func setRulerColor(_ sender: Any) {
+ applyRulerColor(rulerColorWell.color)
+ }
+
+ @objc func resetRulerColor(_ sender: Any) {
+ applyRulerColor(Prefs.defaultRulerFillColor)
+ }
+
+ @objc func closeRulerSettings(_ sender: Any) {
+ close()
+ }
+
+ func updateView() {
+ let currentColor = rulerController?.state.settings.rulerColor ?? Prefs.defaultRulerFillColor
+ let currentSettings = rulerController?.state.settings
+ let hasRuler = rulerController != nil
+
+ rulerColorWell.supportsAlpha = false
+ rulerColorWell.color = currentColor
+ rulerColorWell.isEnabled = hasRuler
+ resetRulerColorButton.isEnabled = hasRuler
+ resetRulerColorButton.isHidden = Prefs.colorsMatch(currentColor, Prefs.defaultRulerFillColor)
+ foregroundOpacitySlider.isEnabled = hasRuler
+ backgroundOpacitySlider.isEnabled = hasRuler
+ floatRulersCheckbox.isEnabled = hasRuler
+ rulerShadowCheckbox.isEnabled = hasRuler
+
+ foregroundOpacitySlider.integerValue = currentSettings?.foregroundOpacity ?? 90
+ backgroundOpacitySlider.integerValue = currentSettings?.backgroundOpacity ?? 50
+ foregroundOpacityLabel.stringValue = "\(foregroundOpacitySlider.integerValue)%"
+ backgroundOpacityLabel.stringValue = "\(backgroundOpacitySlider.integerValue)%"
+ floatRulersCheckbox.state = currentSettings?.floatRulers == true ? .on : .off
+ rulerShadowCheckbox.state = currentSettings?.rulerShadow == true ? .on : .off
+ }
+
+ private static func makeWindow(
+ rulerColorWell: RulerColorWell,
+ resetRulerColorButton: NSButton,
+ foregroundOpacitySlider: NSSlider,
+ backgroundOpacitySlider: NSSlider,
+ foregroundOpacityLabel: NSTextField,
+ backgroundOpacityLabel: NSTextField,
+ floatRulersCheckbox: NSButton,
+ rulerShadowCheckbox: NSButton,
+ closeButton: NSButton
+ ) -> NSPanel {
+ let contentView = NSView()
+ let colorLabel = makeLabel(
+ NSLocalizedString(
+ "Ruler Color",
+ comment: "Label for the active ruler color setting"
+ )
+ )
+ let foregroundLabel = makeLabel(
+ NSLocalizedString(
+ "Foreground Opacity",
+ comment: "Label for the active ruler foreground opacity setting"
+ )
+ )
+ let backgroundLabel = makeLabel(
+ NSLocalizedString(
+ "Background Opacity",
+ comment: "Label for the active ruler background opacity setting"
+ )
+ )
+ let colorRow = NSStackView(views: [colorLabel, resetRulerColorButton, rulerColorWell])
+ let foregroundHeaderRow = NSStackView(views: [foregroundLabel, foregroundOpacityLabel])
+ let backgroundHeaderRow = NSStackView(views: [backgroundLabel, backgroundOpacityLabel])
+ let closeRow = NSView()
+ closeRow.addSubview(closeButton)
+ let contentStack = NSStackView(views: [
+ colorRow,
+ foregroundHeaderRow,
+ foregroundOpacitySlider,
+ backgroundHeaderRow,
+ backgroundOpacitySlider,
+ floatRulersCheckbox,
+ rulerShadowCheckbox,
+ closeRow,
+ ])
+
+ for row in [colorRow, foregroundHeaderRow, backgroundHeaderRow] {
+ row.orientation = .horizontal
+ row.alignment = .centerY
+ row.distribution = .fill
+ row.spacing = 10
+ row.translatesAutoresizingMaskIntoConstraints = false
+ }
+
+ contentStack.orientation = .vertical
+ contentStack.alignment = .leading
+ contentStack.distribution = .fill
+ contentStack.spacing = 8
+ contentStack.translatesAutoresizingMaskIntoConstraints = false
+
+ colorLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ foregroundLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ backgroundLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+ foregroundOpacityLabel.setContentHuggingPriority(.required, for: .horizontal)
+ backgroundOpacityLabel.setContentHuggingPriority(.required, for: .horizontal)
+ rulerColorWell.translatesAutoresizingMaskIntoConstraints = false
+ resetRulerColorButton.translatesAutoresizingMaskIntoConstraints = false
+ foregroundOpacitySlider.translatesAutoresizingMaskIntoConstraints = false
+ backgroundOpacitySlider.translatesAutoresizingMaskIntoConstraints = false
+ floatRulersCheckbox.translatesAutoresizingMaskIntoConstraints = false
+ rulerShadowCheckbox.translatesAutoresizingMaskIntoConstraints = false
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
+ closeRow.translatesAutoresizingMaskIntoConstraints = false
+
+ contentView.addSubview(contentStack)
+ NSLayoutConstraint.activate([
+ colorRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ foregroundHeaderRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ backgroundHeaderRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ closeRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ rulerColorWell.widthAnchor.constraint(equalToConstant: 60),
+ rulerColorWell.heightAnchor.constraint(equalToConstant: 24),
+ resetRulerColorButton.widthAnchor.constraint(equalToConstant: 28),
+ resetRulerColorButton.heightAnchor.constraint(equalToConstant: 28),
+ foregroundOpacitySlider.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ backgroundOpacitySlider.widthAnchor.constraint(equalTo: contentStack.widthAnchor),
+ floatRulersCheckbox.widthAnchor.constraint(lessThanOrEqualTo: contentStack.widthAnchor),
+ rulerShadowCheckbox.widthAnchor.constraint(lessThanOrEqualTo: contentStack.widthAnchor),
+ closeButton.trailingAnchor.constraint(equalTo: closeRow.trailingAnchor),
+ closeButton.topAnchor.constraint(equalTo: closeRow.topAnchor),
+ closeButton.bottomAnchor.constraint(equalTo: closeRow.bottomAnchor),
+ closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: closeRow.leadingAnchor),
+ contentStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
+ contentStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
+ contentStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18),
+ contentStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -18),
+ ])
+
+ let window = NSPanel(
+ contentRect: NSRect(x: 0, y: 0, width: 315, height: 270),
+ styleMask: [.titled, .closable],
+ backing: .buffered,
+ defer: false
+ )
+ window.title = NSLocalizedString(
+ "Ruler Settings",
+ comment: "Window title for the active ruler settings panel"
+ )
+ window.contentView = contentView
+ window.identifier = NSUserInterfaceItemIdentifier("ruler-settings-window")
+ window.setAccessibilityIdentifier("ruler-settings-window")
+ window.isMovableByWindowBackground = true
+ window.isReleasedWhenClosed = false
+ return window
+ }
+
+ private static func makeLabel(_ title: String) -> NSTextField {
+ let label = NSTextField(labelWithString: title)
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ return label
+ }
+
+ private static func makeValueLabel() -> NSTextField {
+ let label = NSTextField(labelWithString: "")
+ label.alignment = .right
+ return label
+ }
+
+ private static func makeOpacitySlider() -> NSSlider {
+ let slider = NSSlider(frame: .zero)
+ slider.minValue = 5
+ slider.maxValue = 100
+ slider.doubleValue = 50
+ slider.numberOfTickMarks = 20
+ slider.allowsTickMarkValuesOnly = true
+ slider.tickMarkPosition = .below
+ slider.isContinuous = true
+ return slider
+ }
+
+ private func configureControls() {
+ rulerColorWell.isContinuous = true
+ rulerColorWell.supportsAlpha = false
+ rulerColorWell.identifier = NSUserInterfaceItemIdentifier("ruler-settings-color-well")
+ rulerColorWell.setAccessibilityIdentifier("ruler-settings-color-well")
+ rulerColorWell.target = self
+ rulerColorWell.action = #selector(setRulerColor(_:))
+
+ resetRulerColorButton.target = self
+ resetRulerColorButton.action = #selector(resetRulerColor(_:))
+ configureResetRulerColorButtonAppearance(
+ resetRulerColorButton,
+ identifier: "reset-ruler-settings-color-button"
+ )
+
+ foregroundOpacitySlider.target = self
+ foregroundOpacitySlider.action = #selector(setForegroundOpacity(_:))
+ foregroundOpacitySlider.identifier = NSUserInterfaceItemIdentifier("ruler-settings-foreground-opacity-slider")
+ foregroundOpacitySlider.setAccessibilityIdentifier("ruler-settings-foreground-opacity-slider")
+ foregroundOpacityLabel.identifier = NSUserInterfaceItemIdentifier("ruler-settings-foreground-opacity-label")
+ foregroundOpacityLabel.setAccessibilityIdentifier("ruler-settings-foreground-opacity-label")
+
+ backgroundOpacitySlider.target = self
+ backgroundOpacitySlider.action = #selector(setBackgroundOpacity(_:))
+ backgroundOpacitySlider.identifier = NSUserInterfaceItemIdentifier("ruler-settings-background-opacity-slider")
+ backgroundOpacitySlider.setAccessibilityIdentifier("ruler-settings-background-opacity-slider")
+ backgroundOpacityLabel.identifier = NSUserInterfaceItemIdentifier("ruler-settings-background-opacity-label")
+ backgroundOpacityLabel.setAccessibilityIdentifier("ruler-settings-background-opacity-label")
+
+ floatRulersCheckbox.target = self
+ floatRulersCheckbox.action = #selector(setFloatRulers(_:))
+ floatRulersCheckbox.identifier = NSUserInterfaceItemIdentifier("ruler-settings-float-rulers-checkbox")
+ floatRulersCheckbox.setAccessibilityIdentifier("ruler-settings-float-rulers-checkbox")
+
+ rulerShadowCheckbox.target = self
+ rulerShadowCheckbox.action = #selector(setRulerShadow(_:))
+ rulerShadowCheckbox.identifier = NSUserInterfaceItemIdentifier("ruler-settings-ruler-shadow-checkbox")
+ rulerShadowCheckbox.setAccessibilityIdentifier("ruler-settings-ruler-shadow-checkbox")
+
+ closeButton.target = self
+ closeButton.action = #selector(closeRulerSettings(_:))
+ closeButton.identifier = NSUserInterfaceItemIdentifier("ruler-settings-close-button")
+ closeButton.setAccessibilityIdentifier("ruler-settings-close-button")
+ }
+
+ private func applyRulerColor(_ color: NSColor) {
+ applySettings { settings in
+ settings.setRulerColor(color)
+ }
+ updateView()
+ }
+
+ private func applySettings(_ update: (inout RulerSettings) -> Void) {
+ rulerController?.updateSettings(update)
+ }
+
+ private func subscribeToColorPanel() {
+ colorPanelObserver = NotificationCenter.default.addObserver(
+ forName: NSColorPanel.colorDidChangeNotification,
+ object: NSColorPanel.shared,
+ queue: .main
+ ) { [weak self] notification in
+ self?.updateRulerColorFromColorPanel(notification)
+ }
+ }
+
+ private func updateRulerColorFromColorPanel(_ notification: Notification) {
+ guard window?.isVisible == true,
+ let colorPanel = notification.object as? NSColorPanel,
+ colorPanel.isVisible,
+ activeRulerColorWell === rulerColorWell else { return }
+
+ applyRulerColor(colorPanel.color)
+ }
+
+ private func position(_ settingsWindow: NSWindow, attachedTo controller: GroupedRulerController) {
+ let parentFrame = controller.groupedWindow.frame
+ let settingsSize = settingsWindow.frame.size
+ let margin: CGFloat = 12
+ let frame: NSRect
+
+ if controller.state.visibility.showsHorizontal {
+ let horizontalFrame = controller.groupedWindow.screenFrame(for: .horizontal)
+ let x = clamp(
+ horizontalFrame.midX - settingsSize.width / 2,
+ lower: parentFrame.minX + margin,
+ upper: parentFrame.maxX - settingsSize.width - margin
+ )
+ let belowY = horizontalFrame.minY - settingsSize.height - margin
+ let aboveY = horizontalFrame.maxY + margin
+ let y = belowY >= parentFrame.minY + margin
+ ? belowY
+ : min(aboveY, parentFrame.maxY - settingsSize.height - margin)
+ frame = NSRect(origin: NSPoint(x: x, y: y), size: settingsSize)
+ } else {
+ let verticalFrame = controller.groupedWindow.screenFrame(for: .vertical)
+ let y = clamp(
+ verticalFrame.midY - settingsSize.height / 2,
+ lower: parentFrame.minY + margin,
+ upper: parentFrame.maxY - settingsSize.height - margin
+ )
+ let rightX = verticalFrame.maxX + margin
+ let leftX = verticalFrame.minX - settingsSize.width - margin
+ let x = rightX + settingsSize.width <= parentFrame.maxX - margin
+ ? rightX
+ : max(leftX, parentFrame.minX + margin)
+ frame = NSRect(origin: NSPoint(x: x, y: y), size: settingsSize)
+ }
+
+ settingsWindow.setFrame(frame, display: true)
+ }
+
+ private func clamp(_ value: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat {
+ guard lower <= upper else { return lower }
+
+ return min(max(value, lower), upper)
+ }
+
+ private func detachWindowIfNeeded() {
+ guard let settingsWindow = window else { return }
+
+ if let sheetParent = settingsWindow.sheetParent {
+ sheetParent.endSheet(settingsWindow)
+ }
+ if let parentWindow = settingsWindow.parent {
+ parentWindow.removeChildWindow(settingsWindow)
+ }
+ }
+
+ private func closeSheetColorControls() {
+ if let foregroundOpacity = rulerController?.state.settings.foregroundOpacity {
+ rulerController?.opacity = foregroundOpacity
+ }
+ rulerColorWell.deactivate()
+ closeRulerColorPanel()
+ }
+}
+
func closeRulerColorPanel() {
+ activeRulerColorWell = nil
let colorPanel = NSColorPanel.shared
colorPanel.animationBehavior = .none
colorPanel.setTarget(nil)
diff --git a/Free Ruler/RuleView.swift b/Free Ruler/RuleView.swift
index ea1b1ee..0651ab3 100644
--- a/Free Ruler/RuleView.swift
+++ b/Free Ruler/RuleView.swift
@@ -506,6 +506,13 @@ class RuleView: NSView {
}
}
+ var settingsOverride: RulerSettings? {
+ didSet {
+ color = RulerColors(customFill: settingsOverride?.rulerColor)
+ redrawForPreferenceChange()
+ }
+ }
+
var screen: NSScreen? {
guard let window = window else {
return nil
@@ -514,11 +521,11 @@ class RuleView: NSView {
}
var unit: Unit {
- prefs.unit
+ return settingsOverride?.unit ?? prefs.unit
}
var zeroCorner: ZeroCorner {
- prefs.zeroCorner
+ return settingsOverride?.zeroCorner ?? prefs.zeroCorner
}
var resizeHandleExclusionFrame: NSRect? {
diff --git a/Free Ruler/Ruler.swift b/Free Ruler/Ruler.swift
index 82b8f4d..914ad86 100644
--- a/Free Ruler/Ruler.swift
+++ b/Free Ruler/Ruler.swift
@@ -157,6 +157,10 @@ struct RulerSettings: Equatable, Codable {
try container.encode(zeroCorner.rawValue, forKey: .zeroCorner)
}
+ mutating func setRulerColor(_ color: NSColor) {
+ rulerColor = RulerSettings.normalizedColor(color)
+ }
+
private static func normalizedColor(_ color: NSColor) -> NSColor {
guard let rgbColor = color.usingColorSpace(.deviceRGB) else {
return Prefs.defaultRulerFillColor
diff --git a/Free Ruler/de.lproj/MainMenu.strings b/Free Ruler/de.lproj/MainMenu.strings
index 397ec7e..2420849 100644
--- a/Free Ruler/de.lproj/MainMenu.strings
+++ b/Free Ruler/de.lproj/MainMenu.strings
@@ -151,3 +151,6 @@
/* Class = "NSMenuItem"; title = "Cycle Units"; ObjectID = "2nm-aL-kZd"; */
"2nm-aL-kZd.title" = "Nächste Einheit auswählen";
+
+/* Class = "NSMenuItem"; title = "Ruler Settings…"; ObjectID = "rSt-Tg-232"; */
+"rSt-Tg-232.title" = "Lineal-Einstellungen…";
diff --git a/Free Ruler/es.lproj/MainMenu.strings b/Free Ruler/es.lproj/MainMenu.strings
index 1be4959..dd331e8 100644
--- a/Free Ruler/es.lproj/MainMenu.strings
+++ b/Free Ruler/es.lproj/MainMenu.strings
@@ -148,3 +148,6 @@
/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */
"x3v-GG-iWU.title" = "Copiar";
+
+/* Class = "NSMenuItem"; title = "Ruler Settings…"; ObjectID = "rSt-Tg-232"; */
+"rSt-Tg-232.title" = "Ajustes de regla…";
diff --git a/Free Ruler/fi.lproj/MainMenu.strings b/Free Ruler/fi.lproj/MainMenu.strings
index d81c26d..d88d824 100644
--- a/Free Ruler/fi.lproj/MainMenu.strings
+++ b/Free Ruler/fi.lproj/MainMenu.strings
@@ -151,3 +151,6 @@
/* Class = "NSMenuItem"; title = "Cycle Units"; ObjectID = "2nm-aL-kZd"; */
"2nm-aL-kZd.title" = "Vaihda yksikköä";
+
+/* Class = "NSMenuItem"; title = "Ruler Settings…"; ObjectID = "rSt-Tg-232"; */
+"rSt-Tg-232.title" = "Viivaimen asetukset…";
diff --git a/Free Ruler/ja.lproj/MainMenu.strings b/Free Ruler/ja.lproj/MainMenu.strings
index 01e477a..f851bc9 100644
--- a/Free Ruler/ja.lproj/MainMenu.strings
+++ b/Free Ruler/ja.lproj/MainMenu.strings
@@ -148,3 +148,6 @@
/* Class = "NSMenu"; title = "Unit"; ObjectID = "z2p-dA-zcS"; */
"z2p-dA-zcS.title" = "単位";
+
+/* Class = "NSMenuItem"; title = "Ruler Settings…"; ObjectID = "rSt-Tg-232"; */
+"rSt-Tg-232.title" = "定規設定…";
diff --git a/Free Ruler/zh-hans.lproj/MainMenu.strings b/Free Ruler/zh-hans.lproj/MainMenu.strings
index 3b14cd0..9b679a4 100644
--- a/Free Ruler/zh-hans.lproj/MainMenu.strings
+++ b/Free Ruler/zh-hans.lproj/MainMenu.strings
@@ -151,3 +151,6 @@
/* Class = "NSMenuItem"; title = "Cycle Units"; ObjectID = "2nm-aL-kZd"; */
"2nm-aL-kZd.title" = "循环单位";
+
+/* Class = "NSMenuItem"; title = "Ruler Settings…"; ObjectID = "rSt-Tg-232"; */
+"rSt-Tg-232.title" = "尺子设置…";
diff --git a/FreeRulerTests/RulerCoreTests.swift b/FreeRulerTests/RulerCoreTests.swift
index afcc7bb..a1af795 100644
--- a/FreeRulerTests/RulerCoreTests.swift
+++ b/FreeRulerTests/RulerCoreTests.swift
@@ -210,6 +210,307 @@ final class RulerCoreTests: XCTestCase {
XCTAssertTrue(manager.controllers[1].groupedWindow.isRuleVisible(.vertical))
}
+ func testManagedGroupedRulerAppliesPerRulerSettingsToWindowAndRules() {
+ let color = NSColor(deviceRed: 0.1, green: 0.4, blue: 0.8, alpha: 1)
+ let settings = RulerSettings(
+ unit: .inches,
+ rulerColor: color,
+ foregroundOpacity: 73,
+ backgroundOpacity: 31,
+ floatRulers: false,
+ rulerShadow: true,
+ zeroCorner: .bottomRight
+ )
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: settings,
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 200, y: 300),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ defer {
+ controller.hide()
+ }
+
+ XCTAssertEqual(controller.groupedWindow.horizontalRule.unit, .inches)
+ XCTAssertEqual(controller.groupedWindow.verticalRule.unit, .inches)
+ XCTAssertEqual(controller.groupedWindow.horizontalRule.zeroCorner, .bottomRight)
+ XCTAssertEqual(controller.groupedWindow.verticalRule.zeroCorner, .bottomRight)
+ assertColor(controller.groupedWindow.horizontalRule.color.fill, equals: color)
+ assertColor(controller.groupedWindow.verticalRule.color.fill, equals: color)
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.73, accuracy: 0.0001)
+ XCTAssertFalse(controller.groupedWindow.isFloatingPanel)
+ XCTAssertTrue(controller.groupedWindow.hasShadow)
+
+ controller.background()
+
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.31, accuracy: 0.0001)
+ }
+
+ func testManagedGroupedRulerIgnoresDefaultPreferenceChanges() {
+ withRestoredRulerPreferences {
+ let color = NSColor(deviceRed: 0.2, green: 0.3, blue: 0.7, alpha: 1)
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: RulerSettings(
+ unit: .inches,
+ rulerColor: color,
+ foregroundOpacity: 64,
+ backgroundOpacity: 28,
+ floatRulers: false,
+ rulerShadow: false,
+ zeroCorner: .bottomLeft
+ ),
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 240, y: 320),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ defer {
+ controller.hide()
+ }
+
+ prefs.unit = .millimeters
+ prefs.rulerColor = NSColor(deviceRed: 0.9, green: 0.1, blue: 0.2, alpha: 1)
+ prefs.foregroundOpacity = 12
+ prefs.backgroundOpacity = 9
+ prefs.floatRulers = true
+ prefs.rulerShadow = true
+ prefs.zeroCorner = .topRight
+
+ XCTAssertEqual(controller.state.settings.unit, .inches)
+ XCTAssertEqual(controller.groupedWindow.horizontalRule.unit, .inches)
+ XCTAssertEqual(controller.groupedWindow.horizontalRule.zeroCorner, .bottomLeft)
+ assertColor(controller.groupedWindow.horizontalRule.color.fill, equals: color)
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.64, accuracy: 0.0001)
+ XCTAssertFalse(controller.groupedWindow.isFloatingPanel)
+ XCTAssertFalse(controller.groupedWindow.hasShadow)
+ }
+ }
+
+ func testRulerSettingsControllerUpdatesRulerSettingsWithoutChangingDefaults() {
+ withRestoredRulerPreferences {
+ let defaultColor = NSColor(deviceRed: 0.15, green: 0.25, blue: 0.35, alpha: 1)
+ prefs.rulerColor = defaultColor
+ prefs.foregroundOpacity = 90
+ prefs.backgroundOpacity = 50
+ prefs.floatRulers = true
+ prefs.rulerShadow = false
+
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: RulerSettings(
+ rulerColor: NSColor(deviceRed: 0.4, green: 0.5, blue: 0.6, alpha: 1),
+ foregroundOpacity: 80,
+ backgroundOpacity: 45,
+ floatRulers: false,
+ rulerShadow: false
+ ),
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 240, y: 320),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ let settingsController = RulerSettingsController(rulerController: controller)
+ defer {
+ settingsController.close()
+ controller.hide()
+ }
+
+ settingsController.rulerColorWell.color = NSColor(
+ deviceRed: 0.8,
+ green: 0.2,
+ blue: 0.4,
+ alpha: 0.35
+ )
+ settingsController.setRulerColor(settingsController.rulerColorWell)
+
+ let normalizedColor = NSColor(deviceRed: 0.8, green: 0.2, blue: 0.4, alpha: 1)
+ assertColor(controller.state.settings.rulerColor, equals: normalizedColor)
+ assertColor(controller.groupedWindow.horizontalRule.color.fill, equals: normalizedColor)
+ assertColor(controller.groupedWindow.verticalRule.color.fill, equals: normalizedColor)
+ assertColor(settingsController.rulerColorWell.color, equals: normalizedColor)
+ assertColor(prefs.rulerColor, equals: defaultColor)
+ XCTAssertFalse(settingsController.resetRulerColorButton.isHidden)
+
+ settingsController.foregroundOpacitySlider.integerValue = 65
+ settingsController.setForegroundOpacity(settingsController.foregroundOpacitySlider)
+
+ XCTAssertEqual(controller.state.settings.foregroundOpacity, 65)
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.65, accuracy: 0.0001)
+ XCTAssertEqual(settingsController.foregroundOpacityLabel.stringValue, "65%")
+ XCTAssertEqual(prefs.foregroundOpacity, 90)
+
+ settingsController.backgroundOpacitySlider.integerValue = 35
+ settingsController.setBackgroundOpacity(settingsController.backgroundOpacitySlider)
+
+ XCTAssertEqual(controller.state.settings.backgroundOpacity, 35)
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.35, accuracy: 0.0001)
+ XCTAssertEqual(settingsController.backgroundOpacityLabel.stringValue, "35%")
+ XCTAssertEqual(prefs.backgroundOpacity, 50)
+
+ settingsController.floatRulersCheckbox.state = .on
+ settingsController.setFloatRulers(settingsController.floatRulersCheckbox)
+
+ XCTAssertTrue(controller.state.settings.floatRulers)
+ XCTAssertTrue(controller.groupedWindow.isFloatingPanel)
+ XCTAssertTrue(settingsController.floatRulersCheckbox.state == .on)
+ XCTAssertTrue(prefs.floatRulers)
+
+ settingsController.rulerShadowCheckbox.state = .on
+ settingsController.setRulerShadow(settingsController.rulerShadowCheckbox)
+
+ XCTAssertTrue(controller.state.settings.rulerShadow)
+ XCTAssertTrue(controller.groupedWindow.hasShadow)
+ XCTAssertTrue(settingsController.rulerShadowCheckbox.state == .on)
+ XCTAssertFalse(prefs.rulerShadow)
+
+ settingsController.resetRulerColor(settingsController.resetRulerColorButton)
+
+ assertColor(controller.state.settings.rulerColor, equals: Prefs.defaultRulerFillColor)
+ assertColor(controller.groupedWindow.horizontalRule.color.fill, equals: Prefs.defaultRulerFillColor)
+ assertColor(prefs.rulerColor, equals: defaultColor)
+ XCTAssertTrue(settingsController.resetRulerColorButton.isHidden)
+ }
+ }
+
+ func testRulerSettingsControllerPresentsAsAttachedSheetOnRulerWindow() {
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: RulerSettings(),
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 240, y: 320),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ let settingsController = RulerSettingsController(rulerController: controller)
+ defer {
+ settingsController.close()
+ controller.hide()
+ }
+
+ controller.show()
+ settingsController.show(attachedTo: controller, sender: self)
+
+ guard let settingsWindow = settingsController.window else {
+ XCTFail("Expected settings window")
+ return
+ }
+ XCTAssertTrue(controller.groupedWindow.childWindows?.contains(settingsWindow) ?? false)
+ XCTAssertNil(settingsWindow.sheetParent)
+ }
+
+ func testRulerSettingsControllerRestoresForegroundOpacityWhenClosingSheet() {
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: RulerSettings(
+ foregroundOpacity: 80,
+ backgroundOpacity: 45
+ ),
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 240, y: 320),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ let settingsController = RulerSettingsController(rulerController: controller)
+ defer {
+ settingsController.close()
+ controller.hide()
+ }
+
+ controller.show()
+ settingsController.show(attachedTo: controller, sender: self)
+ settingsController.backgroundOpacitySlider.integerValue = 35
+ settingsController.setBackgroundOpacity(settingsController.backgroundOpacitySlider)
+
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.35, accuracy: 0.0001)
+
+ settingsController.close()
+
+ XCTAssertEqual(controller.groupedWindow.alphaValue, 0.8, accuracy: 0.0001)
+ }
+
+ func testRulerSettingsControllerCloseButtonClosesAttachedSheet() {
+ let controller = GroupedRulerController(
+ state: RulerInstanceState(
+ settings: RulerSettings(),
+ layout: RulerLayoutState(
+ zeroPoint: NSPoint(x: 240, y: 320),
+ horizontalLength: 260,
+ verticalLength: 180
+ )
+ )
+ )
+ let settingsController = RulerSettingsController(rulerController: controller)
+ defer {
+ settingsController.close()
+ controller.hide()
+ }
+
+ controller.show()
+ settingsController.show(attachedTo: controller, sender: self)
+ guard let settingsWindow = settingsController.window else {
+ XCTFail("Expected settings window")
+ return
+ }
+
+ settingsController.closeButton.performClick(self)
+
+ XCTAssertFalse(controller.groupedWindow.childWindows?.contains(settingsWindow) ?? false)
+ XCTAssertFalse(settingsWindow.isVisible)
+ }
+
+ func testRulerManagerCopiesUpdatedDefaultsOnlyForNewRulers() {
+ withRestoredRulerPreferences {
+ prefs.unit = .pixels
+ prefs.rulerColor = NSColor(deviceRed: 0.1, green: 0.2, blue: 0.3, alpha: 1)
+ prefs.zeroCorner = .topLeft
+ let manager = RulerManager()
+ defer {
+ for controller in manager.controllers {
+ controller.hide()
+ }
+ }
+
+ let existing = manager.createRuler(
+ screenFrame: NSRect(x: 0, y: 0, width: 1000, height: 800)
+ )
+
+ prefs.unit = .millimeters
+ prefs.rulerColor = NSColor(deviceRed: 0.8, green: 0.7, blue: 0.2, alpha: 1)
+ prefs.zeroCorner = .topRight
+ let createdAfterDefaultsChange = manager.createRuler(
+ screenFrame: NSRect(x: 0, y: 0, width: 1000, height: 800)
+ )
+
+ XCTAssertEqual(existing.state.settings.unit, .pixels)
+ XCTAssertEqual(existing.groupedWindow.horizontalRule.unit, .pixels)
+ XCTAssertEqual(existing.groupedWindow.horizontalRule.zeroCorner, .topLeft)
+ assertColor(
+ existing.groupedWindow.horizontalRule.color.fill,
+ equals: NSColor(deviceRed: 0.1, green: 0.2, blue: 0.3, alpha: 1)
+ )
+ XCTAssertEqual(createdAfterDefaultsChange.state.settings.unit, .millimeters)
+ XCTAssertEqual(createdAfterDefaultsChange.groupedWindow.horizontalRule.unit, .millimeters)
+ XCTAssertEqual(createdAfterDefaultsChange.groupedWindow.horizontalRule.zeroCorner, .topRight)
+ assertColor(
+ createdAfterDefaultsChange.groupedWindow.horizontalRule.color.fill,
+ equals: NSColor(deviceRed: 0.8, green: 0.7, blue: 0.2, alpha: 1)
+ )
+ }
+ }
+
func testZeroCornerRawValuesPreservePersistedOrder() {
XCTAssertEqual(ZeroCorner.topLeft.rawValue, 0)
XCTAssertEqual(ZeroCorner.topRight.rawValue, 1)