Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ expect rough edges until 1.0.
## [Unreleased]

### Changed
- **The popover follows your Mac's light or dark mode.** It used to be dark all the time. On a
light Mac it is now light glass, with a darker green for text so it stays readable. Settings,
About and the other windows follow too, with the same glass or solid background. Selected chips now use dark text on the green
instead of white, which was hard to read. If you liked it dark, pick Dark in Settings ›
Appearance.

- **The popover is shorter: settings below DPI collapse.** Polling rate, Lighting, Power
Management, Buttons and Profiles are now compact cards of one row each, each showing
its current value ("1000 Hz", "Static · 80%", the active profile). Click a row to open its
Expand Down Expand Up @@ -39,6 +45,12 @@ expect rough edges until 1.0.
it reads the answers, using a fake mouse instead of hardware.

### Added
- **Settings › Appearance.** Choose the theme for the popover and the app's windows: System
(the default), Dark, Light or Razer Green, a dark look with a faint green tint. The popover
also gets a thin green edge. Turn off "Glass effect"
for a solid background instead of the see-through one. It's easier to read over busy windows.
Changes apply straight away, even with the popover open.

- **The Cobra HyperSpeed works over Bluetooth.** Battery, DPI, brightness and lighting
colour all work with the mouse in Bluetooth mode, so you no longer need the dongle or a
cable. The battery history carries over too, because the mouse reports the same serial on
Expand Down
10 changes: 5 additions & 5 deletions Sources/MacRazer/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct AboutView: View {
Text("by").font(.system(size: 12)).foregroundStyle(.secondary)
Link("SorcRR", destination: ProjectLinks.developer)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Color.razerGreen)
.foregroundStyle(Color.razerGreenText)
}
whatsNewRow
}
Expand Down Expand Up @@ -100,7 +100,7 @@ struct AboutView: View {
Link("PR #2583", destination: ProjectLinks.cobraHyperSpeedPR)
}
.font(.system(size: 11.5, weight: .medium))
.foregroundStyle(Color.razerGreen)
.foregroundStyle(Color.razerGreenText)
}
}

Expand All @@ -113,7 +113,7 @@ struct AboutView: View {
+ "but you run it at your own risk.")
Link("View the source", destination: ProjectLinks.repo)
.font(.system(size: 11.5, weight: .medium))
.foregroundStyle(Color.razerGreen)
.foregroundStyle(Color.razerGreenText)
}
}

Expand All @@ -131,7 +131,7 @@ struct AboutView: View {
Image(systemName: "chevron.right").font(.system(size: 8, weight: .semibold))
}
.font(.system(size: 11.5, weight: .medium))
.foregroundStyle(Color.razerGreen)
.foregroundStyle(Color.razerGreenText)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Expand Down Expand Up @@ -177,7 +177,7 @@ struct AboutView: View {
Label("Leave a Tip", systemImage: "heart")
}
.buttonStyle(.bordered)
.tint(.razerGreen)
.tint(.razerGreenText)
Spacer(minLength: 0)
Button("Done") { onDone() }
.keyboardShortcut(.defaultAction)
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/AboutWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ final class AboutWindowController: AppWindowPresenter {
w.title = "About MacRazer"
w.styleMask = [.titled, .closable]
w.isReleasedWhenClosed = false
// Match the popover and the other windows: the green accent needs a dark ground.
w.appearance = NSAppearance(named: .darkAqua)
// Themed like the popover per Settings › Appearance, kept up to date when it changes.
AppearanceSettings.shared.track(w)
w.center()
hosting = controller
window = w
Expand Down
43 changes: 38 additions & 5 deletions Sources/MacRazer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, UNU
model: DeviceTestModel(controller: controller, permissions: permissions))
private let updateChecker = UpdateChecker()
private let launchAtLogin = LaunchAtLogin()
private let appearance = AppearanceSettings.shared
/// The solid colour or green tint behind the popover; see `PopoverBackdropView`.
private let popoverBackdrop = PopoverBackdropView()
private lazy var aboutWindow = AboutWindowController()
/// Closing it re-asks the auto-install gate. This window opens unfocused after an
/// automatic install and can sit unnoticed for days, and while it is open it holds back
Expand Down Expand Up @@ -103,13 +106,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, UNU
popover.behavior = .transient
popover.delegate = self
popover.animates = false // open immediately, no slide animation
// Force a dark popover regardless of system appearance — the Razer-green accent and
// logo have poor contrast on the light-mode grey material; dark is also the gaming
// aesthetic and makes the green pop.
popover.appearance = NSAppearance(named: .darkAqua)
// Light or dark, glass or solid, per Settings › Appearance. Re-applied whenever the
// setting changes or macOS switches between light and dark, including while the
// popover is open. The popover's appearance is always set explicitly, never left nil:
// a nil one would follow the menu bar, which macOS tints from the wallpaper rather
// than the system's light or dark mode.
Publishers.CombineLatest3(appearance.$theme, appearance.$glass,
NSApp.publisher(for: \.effectiveAppearance))
.sink { [weak self] theme, glass, system in
self?.applyAppearance(theme: theme, glass: glass, systemIsDark: system.isDark)
}
.store(in: &cancellables)
let hosting = NSHostingController(rootView: PopoverView(
controller: controller, remapper: remapper, updateChecker: updateChecker,
launchAtLogin: launchAtLogin,
launchAtLogin: launchAtLogin, appearance: appearance,
onOpenSettings: { [weak self] in
// Close first: the popover is transient and would dismiss itself the moment
// the window takes focus, which looks like the click did two things.
Expand Down Expand Up @@ -367,6 +377,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, UNU
windowsBeforePopover = Set(NSApp.windows.filter(\.isVisible).map(ObjectIdentifier.init))
// Show first (instant), then kick off the refresh so the open never waits on IO.
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
// The frame view the backdrop lives in only exists once the popover is shown.
installPopoverBackdrop()
// Activate, not just make key. This app has no Dock icon, so clicking the menu bar
// item does not make it the active application, and a control in an inactive app's
// window does not respond to the first click the way it looks like it should. Every
Expand Down Expand Up @@ -510,6 +522,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, UNU
@objc private func openDeviceTest() { present(deviceTestWindow) }
@objc private func quit() { NSApplication.shared.terminate(nil) }

// MARK: - Appearance

private func applyAppearance(theme: PopoverTheme, glass: Bool, systemIsDark: Bool) {
let look = PopoverLook.resolve(theme: theme, glass: glass, systemIsDark: systemIsDark)
popover.appearance = NSAppearance(named: look.appearanceName)
popoverBackdrop.fill = look.backdropColor
appearance.applyToWindows(look)
}

/// Puts the backdrop into the popover's frame view, just under the content: above the
/// native material, so a solid colour hides it, and filling the arrow too. NSPopover
/// keeps its frame view between shows, so this only does work the first time.
private func installPopoverBackdrop() {
guard let content = popover.contentViewController?.view,
let frameView = content.superview,
popoverBackdrop.superview !== frameView else { return }
popoverBackdrop.frame = frameView.bounds
popoverBackdrop.autoresizingMask = [.width, .height]
frameView.addSubview(popoverBackdrop, positioned: .below, relativeTo: content)
}

// MARK: - NSPopoverDelegate

func popoverDidShow(_ notification: Notification) { controller.setPopoverVisible(true) }
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/ColorPickerPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct ColorPickerPage: View {
Text("Colour").font(.system(size: 16, weight: .semibold))
Spacer()
Circle().fill(color).frame(width: 22, height: 22)
.overlay(Circle().strokeBorder(.white.opacity(0.35), lineWidth: 1))
.overlay(Circle().strokeBorder(Color.primary.opacity(0.25), lineWidth: 1))
}
wheel
HStack(spacing: 8) {
Expand All @@ -76,7 +76,7 @@ struct ColorPickerPage: View {
gradient: Gradient(colors: [.white, .white.opacity(0)]),
center: .center, startRadius: 0, endRadius: r)))
.overlay(Circle().fill(.black.opacity(1 - bri)))
.overlay(Circle().strokeBorder(.white.opacity(0.15), lineWidth: 1))
.overlay(Circle().strokeBorder(Color.primary.opacity(0.15), lineWidth: 1))
.frame(width: side, height: side)
Circle().stroke(.white, lineWidth: 2)
.frame(width: 15, height: 15)
Expand Down
4 changes: 4 additions & 0 deletions Sources/MacRazer/CrashReportView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ struct CrashReportView: View {
set: { model.comment = String($0.prefix(DeviceReportOutput.maxComment)) }))
.font(.system(size: 12))
.frame(height: 54)
// The editor's own opaque background would sit as a flat box on a glass or tinted
// window; a faint fill like the cards keeps it readable on all of them.
.scrollContentBackground(.hidden)
.background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 6))
.overlay(alignment: .topLeading) {
if model.comment.isEmpty {
Text("What were you doing when it quit? (optional)")
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/CrashReportWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ final class CrashReportWindowController: NSObject, AppWindowPresenter, NSWindowD
w.title = "MacRazer Crash Report"
w.styleMask = [.titled, .closable]
w.isReleasedWhenClosed = false
// Match the other windows: the green accent needs a dark ground.
w.appearance = NSAppearance(named: .darkAqua)
// Themed like the popover per Settings › Appearance, kept up to date when it changes.
AppearanceSettings.shared.track(w)
w.delegate = self
w.center()
window = w
Expand Down
16 changes: 10 additions & 6 deletions Sources/MacRazer/DeviceTestView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct DeviceTestView: View {
private var intro: some View {
VStack(alignment: .leading, spacing: 12) {
if model.isKnownSupported {
tag("Fully supported", color: .razerGreen)
tag("Fully supported", color: .razerGreenText)
title("Your \(model.deviceName) is already supported")
note("The test checks it really works on your mouse and firmware. Reports like this keep the supported list honest, and catch it early if a firmware update breaks something.")
} else {
Expand Down Expand Up @@ -165,7 +165,7 @@ struct DeviceTestView: View {
VStack(spacing: 4) {
row("Yours", record.data?.original.map(String.init) ?? "No answer")
row("Test, read back", "\(record.data?.test.map(String.init) ?? "–"), \(record.data?.readBack.map(String.init) ?? "–")")
row("Put back", record.data?.restored == true ? "Yes" : "No", color: record.data?.restored == true ? .razerGreen : .batteryMid)
row("Put back", record.data?.restored == true ? "Yes" : "No", color: record.data?.restored == true ? .razerGreenText : .batteryMid)
if let stages = record.data?.stages { row("Stages", stages.map(String.init).joined(separator: " · ")) }
if let max = record.data?.maxProbe { row("Highest it kept", String(max)) }
}
Expand Down Expand Up @@ -195,7 +195,7 @@ struct DeviceTestView: View {
VStack(spacing: 4) {
row("Yours", record.data?.original.map { "\($0) Hz" } ?? "No answer")
row("Test, read back", "\(record.data?.test.map { "\($0) Hz" } ?? "–"), \(record.data?.readBack.map { "\($0) Hz" } ?? "–")")
row("Put back", record.data?.restored == true ? "Yes" : "No", color: record.data?.restored == true ? .razerGreen : .batteryMid)
row("Put back", record.data?.restored == true ? "Yes" : "No", color: record.data?.restored == true ? .razerGreenText : .batteryMid)
}
}
outcomeRow("Polling rate", record.outcome)
Expand Down Expand Up @@ -257,7 +257,7 @@ struct DeviceTestView: View {
card {
VStack(spacing: 4) {
row(report.device.name, String(format: "0x%04X", report.device.productID) + (report.identify.data?.firmware.map { ", \($0)" } ?? ""))
if !verdict.passed.isEmpty { row("Worked", verdict.passed.joined(separator: ", "), color: .razerGreen) }
if !verdict.passed.isEmpty { row("Worked", verdict.passed.joined(separator: ", "), color: .razerGreenText) }
if !verdict.failed.isEmpty { row("Didn't work", verdict.failed.joined(separator: ", "), color: .batteryMid) }
if !verdict.notSupported.isEmpty { row("Not on this mouse", verdict.notSupported.joined(separator: ", ")) }
if !verdict.skipped.isEmpty { row("Skipped", verdict.skipped.joined(separator: ", ")) }
Expand All @@ -284,6 +284,10 @@ struct DeviceTestView: View {
set: { model.comment = String($0.prefix(DeviceReportOutput.maxComment)) }))
.font(.system(size: 12))
.frame(height: 54)
// The editor's own opaque background would sit as a flat box on a glass or tinted
// window; a faint fill like the cards keeps it readable on all of them.
.scrollContentBackground(.hidden)
.background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 6))
.overlay(alignment: .topLeading) {
if model.comment.isEmpty {
Text("Comments or feedback (optional)").font(.system(size: 12)).foregroundStyle(.tertiary)
Expand Down Expand Up @@ -366,7 +370,7 @@ struct DeviceTestView: View {
private func stepTitle(_ text: String, changes: Bool) -> some View {
HStack(spacing: 8) {
title(text)
tag(changes ? "Changes, then puts back" : "Reads only", color: changes ? .batteryMid : .razerGreen)
tag(changes ? "Changes, then puts back" : "Reads only", color: changes ? .batteryMid : .razerGreenText)
}
}

Expand Down Expand Up @@ -403,7 +407,7 @@ struct DeviceTestView: View {
private func outcomeRow(_ label: String, _ outcome: DeviceReport.Outcome) -> some View {
let (text, color): (String, Color) = {
switch outcome {
case .passed: return ("Works", .razerGreen)
case .passed: return ("Works", .razerGreenText)
case .failed: return ("Didn't work", .batteryMid)
case .notSupported: return ("Not on this mouse", .secondary)
case .skipped: return ("Skipped", .secondary)
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/DeviceTestWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ final class DeviceTestWindowController: NSObject, AppWindowPresenter, NSWindowDe
let w = NSWindow(contentViewController: hosting)
w.styleMask = [.titled, .closable]
w.isReleasedWhenClosed = false
// Match the popover's forced-dark look, like the app's other windows.
w.appearance = NSAppearance(named: .darkAqua)
// Themed like the popover per Settings › Appearance, kept up to date when it changes.
AppearanceSettings.shared.track(w)
w.delegate = self
w.center()
window = w
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/PermissionsWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ final class PermissionsWindowController: AppWindowPresenter {
w.title = "MacRazer Setup"
w.styleMask = [.titled, .closable]
w.isReleasedWhenClosed = false
// Match the popover's forced-dark look so the green accent reads correctly.
w.appearance = NSAppearance(named: .darkAqua)
// Themed like the popover per Settings › Appearance, kept up to date when it changes.
AppearanceSettings.shared.track(w)
w.center()
window = w
}
Expand Down
Loading
Loading