diff --git a/Free Ruler/AppDelegate.swift b/Free Ruler/AppDelegate.swift index 1555a9b..41c91ee 100644 --- a/Free Ruler/AppDelegate.swift +++ b/Free Ruler/AppDelegate.swift @@ -559,7 +559,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Application Quit + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + closeRulerColorPanel() + return .terminateNow + } + func applicationWillTerminate(_ aNotification: Notification) { + closeRulerColorPanel() prefs.save() } diff --git a/Free Ruler/AppStoreScreenshotPreview.swift b/Free Ruler/AppStoreScreenshotPreview.swift index a0834ff..0dae12d 100644 --- a/Free Ruler/AppStoreScreenshotPreview.swift +++ b/Free Ruler/AppStoreScreenshotPreview.swift @@ -258,11 +258,11 @@ private enum AppStoreScreenshotLayout { static let screen2RulerLength: CGFloat = 2200 static let screen3BackgroundColor = #colorLiteral(red: 0.875857736, green: 0.8972384907, blue: 0.94, alpha: 1) - static let screen3RulerScale: CGFloat = 10.28 static let screen3RulerOpacity: CGFloat = 1 static let screen3RulerCount = 7 - static let screen3FirstRulerX: CGFloat = 0 - static let screen3RulerGap: CGFloat = 0 + static let screen3RulerGap: CGFloat = 30 + static let screen3FirstRulerStart: CGFloat = screen3RulerGap + static let screen3LastRulerEnd: CGFloat = canvasWidth - screen3RulerGap static let screen3RulerArcTop: CGFloat = 450 static let screen3RulerArcBottom: CGFloat = 1000 static let screen3RulerCurvature: CGFloat = 1.5 @@ -421,14 +421,22 @@ private enum AppStoreScreenshotLayout { ) } + static var screen3RulerScale: CGFloat { + screen3ScaledRulerThickness / Ruler.thickness + } + + private static var screen3AvailableRulerWidth: CGFloat { + screen3LastRulerEnd - screen3FirstRulerStart - (CGFloat(screen3RulerCount - 1) * screen3RulerGap) + } + static var screen3ScaledRulerThickness: CGFloat { - Ruler.thickness * screen3RulerScale + screen3AvailableRulerWidth / CGFloat(screen3RulerCount) } static func screen3RulerRect(index: Int) -> NSRect { let top = screen3RulerTopY(index: index) return NSRect( - x: screen3FirstRulerX + CGFloat(index) * (screen3ScaledRulerThickness + screen3RulerGap), + x: screen3FirstRulerStart + CGFloat(index) * (screen3ScaledRulerThickness + screen3RulerGap), y: top, width: screen3ScaledRulerThickness, height: screen3RulerOffscreenBottom - top @@ -873,6 +881,7 @@ private final class AppStoreScreenshotScenarioNSView: NSView { snapshotWindow.contentView = view snapshotWindow.orderFrontRegardless() snapshotWindow.makeKeyAndOrderFront(nil) + snapshotWindow.makeFirstResponder(nil) defer { snapshotWindow.orderOut(nil) snapshotWindow.contentView = nil diff --git a/Free Ruler/PreferencesController.swift b/Free Ruler/PreferencesController.swift index 7649831..be2db7e 100644 --- a/Free Ruler/PreferencesController.swift +++ b/Free Ruler/PreferencesController.swift @@ -1,22 +1,38 @@ import Cocoa -import ObjectiveC.runtime + +private let colorPanelOpaqueConfigurationRetryDelays: [TimeInterval] = [0.1, 0.3] +private let rulerColorPanelIdentifier = NSUserInterfaceItemIdentifier("ruler-color-panel") +private let rulerColorPanelOpaqueAccessibilityValue = "ruler-color-panel-alpha-hidden" func configureOpaqueColorPicking() { + let colorPanel = NSColorPanel.shared + colorPanel.identifier = rulerColorPanelIdentifier + colorPanel.setAccessibilityIdentifier(rulerColorPanelIdentifier.rawValue) + if UI_TESTS { + colorPanel.setAccessibilityValue(rulerColorPanelOpaqueAccessibilityValue) + } setColorPickingIgnoresAlpha(true) - NSColorPanel.shared.showsAlpha = false - NSColorPanel.shared.isContinuous = true - NSColorPanel.shared.animationBehavior = .none + colorPanel.showsAlpha = false + colorPanel.isContinuous = true + colorPanel.animationBehavior = .none + colorPanel.isRestorable = false } -private func setColorPickingIgnoresAlpha(_ ignoresAlpha: Bool) { - typealias Setter = @convention(c) (AnyClass, Selector, Bool) -> Void - let selector = Selector(("setIgnoresAlpha:")) +private func configureOpaqueColorPickingAfterPanelUpdates() { + configureOpaqueColorPicking() - guard let method = class_getClassMethod(NSColor.self, selector) else { return } + // The shared color panel can rebuild picker controls shortly after opening; reapply during + // that churn so alpha controls stay hidden without doing work for every color change. + for delay in colorPanelOpaqueConfigurationRetryDelays { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + configureOpaqueColorPicking() + } + } +} +private func setColorPickingIgnoresAlpha(_ ignoresAlpha: Bool) { // AppKit still consults this deprecated global switch when deciding whether color wells support alpha. - let setter = unsafeBitCast(method_getImplementation(method), to: Setter.self) - setter(NSColor.self, selector, ignoresAlpha) + NSColor.ignoresAlpha = ignoresAlpha } class RulerColorWell: NSColorWell { @@ -45,8 +61,9 @@ class RulerColorWell: NSColorWell { colorPanel.color = color colorPanel.setTarget(self) colorPanel.setAction(#selector(takeColorFrom(_:))) - NSApp.orderFrontColorPanel(self) + colorPanel.orderFront(self) configureForOpaqueColors() + configureOpaqueColorPickingAfterPanelUpdates() } override func draw(_ dirtyRect: NSRect) { @@ -136,6 +153,7 @@ class PreferencesController: NSWindowController, NSWindowDelegate, NotificationP } func windowWillClose(_ notification: Notification) { + rulerColorWell.deactivate() closeRulerColorPanel() // send closed notification @@ -263,16 +281,15 @@ class PreferencesController: NSWindowController, NSWindowDelegate, NotificationP let colorPanel = notification.object as? NSColorPanel, colorPanel.isVisible else { return } - configureOpaqueColorPicking() prefs.rulerColor = colorPanel.color } } -private func closeRulerColorPanel() { +func closeRulerColorPanel() { let colorPanel = NSColorPanel.shared colorPanel.animationBehavior = .none colorPanel.setTarget(nil) colorPanel.setAction(nil) - colorPanel.orderOut(nil) + colorPanel.close() } diff --git a/FreeRulerUITests/FreeRulerUITests.swift b/FreeRulerUITests/FreeRulerUITests.swift index f2d8378..7420e8f 100644 --- a/FreeRulerUITests/FreeRulerUITests.swift +++ b/FreeRulerUITests/FreeRulerUITests.swift @@ -3,6 +3,8 @@ import Darwin final class FreeRulerUITests: XCTestCase { + private let opaqueColorPanelValue = "ruler-color-panel-alpha-hidden" + private var app: XCUIApplication! private var cursorStateURL: URL! @@ -103,6 +105,45 @@ final class FreeRulerUITests: XCTestCase { XCTAssertTrue(preferences.waitForNonExistence(timeout: 2)) } + func testRulerColorPanelHidesOpacityControl() { + openRulerColorPanel() + XCTAssertTrue( + colorPanel.waitForVisibleFrame(timeout: 1), + "The ruler color panel should be visible." + ) + XCTAssertEqual( + colorPanel.value as? String, + opaqueColorPanelValue, + "The ruler color panel should be configured for opaque color picking." + ) + XCTAssertEqual( + visibleSliderCount(in: colorPanel), + 1, + "The ruler color panel should show the color slider, but not an opacity slider." + ) + } + + func testClosingPreferencesClosesRulerColorPanel() { + openRulerColorPanel() + + preferencesWindow.click() + app.typeKey("w", modifierFlags: .command) + + XCTAssertTrue(preferencesWindow.waitForNonExistence(timeout: 2)) + XCTAssertTrue(colorPanel.waitForNonExistence(timeout: 2)) + } + + func testRulerColorPanelDoesNotReopenAfterRelaunch() { + openRulerColorPanel() + + app.terminate() + app.launch() + app.activate() + + XCTAssertTrue(horizontalRuler.waitForExistence(timeout: 3)) + XCTAssertTrue(colorPanel.waitForNonExistence(timeout: 2)) + } + func testRulerCloseWithCommandW() { XCTAssertTrue(horizontalRuler.waitForExistence(timeout: 3)) XCTAssertTrue(verticalRuler.waitForExistence(timeout: 3)) @@ -298,6 +339,14 @@ final class FreeRulerUITests: XCTestCase { app.checkBoxes["ruler-shadow-checkbox"] } + private var rulerColorWell: XCUIElement { + app.colorWells["ruler-color-well"] + } + + private var colorPanel: XCUIElement { + app.windows["ruler-color-panel"] + } + private var hotkeyBezelLabel: XCUIElement { app.staticTexts["hotkey-bezel-label"] } @@ -321,6 +370,19 @@ final class FreeRulerUITests: XCTestCase { } } + private func openRulerColorPanel() { + openPreferences() + + if colorPanel.exists { + colorPanel.click() + app.typeKey("w", modifierFlags: .command) + XCTAssertTrue(colorPanel.waitForNonExistence(timeout: 2)) + } + + rulerColorWell.click() + XCTAssertTrue(colorPanel.waitForExistence(timeout: 3)) + } + private func setGroupRulers(_ enabled: Bool) { openPreferences() @@ -486,6 +548,10 @@ final class FreeRulerUITests: XCTestCase { return String(cString: passwd.pointee.pw_dir) } + + private func visibleSliderCount(in element: XCUIElement) -> Int { + return element.sliders.allElementsBoundByIndex.filter(\.hasVisibleFrame).count + } } private extension XCUIElement { @@ -495,6 +561,38 @@ private extension XCUIElement { return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed } + func waitForNoVisibleFrame(timeout: TimeInterval) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + + while Date() < deadline { + if !hasVisibleFrame { + return true + } + + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + } + + return !hasVisibleFrame + } + + func waitForVisibleFrame(timeout: TimeInterval) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + + while Date() < deadline { + if hasVisibleFrame { + return true + } + + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + } + + return hasVisibleFrame + } + + var hasVisibleFrame: Bool { + return exists && !frame.isEmpty && !frame.isNull + } + func waitForFrameChange(from originalFrame: CGRect, timeout: TimeInterval) -> Bool { let deadline = Date().addingTimeInterval(timeout) diff --git a/appstore/screenshots/01-measure-anything.png b/appstore/screenshots/01-measure-anything.png index e2852c7..48d9075 100644 Binary files a/appstore/screenshots/01-measure-anything.png and b/appstore/screenshots/01-measure-anything.png differ diff --git a/appstore/screenshots/02-custom-colors.png b/appstore/screenshots/02-custom-colors.png index 41f3b02..383a92f 100644 Binary files a/appstore/screenshots/02-custom-colors.png and b/appstore/screenshots/02-custom-colors.png differ diff --git a/appstore/screenshots/03-switch-units.png b/appstore/screenshots/03-switch-units.png index 917c5ad..828ede9 100644 Binary files a/appstore/screenshots/03-switch-units.png and b/appstore/screenshots/03-switch-units.png differ diff --git a/appstore/screenshots/04-customize-rulers.png b/appstore/screenshots/04-customize-rulers.png index 128abe1..bff478d 100644 Binary files a/appstore/screenshots/04-customize-rulers.png and b/appstore/screenshots/04-customize-rulers.png differ