diff --git a/Free Ruler.xcodeproj/project.pbxproj b/Free Ruler.xcodeproj/project.pbxproj index af159b6..08a89a4 100644 --- a/Free Ruler.xcodeproj/project.pbxproj +++ b/Free Ruler.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 50D7BEEB227D432E0008B95E /* RulerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEEA227D432E0008B95E /* RulerWindow.swift */; }; 50D7BEED227D5C810008B95E /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D7BEEC227D5C810008B95E /* RuleView.swift */; }; 50FC527E25BF326800B84228 /* FreeRuler.help in Resources */ = {isa = PBXBuildFile; fileRef = 50FC527D25BF326800B84228 /* FreeRuler.help */; }; + 62EA14412D47120F00DA3964 /* ScaleController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62EA14402D47120F00DA3964 /* ScaleController.xib */; }; + 62EA14432D4713C000DA3964 /* ScaleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EA14422D4713C000DA3964 /* ScaleController.swift */; }; 6F4102892260712F00F06A10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4102882260712F00F06A10 /* AppDelegate.swift */; }; 6F41028E2260713100F06A10 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6F41028C2260713100F06A10 /* MainMenu.xib */; }; 6F41029F22607DC900F06A10 /* HorizontalRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F41029E22607DC900F06A10 /* HorizontalRule.swift */; }; @@ -60,6 +62,8 @@ 50FC527D25BF326800B84228 /* FreeRuler.help */ = {isa = PBXFileReference; lastKnownFileType = folder; path = FreeRuler.help; sourceTree = ""; }; 53AF6A4225A456E30076AAB7 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/MainMenu.strings; sourceTree = ""; }; 53AF6A4325A456E30076AAB7 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/PreferencesController.strings; sourceTree = ""; }; + 62EA14402D47120F00DA3964 /* ScaleController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ScaleController.xib; sourceTree = ""; }; + 62EA14422D4713C000DA3964 /* ScaleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleController.swift; sourceTree = ""; }; 6F4102852260712F00F06A10 /* Free Ruler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Free Ruler.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 6F4102882260712F00F06A10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6F41028D2260713100F06A10 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -109,6 +113,8 @@ 507FED5F2280E13200BD77DC /* Prefs.swift */, 50008B6022846FCD001E3EE4 /* Notifications.swift */, 6F4102882260712F00F06A10 /* AppDelegate.swift */, + 62EA14402D47120F00DA3964 /* ScaleController.xib */, + 62EA14422D4713C000DA3964 /* ScaleController.swift */, 6F41028C2260713100F06A10 /* MainMenu.xib */, 50D7BEE8227D43270008B95E /* Ruler.swift */, 50D7BEE6227D42FD0008B95E /* RulerController.swift */, @@ -193,6 +199,7 @@ files = ( 50C6D891228BDBAD0091F19E /* Images.xcassets in Resources */, 50FC527E25BF326800B84228 /* FreeRuler.help in Resources */, + 62EA14412D47120F00DA3964 /* ScaleController.xib in Resources */, 507FED44227FFF5300BD77DC /* PreferencesController.xib in Resources */, 6F41028E2260713100F06A10 /* MainMenu.xib in Resources */, ); @@ -205,6 +212,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 62EA14432D4713C000DA3964 /* ScaleController.swift in Sources */, 50008B6122846FCD001E3EE4 /* Notifications.swift in Sources */, 507FED602280E13200BD77DC /* Prefs.swift in Sources */, 50CCB206227FCD26004645C5 /* AppIconLayout.swift in Sources */, diff --git a/Free Ruler/AppDelegate.swift b/Free Ruler/AppDelegate.swift index 5185d81..a8ed421 100644 --- a/Free Ruler/AppDelegate.swift +++ b/Free Ruler/AppDelegate.swift @@ -19,6 +19,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var pixelsMenuItem: NSMenuItem! @IBOutlet weak var millimetersMenuItem: NSMenuItem! @IBOutlet weak var inchesMenuItem: NSMenuItem! + @IBOutlet weak var scaledMenuItem: NSMenuItem! @IBOutlet weak var cycleUnitsMenuItem: NSMenuItem! @IBOutlet weak var floatRulersMenuItem: NSMenuItem! @@ -27,6 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var alignRulersMenuItem: NSMenuItem! var preferencesController: PreferencesController? = nil + var scaleController: ScaleController? = nil // MARK: - Lifecycle @@ -73,6 +75,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { pixelsMenuItem?.state = prefs.unit == .pixels ? .on : .off millimetersMenuItem?.state = prefs.unit == .millimeters ? .on : .off inchesMenuItem?.state = prefs.unit == .inches ? .on : .off + scaledMenuItem?.state = prefs.unit == .scaled ? .on : .off } func redrawRulers() { @@ -138,6 +141,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func setUnitInches(_ sender: Any) { prefs.unit = .inches } + @IBAction func setUnitScaled(_ sender: Any) { + prefs.unit = .scaled + } @IBAction func cycleUnits(_ sender: Any) { switch prefs.unit { case .pixels: @@ -145,6 +151,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { case .millimeters: prefs.unit = .inches case .inches: + prefs.unit = .scaled + case .scaled: prefs.unit = .pixels } } @@ -170,6 +178,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @IBAction func openScale(_ sender: Any) { + if scaleController == nil { + scaleController = ScaleController() + } + + if scaleController != nil { + scaleController?.xRulerWidth = rulers[1].rulerWindow.frame.width; + scaleController?.yRulerHeight = rulers[0].rulerWindow.frame.height; + scaleController?.showWindow(self) + } + } + @IBAction func alignRulersAtMouseLocation(_ sender: Any) { var mouseLoc = NSEvent.mouseLocation mouseLoc.x = mouseLoc.x.rounded() diff --git a/Free Ruler/Base.lproj/MainMenu.xib b/Free Ruler/Base.lproj/MainMenu.xib index 432c5fb..b3478fe 100644 --- a/Free Ruler/Base.lproj/MainMenu.xib +++ b/Free Ruler/Base.lproj/MainMenu.xib @@ -1,8 +1,7 @@ - + - - + @@ -22,6 +21,7 @@ + @@ -159,6 +159,12 @@ + + + + + + @@ -166,6 +172,12 @@ + + + + + + diff --git a/Free Ruler/HorizontalRule.swift b/Free Ruler/HorizontalRule.swift index 2fabeb9..75f2cc8 100644 --- a/Free Ruler/HorizontalRule.swift +++ b/Free Ruler/HorizontalRule.swift @@ -51,6 +51,13 @@ class HorizontalRule: RuleView { mediumTicks = 8 smallTicks = 4 tinyTicks = 1 + case .scaled: + tickScale = 1 / prefs.xscale + textScale = 1 + largeTicks = 10 + mediumTicks = 5 + smallTicks = 1 + tinyTicks = nil default: tickScale = 1 textScale = 1 @@ -150,7 +157,7 @@ class HorizontalRule: RuleView { NSAttributedString.Key.foregroundColor: color.mouseNumber, ] - let mouseNumber = self.getMouseNumberLabel(number) + let mouseNumber = self.getXMouseNumberLabel(number) let label = NSAttributedString(string: mouseNumber, attributes: attributes) let labelSize = label.size() @@ -188,5 +195,5 @@ class HorizontalRule: RuleView { context: nil ) } - + } diff --git a/Free Ruler/Prefs.swift b/Free Ruler/Prefs.swift index 6772dcd..01a408b 100644 --- a/Free Ruler/Prefs.swift +++ b/Free Ruler/Prefs.swift @@ -17,6 +17,7 @@ let prefs = Prefs.shared case pixels case millimeters case inches + case scaled } class Prefs: NSObject { @@ -31,6 +32,8 @@ class Prefs: NSObject { @objc dynamic var foregroundOpacity : Int @objc dynamic var backgroundOpacity : Int @objc dynamic var unit : Unit + @objc dynamic var xscale : CGFloat + @objc dynamic var yscale : CGFloat // MARK: - public save method func save() { @@ -47,7 +50,9 @@ class Prefs: NSObject { "rulerShadow": false, "foregroundOpacity": 90, "backgroundOpacity": 50, - "unit": Unit.pixels.rawValue + "unit": Unit.pixels.rawValue, + "xscale": 1.0, + "yscale": 1.0 ] private override init() { @@ -59,7 +64,9 @@ class Prefs: NSObject { foregroundOpacity = defaults.integer(forKey: "foregroundOpacity") backgroundOpacity = defaults.integer(forKey: "backgroundOpacity") unit = Unit(rawValue: defaults.integer(forKey: "unit")) ?? .pixels - + xscale = CGFloat(defaults.float(forKey: "xscale")) + yscale = CGFloat(defaults.float(forKey: "yscale")) + super.init() addObservers() @@ -87,6 +94,12 @@ class Prefs: NSObject { observe(\Prefs.unit, options: .new) { prefs, changed in self.defaults.set(prefs.unit.rawValue, forKey: "unit") }, + observe(\Prefs.xscale, options: .new) { prefs, changed in + self.defaults.set(prefs.xscale.description, forKey: "xscale") + }, + observe(\Prefs.yscale, options: .new) { prefs, changed in + self.defaults.set(prefs.yscale.description, forKey: "yscale") + }, ] } diff --git a/Free Ruler/RuleView.swift b/Free Ruler/RuleView.swift index deac093..8b84600 100644 --- a/Free Ruler/RuleView.swift +++ b/Free Ruler/RuleView.swift @@ -72,10 +72,12 @@ class RuleView: NSView { return "mm" case .inches: return "in" + case .scaled: + return "sc" } } - func getMouseNumberLabel(_ number: CGFloat) -> String { + func getXMouseNumberLabel(_ number: CGFloat) -> String { switch prefs.unit { case .pixels: return String(format: "%d", Int(number)) @@ -83,6 +85,20 @@ class RuleView: NSView { return String(format: "%.1f", number / (screen?.dpmm.width ?? NSScreen.defaultDpmm)) case .inches: return String(format: "%.3f", number / (screen?.dpi.width ?? NSScreen.defaultDpi)) + case .scaled: + return String(format: "%.3f", number * prefs.xscale ) + } + } + func getYMouseNumberLabel(_ number: CGFloat) -> String { + switch prefs.unit { + case .pixels: + return String(format: "%d", Int(number)) + case .millimeters: + return String(format: "%.1f", number / (screen?.dpmm.width ?? NSScreen.defaultDpmm)) + case .inches: + return String(format: "%.3f", number / (screen?.dpi.width ?? NSScreen.defaultDpi)) + case .scaled: + return String(format: "%.3f", number * prefs.yscale ) } } diff --git a/Free Ruler/ScaleController.swift b/Free Ruler/ScaleController.swift new file mode 100644 index 0000000..ab6b6ac --- /dev/null +++ b/Free Ruler/ScaleController.swift @@ -0,0 +1,121 @@ +// +// ScaleController.swift +// Free Ruler +// +// Created by Kevin Meziere on 1/26/25. +// Copyright © 2025 Free Ruler. All rights reserved. +// + +import Cocoa + +class ScaleController: NSWindowController, NSWindowDelegate, + NSTextFieldDelegate, NotificationPoster +{ + + @IBOutlet weak var xScaleTextField: NSTextField! + @IBOutlet weak var yScaleTextField: NSTextField! + @IBOutlet weak var lockedRatioButton: NSButton! + + var observers: [NSKeyValueObservation] = [] + + public var xRulerWidth: CGFloat = 0 + public var yRulerHeight: CGFloat = 0 + + var lockedAspectRatio: Bool = true + + override var windowNibName: String { + return "ScaleController" + } + + override func windowDidLoad() { + super.windowDidLoad() + window?.isMovableByWindowBackground = true + + lockedAspectRatio = prefs.xscale == prefs.yscale + } + + func windowDidBecomeKey(_ notification: Notification) { + lockedRatioButton.state = lockedAspectRatio ? .on : .off + xScaleTextField.stringValue = String( + format: "%.5f", xRulerWidth * prefs.xscale) + yScaleTextField.stringValue = String( + format: "%.5f", yRulerHeight * prefs.yscale) + + } + + override func showWindow(_ sender: Any?) { + window?.makeKeyAndOrderFront(sender) + window?.center() + } + + override func windowWillLoad() { + + } + + @IBAction func xValueChanged(_ sender: NSTextField) { + + } + + func controlTextDidChange(_ obj: Notification) { + + let textField = obj.object as! NSTextField + + // https://stackoverflow.com/a/52311371 + var stringValue = textField.stringValue + + let charSet = NSCharacterSet(charactersIn: "1234567890.").inverted + let chars = textField.stringValue.components(separatedBy: charSet) + stringValue = chars.joined() + + // Second step : only one '.' + let comma = NSCharacterSet(charactersIn: ".") + let chuncks = stringValue.components(separatedBy: comma as CharacterSet) + switch chuncks.count { + case 0: + stringValue = "" + case 1: + stringValue = "\(chuncks[0])" + default: + stringValue = "\(chuncks[0]).\(chuncks[1])" + } + + // replace string + textField.stringValue = stringValue + + if textField.identifier?.rawValue == "xscale" { + if lockedRatioButton.state == .on { + yScaleTextField.stringValue = String( + format: "%.5f", + (CGFloat(yRulerHeight * Double(stringValue)!) / xRulerWidth) + ) + } + } + + if textField.identifier?.rawValue == "yscale" { + if lockedRatioButton.state == .on { + xScaleTextField.stringValue = String( + format: "%.5f", + (CGFloat(xRulerWidth * Double(stringValue)!) / yRulerHeight) + ) + } + } + + } + + @IBAction func lockRatio(_ sender: Any) { + if lockedRatioButton.state == .on { + yScaleTextField.stringValue = String( + format: "%.5f", + (CGFloat(yRulerHeight * Double(xScaleTextField.stringValue)!) + / xRulerWidth)) + } + + } + + @IBAction func saveScale(_ sender: Any) { + prefs.xscale = Double(xScaleTextField.stringValue)! / xRulerWidth + prefs.yscale = Double(yScaleTextField.stringValue)! / yRulerHeight + self.window?.close() + } + +} diff --git a/Free Ruler/ScaleController.xib b/Free Ruler/ScaleController.xib new file mode 100644 index 0000000..a44a484 --- /dev/null +++ b/Free Ruler/ScaleController.xib @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Free Ruler/VerticalRule.swift b/Free Ruler/VerticalRule.swift index ec80284..1078272 100644 --- a/Free Ruler/VerticalRule.swift +++ b/Free Ruler/VerticalRule.swift @@ -52,6 +52,13 @@ class VerticalRule: RuleView { mediumTicks = 8 smallTicks = 4 tinyTicks = 1 + case .scaled: + tickScale = 1 / prefs.yscale + textScale = 1 + largeTicks = 10 + mediumTicks = 5 + smallTicks = 1 + tinyTicks = nil default: tickScale = 1 textScale = 1 @@ -151,7 +158,7 @@ class VerticalRule: RuleView { NSAttributedString.Key.foregroundColor: color.mouseNumber, ] - let mouseNumber = self.getMouseNumberLabel(number) + let mouseNumber = self.getYMouseNumberLabel(number) let label = NSAttributedString(string: mouseNumber, attributes: attributes) let labelSize = label.size()