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
3 changes: 3 additions & 0 deletions PanBar/Data/Persistence/Repositories/SettingsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ struct SettingsRepository {
static let tickerShowTotalAssets = "ticker_show_total_assets"
static let tickerShowTodayPnL = "ticker_show_today_pnl"
static let tickerShowAllTimePnL = "ticker_show_alltime_pnl"
static let tickerShowAppIcon = "ticker_show_app_icon"
static let tickerShowQuoteCode = "ticker_show_quote_code"
static let tickerShowQuoteName = "ticker_show_quote_name"
static let tickerMenuBarWidth = "ticker_menu_bar_width"
static let tickerScrollMenuBarWidth = "ticker_scroll_menu_bar_width"
static let tickerCarouselMenuBarWidth = "ticker_carousel_menu_bar_width"
Expand Down
26 changes: 25 additions & 1 deletion PanBar/Infrastructure/TickerPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ final class TickerPreferences: ObservableObject {
@Published var showAllTimePnL: Bool {
didSet { try? repo.set(SettingsRepository.Keys.tickerShowAllTimePnL, showAllTimePnL ? "1" : "0") }
}
@Published var showAppIcon: Bool {
didSet { try? repo.set(SettingsRepository.Keys.tickerShowAppIcon, showAppIcon ? "1" : "0") }
}
@Published var showQuoteCode: Bool {
didSet { try? repo.set(SettingsRepository.Keys.tickerShowQuoteCode, showQuoteCode ? "1" : "0") }
}
@Published var showQuoteName: Bool {
didSet { try? repo.set(SettingsRepository.Keys.tickerShowQuoteName, showQuoteName ? "1" : "0") }
}
@Published var menuBarWidth: Int {
didSet {
let clamped = Self.clampMenuBarWidth(menuBarWidth)
Expand Down Expand Up @@ -139,9 +148,20 @@ final class TickerPreferences: ObservableObject {
} else {
self.tickerIndexIDs = []
}
self.displayMode = TickerDisplayMode(rawValue: repo.string(SettingsRepository.Keys.tickerDisplayMode) ?? "") ?? .scroll
let storedDisplayMode = TickerDisplayMode(rawValue: repo.string(SettingsRepository.Keys.tickerDisplayMode) ?? "") ?? .scroll
let migratesScrollNoCode = storedDisplayMode == .scrollNoCode
self.displayMode = migratesScrollNoCode ? .scroll : storedDisplayMode
if migratesScrollNoCode {
try? repo.set(SettingsRepository.Keys.tickerDisplayMode, TickerDisplayMode.scroll.rawValue)
}
self.minimalMetric = MinimalMetric(rawValue: repo.string(SettingsRepository.Keys.tickerMinimalMetric) ?? "") ?? .todayPnL
self.carouselDwell = Int(repo.string(SettingsRepository.Keys.tickerCarouselDwell) ?? "") ?? 4
self.showAppIcon = repo.string(SettingsRepository.Keys.tickerShowAppIcon) != "0"
self.showQuoteCode = migratesScrollNoCode ? false : repo.string(SettingsRepository.Keys.tickerShowQuoteCode) != "0"
if migratesScrollNoCode {
try? repo.set(SettingsRepository.Keys.tickerShowQuoteCode, "0")
}
self.showQuoteName = repo.string(SettingsRepository.Keys.tickerShowQuoteName) != "0"
let rawLegacyWidth = Int(repo.string(SettingsRepository.Keys.tickerMenuBarWidth) ?? "") ?? 280
let legacyWidth = Self.clampMenuBarWidth(rawLegacyWidth)
self.menuBarWidth = legacyWidth
Expand All @@ -164,14 +184,18 @@ final class TickerPreferences: ObservableObject {
/// 菜单栏 ticker 的展现形式。
enum TickerDisplayMode: String, CaseIterable, Identifiable, Codable {
case scroll // 经典左右滚动(默认)
case scrollNoCode // 旧版本兼容:现在用 showQuoteCode 控制
case carousel // 一条一条上下淡入轮播
case compact // 三个简写卡片(今日 / 总盈亏 / 总市值)
case minimal // 只显示一个用户选定的数字

static let allCases: [TickerDisplayMode] = [.scroll, .carousel, .compact, .minimal]

var id: String { rawValue }
var displayName: String {
switch self {
case .scroll: return L("displayMode.scroll", comment: "")
case .scrollNoCode: return L("displayMode.scroll", comment: "")
case .carousel: return L("displayMode.carousel", comment: "")
case .compact: return L("displayMode.compact", comment: "")
case .minimal: return L("displayMode.minimal", comment: "")
Expand Down
15 changes: 11 additions & 4 deletions PanBar/MenuBar/CarouselTickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ final class CarouselTickerView: NSView {

let iconWidth: CGFloat = 18
var preferredTotalWidth: CGFloat?
var showsIcon: Bool = true
var visibleTextWidth: CGFloat = 200
var privacyHidden: Bool = false

var totalWidth: CGFloat {
if let preferredTotalWidth {
return max(40, preferredTotalWidth)
}
return iconWidth + 6 + visibleTextWidth + 4
return leadingTextX + visibleTextWidth + 4
}

private var leadingTextX: CGFloat {
showsIcon ? iconWidth + 6 : 2
}

override var isFlipped: Bool { false }
Expand Down Expand Up @@ -127,12 +132,14 @@ final class CarouselTickerView: NSView {
}

override func draw(_ dirtyRect: NSRect) {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
if showsIcon {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
}

let textRect = NSRect(
x: iconWidth + 6,
x: leadingTextX,
y: 0,
width: max(20, totalWidth - iconWidth - 10),
width: max(20, totalWidth - leadingTextX - 4),
height: bounds.height
)

Expand Down
10 changes: 7 additions & 3 deletions PanBar/MenuBar/CompactTickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ final class CompactTickerView: NSView {
var scheme: TickerColorScheme = .east
var privacyHidden: Bool = false
var preferredTotalWidth: CGFloat?
var showsIcon: Bool = true
/// hover 状态(放着满足协议,固定模式实际用不到)
var hovered: Bool = false
var onContentChanged: (() -> Void)?

let iconWidth: CGFloat = 18
private let visibleIconWidth: CGFloat = 18
var iconWidth: CGFloat { showsIcon ? visibleIconWidth : -4 }
private let slotSpacing: CGFloat = 10
private let labelFont = NSFont.systemFont(ofSize: 9, weight: .semibold)
private let valueFont = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .semibold)
Expand Down Expand Up @@ -68,14 +70,16 @@ final class CompactTickerView: NSView {
}

override func draw(_ dirtyRect: NSRect) {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
if showsIcon {
drawIcon(in: NSRect(x: 2, y: (bounds.height - visibleIconWidth) / 2, width: visibleIconWidth, height: visibleIconWidth))
}

if privacyHidden {
let dots = NSAttributedString(string: "•••", attributes: [
.font: valueFont,
.foregroundColor: NSColor.secondaryLabelColor
])
dots.draw(at: NSPoint(x: iconWidth + 8, y: bounds.midY - dots.size().height / 2))
dots.draw(at: NSPoint(x: iconWidth + 10, y: bounds.midY - dots.size().height / 2))
return
}

Expand Down
1 change: 1 addition & 0 deletions PanBar/MenuBar/MenuBarTickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ protocol MenuBarTickerView: NSView {
var totalWidth: CGFloat { get }
var privacyHidden: Bool { get set }
var preferredTotalWidth: CGFloat? { get set }
var showsIcon: Bool { get set }
/// 鼠标 hover 状态;由 controller 监听 button 的 tracking area 同步过来
var hovered: Bool { get set }
/// 内容变化通知(动画帧 / 数据更新),controller 重新捕图。
Expand Down
17 changes: 12 additions & 5 deletions PanBar/MenuBar/MinimalTickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class MinimalTickerView: NSView {
var scheme: TickerColorScheme = .east
var privacyHidden: Bool = false
var preferredTotalWidth: CGFloat?
var showsIcon: Bool = true
var hovered: Bool = false
var onContentChanged: (() -> Void)?

Expand All @@ -24,8 +25,12 @@ final class MinimalTickerView: NSView {
if let preferredTotalWidth {
return max(40, preferredTotalWidth)
}
guard let _ = content else { return iconWidth + 40 }
return iconWidth + 6 + max(56, renderedString().size().width) + 6
guard let _ = content else { return leadingTextX + 40 }
return leadingTextX + max(56, renderedString().size().width) + 6
}

private var leadingTextX: CGFloat {
showsIcon ? iconWidth + 6 : 2
}

override var isFlipped: Bool { false }
Expand Down Expand Up @@ -54,20 +59,22 @@ final class MinimalTickerView: NSView {
}

override func draw(_ dirtyRect: NSRect) {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
if showsIcon {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
}

if privacyHidden {
let dots = NSAttributedString(string: "•••", attributes: [
.font: valueFont,
.foregroundColor: NSColor.secondaryLabelColor
])
dots.draw(at: NSPoint(x: iconWidth + 8, y: bounds.midY - dots.size().height / 2))
dots.draw(at: NSPoint(x: leadingTextX + 4, y: bounds.midY - dots.size().height / 2))
return
}

let str = renderedString()
let size = str.size()
let p = NSPoint(x: iconWidth + 6, y: (bounds.height - size.height) / 2)
let p = NSPoint(x: leadingTextX, y: (bounds.height - size.height) / 2)
str.draw(at: p)
}

Expand Down
13 changes: 10 additions & 3 deletions PanBar/MenuBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ final class StatusItemController {
}

private func applyPrefs() {
renderer = TickerRenderer(scheme: prefs.colorScheme)
renderer = TickerRenderer(
scheme: prefs.colorScheme,
showsQuoteCode: prefs.showQuoteCode,
showsQuoteName: prefs.showQuoteName
)
// mode 变化:整个 view 都要换
if prefs.displayMode != currentMode {
swapTickerView(to: prefs.displayMode)
Expand All @@ -77,7 +81,10 @@ final class StatusItemController {
tickerView.preferredTotalWidth = prefs.compactAutoWidth ? nil : CGFloat(prefs.compactMenuBarWidth)
case .minimal:
tickerView.preferredTotalWidth = nil
case .scrollNoCode:
tickerView.preferredTotalWidth = prefs.scrollAutoWidth ? nil : CGFloat(prefs.scrollMenuBarWidth)
}
tickerView.showsIcon = prefs.showAppIcon
// 各模式独立配置
if let scroll = tickerView as? TickerView {
scroll.pixelsPerSecond = prefs.scrollSpeed.pixelsPerSecond
Expand All @@ -102,7 +109,7 @@ final class StatusItemController {
private static func makeView(for mode: TickerDisplayMode, scheme: TickerColorScheme) -> MenuBarTickerView {
let frame = NSRect(x: 0, y: 0, width: 200, height: 22)
switch mode {
case .scroll:
case .scroll, .scrollNoCode:
return TickerView(frame: frame)
case .carousel:
return CarouselTickerView(frame: frame)
Expand Down Expand Up @@ -207,7 +214,7 @@ final class StatusItemController {
/// 各模式根据当前数据自己组装,写回到 statusItem.length。
private func render(quotes: [SymbolID: Quote]) {
switch currentMode {
case .scroll:
case .scroll, .scrollNoCode:
guard let view = tickerView as? TickerView else { return }
let items = buildTickerItems(quotes: quotes)
view.update(attributed: renderer.render(items: items))
Expand Down
8 changes: 6 additions & 2 deletions PanBar/MenuBar/TickerRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ enum TickerDirection {
struct TickerRenderer {
var scheme: TickerColorScheme = .east
var font: NSFont = NSFont.menuBarFont(ofSize: 0)
var showsQuoteCode: Bool = true
var showsQuoteName: Bool = true

/// 涨色 / 跌色:走 SemanticColors,与 Popover 保持一致 + 菜单栏对比度优化。
private var upColor: NSColor { SemanticColors.upNS(scheme: scheme) }
Expand Down Expand Up @@ -137,8 +139,10 @@ struct TickerRenderer {
let name = shortenedName(q.name, market: q.symbol.market)

let s = NSMutableAttributedString()
s.append(NSAttributedString(string: display + " ", attributes: symbolAttr))
if !name.isEmpty {
if showsQuoteCode {
s.append(NSAttributedString(string: display + " ", attributes: symbolAttr))
}
if showsQuoteName && !name.isEmpty {
s.append(NSAttributedString(string: name + " ", attributes: nameAttr))
}
s.append(NSAttributedString(string: formatPrice(q.price) + " ", attributes: valueAttr))
Expand Down
15 changes: 11 additions & 4 deletions PanBar/MenuBar/TickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class TickerView: NSView {
/// 文字与图标之间的间距。
let iconWidth: CGFloat = 18
var preferredTotalWidth: CGFloat?
var showsIcon: Bool = true
/// 滚动文字可视区宽度(超出会被裁剪)。
var visibleTextWidth: CGFloat = 280
/// 隐私模式:屏幕共享或用户手动开启时,只显示图标和 "•••",不绘制具体行情。
Expand All @@ -42,7 +43,11 @@ final class TickerView: NSView {
if let preferredTotalWidth {
return max(40, preferredTotalWidth)
}
return iconWidth + 6 + visibleTextWidth + 4
return leadingTextX + visibleTextWidth + 4
}

private var leadingTextX: CGFloat {
showsIcon ? iconWidth + 6 : 2
}

// MARK: lifecycle
Expand Down Expand Up @@ -141,12 +146,14 @@ final class TickerView: NSView {
let ctx = NSGraphicsContext.current?.cgContext

// P 图标(左侧)
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
if showsIcon {
drawIcon(in: NSRect(x: 2, y: (bounds.height - iconWidth) / 2, width: iconWidth, height: iconWidth))
}

let textRect = NSRect(
x: iconWidth + 6,
x: leadingTextX,
y: 0,
width: max(20, totalWidth - iconWidth - 10),
width: max(20, totalWidth - leadingTextX - 4),
height: bounds.height
)

Expand Down
24 changes: 24 additions & 0 deletions PanBar/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,30 @@
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "菜单栏宽度: %d pt" } }
}
},
"ticker.contentSection" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Display content" } },
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示内容" } }
}
},
"ticker.showAppIcon" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show app icon" } },
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示应用图标" } }
}
},
"ticker.showQuoteCode" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show quote code" } },
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示代码" } }
}
},
"ticker.showQuoteName" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show quote name" } },
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示名称" } }
}
},
"ticker.summarySection" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Summary metrics" } },
Expand Down
13 changes: 13 additions & 0 deletions PanBar/Settings/Panes/TickerPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ private struct TickerPaneContent: View {
}
}

Section(header: Text(L("ticker.contentSection", comment: "")).font(.headline)) {
Toggle(L("ticker.showAppIcon", comment: ""), isOn: $prefs.showAppIcon)
if showsQuoteContentControls {
Toggle(L("ticker.showQuoteCode", comment: ""), isOn: $prefs.showQuoteCode)
Toggle(L("ticker.showQuoteName", comment: ""), isOn: $prefs.showQuoteName)
}
}

Section(header: Text(L("ticker.summarySection", comment: "")).font(.headline)) {
Toggle(L("ticker.showTodayPnL", comment: ""), isOn: $prefs.showTodayPnL)
Toggle(L("ticker.showAllTimePnL", comment: ""), isOn: $prefs.showAllTimePnL)
Expand Down Expand Up @@ -165,12 +173,17 @@ private struct TickerPaneContent: View {
private var displayModeHint: String {
switch prefs.displayMode {
case .scroll: return L("displayMode.scroll.hint", comment: "")
case .scrollNoCode: return L("displayMode.scroll.hint", comment: "")
case .carousel: return L("displayMode.carousel.hint", comment: "")
case .compact: return L("displayMode.compact.hint", comment: "")
case .minimal: return L("displayMode.minimal.hint", comment: "")
}
}

private var showsQuoteContentControls: Bool {
prefs.displayMode == .scroll || prefs.displayMode == .scrollNoCode || prefs.displayMode == .carousel
}

private func marketBadge(_ m: Market) -> some View {
Text(m.displayName)
.font(.system(size: 10))
Expand Down
Loading