feat: separate dashboard and Stats settings

This commit is contained in:
Serhiy Mytrovtsiy
2020-12-26 13:02:32 +01:00
parent 53501b06c2
commit ec4fc1e2ec
14 changed files with 695 additions and 373 deletions

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
9A0C82E124460F7200FAE3D4 /* StatsKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; };
9A0C82E224460F7200FAE3D4 /* StatsKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A0C82E624460F9A00FAE3D4 /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A654920244074B500E30B74 /* extensions.swift */; };
@@ -369,6 +370,7 @@
7A19DAE52552C326001B192F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
97B5592A24FD84E000D3C4FF /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
98BF5451254DF04C004E9DF5 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
9A045EB62594F8D100ED58F2 /* Dashboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = "<group>"; };
9A0C82D124460DFF00FAE3D4 /* updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = updater.swift; sourceTree = "<group>"; };
9A0C82D324460E4400FAE3D4 /* launchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = launchAtLogin.swift; sourceTree = "<group>"; };
9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StatsKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -713,6 +715,7 @@
isa = PBXGroup;
children = (
9A81C74C24499C7000825D92 /* Settings.swift */,
9A045EB62594F8D100ED58F2 /* Dashboard.swift */,
9A81C74B24499C7000825D92 /* AppSettings.swift */,
9A9EA9442476D34500E3B883 /* Update.swift */,
);
@@ -1427,6 +1430,7 @@
9AABEB7E243FDEF100668CB0 /* main.swift in Sources */,
9AABEB7A243FD26200668CB0 /* AppDelegate.swift in Sources */,
9A9EA9452476D34500E3B883 /* Update.swift in Sources */,
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */,
9A81C74E24499C7000825D92 /* Settings.swift in Sources */,
9A81C74D24499C7000825D92 /* AppSettings.swift in Sources */,
9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */,

View File

@@ -21,7 +21,6 @@ import Fans
var store: Store = Store()
let updater = macAppUpdater(user: "exelban", repo: "stats")
let systemKit: SystemKit = SystemKit()
var smc: SMCService = SMCService()
var modules: [Module] = [
Battery(&store),

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "baseline_settings_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_settings_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_settings_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -52,6 +52,10 @@
"Show icon in dock" = "Show icon in dock";
"Start at login" = "Start at login";
// Dashboard
"Serial number" = "Serial number";
"Uptime" = "Uptime";
// Update
"The latest version of Stats installed" = "The latest version of Stats installed";
"Downloading..." = "Downloading...";

View File

@@ -49,8 +49,8 @@
"Update application" = "Aktualizuj aplikacje";
"Check for updates" = "Sprawdzaj aktualizacje";
"Check for update" = "Sprawdź aktualizacje";
"Show icon in dock" = "Pokaż ikonę w docku";
"Start at login" = "Uruchom przy logowaniu";
"Show icon in dock" = "Pokazuj ikonę w docku";
"Start at login" = "Uruchamiać przy logowaniu";
// Update
"The latest version of Stats installed" = "Najnowsza wersja Stats zainstalowana";

View File

@@ -50,7 +50,7 @@
"Check for updates" = "Перевіряти оновленя";
"Check for update" = "Перевірити оновленя";
"Show icon in dock" = "Показувати іконку в dock";
"Start at login" = "Запуск при логуванні";
"Start at login" = "Запускати при логуванні";
// Update
"The latest version of Stats installed" = "Встановлено останню версію";

View File

@@ -13,11 +13,7 @@ import Cocoa
import StatsKit
import os.log
class ApplicationSettings: NSView {
private let width: CGFloat = 540
private let height: CGFloat = 480
private let deviceInfoHeight: CGFloat = 300
class ApplicationSettings: NSScrollView {
private var updateIntervalValue: AppUpdateInterval {
get {
return store.string(key: "update-interval", defaultValue: AppUpdateIntervals.atStart.rawValue)
@@ -37,255 +33,64 @@ class ApplicationSettings: NSView {
private let updateWindow: UpdateWindow = UpdateWindow()
init() {
super.init(frame: NSRect(x: 0, y: 0, width: width, height: height))
self.wantsLayer = true
self.layer?.backgroundColor = .clear
super.init(frame: NSRect(
x: 0,
y: 0,
width: 540,
height: 480
))
self.addDeviceInfo()
self.addSettings()
self.drawsBackground = false
self.translatesAutoresizingMaskIntoConstraints = true
self.borderType = .noBorder
self.hasVerticalScroller = true
self.hasHorizontalScroller = false
self.autohidesScrollers = true
self.horizontalScrollElasticity = .none
let versionsView = self.versions()
let settingsView = self.settings()
let grid: NSGridView = NSGridView(frame: NSRect(
x: 0,
y: 0,
width: self.frame.width,
height: versionsView.frame.height + settingsView.frame.height
))
grid.rowSpacing = 0
grid.yPlacement = .fill
let separator = NSBox()
separator.boxType = .separator
grid.addRow(with: [versionsView])
grid.addRow(with: [separator])
grid.addRow(with: [settingsView])
grid.row(at: 0).height = versionsView.frame.height
grid.row(at: 2).height = settingsView.frame.height
self.documentView = grid
self.scroll(NSPoint(x: 0, y: grid.frame.size.height))
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidMoveToWindow() {
if let button = self.updateButton, let version = updater.latest {
if version.newest {
button.title = LocalizedString("Update application")
} else {
button.title = LocalizedString("Check for update")
}
}
}
private func addSettings() {
let view: NSView = NSView(frame: NSRect(x: 0, y: 1, width: self.width-1, height: self.height - self.deviceInfoHeight))
let rowHeight: CGFloat = 40
let rowHorizontalPadding: CGFloat = 16
private func versions() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 280))
let leftPanel: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width/2, height: view.frame.height))
leftPanel.wantsLayer = true
let h: CGFloat = 120+60+18
let container: NSGridView = NSGridView(frame: NSRect(x: 0, y: (view.frame.height-h)/2, width: self.frame.width, height: h))
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
var processorInfo = ""
if systemKit.device.info?.cpu?.name != "" {
processorInfo += "\(systemKit.device.info?.cpu?.name ?? LocalizedString("Unknown"))\n"
}
processorInfo += "\(systemKit.device.info?.cpu?.physicalCores ?? 0) cores (\(systemKit.device.info?.cpu?.logicalCores ?? 0) threads)"
leftPanel.addSubview(makeInfoRow(
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*3, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight+8),
title: LocalizedString("Processor"),
value: processorInfo
))
let iconView: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!)
iconView.frame = NSRect(x: (view.frame.width - 50)/2, y: 0, width: 50, height: 50)
let sizeFormatter = ByteCountFormatter()
sizeFormatter.allowedUnits = [.useGB]
sizeFormatter.countStyle = .memory
leftPanel.addSubview(makeInfoRow(
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*2, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Memory"),
value: "\(sizeFormatter.string(fromByteCount: Int64(systemKit.device.info?.ram?.total ?? 0)))"
))
let gpus = systemKit.device.info?.gpu
var gpu: String = LocalizedString("Unknown")
if gpus != nil {
if gpus?.count == 1 {
gpu = gpus![0].name
} else {
gpu = ""
gpus!.forEach{ gpu += "\($0.name)\n" }
}
}
leftPanel.addSubview(makeInfoRow(
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*1, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Graphics"),
value: gpu
))
leftPanel.addSubview(makeInfoRow(
frame: NSRect(x: rowHorizontalPadding, y: 0, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Disk"),
value: "\(systemKit.device.info?.disk?.model ?? systemKit.device.info?.disk?.name ?? LocalizedString("Unknown"))"
))
let rightPanel: NSView = NSView(frame: NSRect(x: self.width/2, y: 0, width: view.frame.width/2, height: view.frame.height))
rightPanel.addSubview(makeSelectRow(
frame: NSRect(x: rowHorizontalPadding*0.5, y: rowHeight*3, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Check for updates"),
action: #selector(self.toggleUpdateInterval),
items: AppUpdateIntervals.allCases.map{ $0.rawValue },
selected: self.updateIntervalValue
))
let temperature = SelectRow(
frame: NSRect(
x: rowHorizontalPadding*0.5,
y: rowHeight*2,
width: rightPanel.frame.width - (rowHorizontalPadding*1.5),
height: rowHeight
),
title: LocalizedString("Temperature"),
action: #selector(toggleTemperatureUnits),
items: TemperatureUnits,
selected: self.temperatureUnitsValue
)
temperature.subviews.forEach { (v: NSView) in
if let view = v as? LabelField {
view.textColor = .secondaryLabelColor
}
}
rightPanel.addSubview(temperature)
rightPanel.addSubview(makeSettingRow(
frame: NSRect(x: rowHorizontalPadding*0.5, y: rowHeight*1, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Show icon in dock"),
action: #selector(self.toggleDock),
state: store.bool(key: "dockIcon", defaultValue: false)
))
rightPanel.addSubview(makeSettingRow(
frame: NSRect(x: rowHorizontalPadding*0.5, y: 0, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
title: LocalizedString("Start at login"),
action: #selector(self.toggleLaunchAtLogin),
state: LaunchAtLogin.isEnabled
))
view.addSubview(leftPanel)
view.addSubview(rightPanel)
self.addSubview(view)
}
func makeSelectRow(frame: NSRect, title: String, action: Selector, items: [String], selected: String) -> NSView {
let row: NSView = NSView(frame: frame)
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (row.frame.height - 32)/2, width: row.frame.width - 52, height: 32), title)
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .secondaryLabelColor
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-28)/2, width: 50, height: 28))
select.target = self
select.action = action
let menu = NSMenu()
items.forEach { (color: String) in
if color.contains("separator") {
menu.addItem(NSMenuItem.separator())
} else {
let interfaceMenu = NSMenuItem(title: color, action: nil, keyEquivalent: "")
menu.addItem(interfaceMenu)
if selected == color {
interfaceMenu.state = .on
}
}
}
select.menu = menu
select.sizeToFit()
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
row.addSubview(select)
row.addSubview(rowTitle)
return row
}
private func makeInfoRow(frame: NSRect, title: String, value: String) -> NSView {
let row: NSView = NSView(frame: frame)
let titleWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 10
let rowTitle: NSTextField = TextView(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: titleWidth, height: 17))
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .secondaryLabelColor
rowTitle.stringValue = title
let rowValue: NSTextField = TextView(frame: NSRect(x: titleWidth, y: (row.frame.height - 16)/2, width: row.frame.width - titleWidth, height: 17))
rowValue.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowValue.alignment = .right
rowValue.stringValue = value
rowValue.isSelectable = true
if value.contains("\n") {
rowValue.frame = NSRect(x: titleWidth, y: 0, width: rowValue.frame.width, height: row.frame.height)
}
row.addSubview(rowTitle)
row.addSubview(rowValue)
return row
}
private func makeSettingRow(frame: NSRect, title: String, action: Selector, state: Bool) -> NSView {
let row: NSView = NSView(frame: frame)
let state: NSControl.StateValue = state ? .on : .off
let rowTitle: NSTextField = TextView(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: row.frame.width - 52, height: 17))
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .secondaryLabelColor
rowTitle.stringValue = title
var toggle: NSControl = NSControl()
if #available(OSX 10.15, *) {
let switchButton = NSSwitch(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height))
switchButton.state = state
switchButton.action = action
switchButton.target = self
toggle = switchButton
} else {
let button: NSButton = NSButton(frame: NSRect(x: row.frame.width - 30, y: 0, width: 30, height: row.frame.height))
button.setButtonType(.switch)
button.state = state
button.title = ""
button.action = action
button.isBordered = false
button.isTransparent = true
button.target = self
toggle = button
}
row.addSubview(toggle)
row.addSubview(rowTitle)
return row
}
private func addDeviceInfo() {
let view: NSView = NSView(frame: NSRect(x: 0, y: self.height - self.deviceInfoHeight, width: self.width, height: self.deviceInfoHeight))
let leftPanel: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.width/2, height: self.deviceInfoHeight))
let deviceImageView: NSImageView = NSImageView(image: systemKit.device.model.icon)
deviceImageView.frame = NSRect(x: (leftPanel.frame.width - 160)/2, y: ((self.deviceInfoHeight - 120)/2) + 22, width: 160, height: 120)
let deviceNameField: NSTextField = TextView(frame: NSRect(x: 0, y: 72, width: leftPanel.frame.width, height: 20))
deviceNameField.alignment = .center
deviceNameField.font = NSFont.systemFont(ofSize: 14, weight: .regular)
deviceNameField.stringValue = systemKit.device.model.name
deviceNameField.isSelectable = true
deviceNameField.toolTip = systemKit.device.modelIdentifier
let osField: NSTextField = TextView(frame: NSRect(x: 0, y: 52, width: leftPanel.frame.width, height: 18))
osField.alignment = .center
osField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
osField.stringValue = "macOS \(systemKit.device.os?.name ?? LocalizedString("Unknown")) (\(systemKit.device.os?.version.getFullVersion() ?? ""))"
osField.isSelectable = true
leftPanel.addSubview(deviceImageView)
leftPanel.addSubview(deviceNameField)
leftPanel.addSubview(osField)
let rightPanel: NSView = NSView(frame: NSRect(x: self.width/2, y: 0, width: self.width/2, height: self.deviceInfoHeight))
let iconView: NSImageView = NSImageView(frame: NSRect(x: (leftPanel.frame.width - 100)/2, y: ((self.deviceInfoHeight - 100)/2) + 32, width: 100, height: 100))
iconView.image = NSImage(named: NSImage.Name("AppIcon"))!
let infoView: NSView = NSView(frame: NSRect(x: 0, y: 54, width: self.width/2, height: 42))
let statsName: NSTextField = TextView(frame: NSRect(x: 0, y: 20, width: leftPanel.frame.width, height: 22))
let statsName: NSTextField = TextView(frame: NSRect(x: 0, y: 20, width: view.frame.width, height: 22))
statsName.alignment = .center
statsName.font = NSFont.systemFont(ofSize: 20, weight: .regular)
statsName.stringValue = "Stats"
@@ -294,31 +99,137 @@ class ApplicationSettings: NSView {
let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
let statsVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: leftPanel.frame.width, height: 16))
let statsVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 16))
statsVersion.alignment = .center
statsVersion.font = NSFont.systemFont(ofSize: 12, weight: .regular)
statsVersion.stringValue = "\(LocalizedString("Version")) \(versionNumber)"
statsVersion.isSelectable = true
statsVersion.toolTip = "Build number: \(buildNumber)"
infoView.addSubview(statsName)
infoView.addSubview(statsVersion)
let button: NSButton = NSButton(frame: NSRect(x: (rightPanel.frame.width - 160)/2, y: 20, width: 160, height: 28))
let button: NSButton = NSButton(frame: NSRect(x: (view.frame.width - 160)/2, y: 0, width: 160, height: 30))
button.title = LocalizedString("Check for update")
button.bezelStyle = .rounded
button.target = self
button.action = #selector(updateAction)
self.updateButton = button
rightPanel.addSubview(iconView)
rightPanel.addSubview(infoView)
rightPanel.addSubview(button)
container.addRow(with: [iconView])
container.addRow(with: [statsName])
container.addRow(with: [statsVersion])
container.addRow(with: [button])
view.addSubview(leftPanel)
view.addSubview(rightPanel)
container.column(at: 0).width = self.frame.width
container.row(at: 1).height = 22
container.row(at: 2).height = 20
container.row(at: 3).height = 30
self.addSubview(view)
view.addSubview(container)
return view
}
private func settings() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
let grid: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 0))
grid.rowSpacing = 10
grid.columnSpacing = 20
grid.xPlacement = .trailing
grid.rowAlignment = .firstBaseline
grid.translatesAutoresizingMaskIntoConstraints = false
let separator = NSBox()
separator.boxType = .separator
grid.addRow(with: self.updates())
grid.addRow(with: self.temperature())
grid.addRow(with: self.dockIcon())
grid.addRow(with: self.startAtLogin())
view.addSubview(grid)
var height: CGFloat = grid.rowSpacing*2
for i in 0..<grid.numberOfRows {
let row = grid.row(at: i)
for a in 0..<row.numberOfCells {
height += row.cell(at: a).contentView?.frame.height ?? 0
}
}
view.setFrameSize(NSSize(width: view.frame.width, height: max(200, height)))
NSLayoutConstraint.activate([
grid.centerXAnchor.constraint(equalTo: view.centerXAnchor),
grid.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
return view
}
// MARK: - Views
private func updates() -> [NSView] {
return [
self.titleView(LocalizedString("Check for updates")),
SelectView(
action: #selector(self.toggleUpdateInterval),
items: AppUpdateIntervals.allCases.map{ KeyValue_t(key: $0.rawValue, value: $0.rawValue) },
selected: self.updateIntervalValue
)
]
}
private func temperature() -> [NSView] {
return [
self.titleView(LocalizedString("Temperature")),
SelectView(
action: #selector(self.toggleTemperatureUnits),
items: TemperatureUnits,
selected: self.temperatureUnitsValue
)
]
}
private func dockIcon() -> [NSView] {
return [
self.titleView(LocalizedString("Show icon in dock")),
self.toggleView(
action: #selector(self.toggleDock),
state: store.bool(key: "dockIcon", defaultValue: false)
)
]
}
private func startAtLogin() -> [NSView] {
return [
self.titleView(LocalizedString("Start at login")),
self.toggleView(
action: #selector(self.toggleLaunchAtLogin),
state: LaunchAtLogin.isEnabled
)
]
}
// MARK: - helpers
private func titleView(_ value: String) -> NSTextField {
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 120, height: 17))
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
field.textColor = .secondaryLabelColor
field.stringValue = value
return field
}
private func toggleView(action: Selector, state: Bool) -> NSView {
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 10))
button.setButtonType(.switch)
button.state = state ? .on : .off
button.title = ""
button.action = action
button.isBordered = false
button.isTransparent = true
button.target = self
return button
}
@objc func updateAction(_ sender: NSObject) {
@@ -354,33 +265,18 @@ class ApplicationSettings: NSView {
self.temperatureUnitsValue = key
}
@objc func toggleDock(_ sender: NSControl) {
var state: NSControl.StateValue? = nil
if #available(OSX 10.15, *) {
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
} else {
state = sender is NSButton ? (sender as! NSButton).state: nil
}
@objc func toggleDock(_ sender: NSButton) {
store.set(key: "dockIcon", value: sender.state == NSControl.StateValue.on)
NSApp.setActivationPolicy(sender.state == .on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory)
if state != nil {
store.set(key: "dockIcon", value: state! == NSControl.StateValue.on)
}
let dockIconStatus = state == NSControl.StateValue.on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
NSApp.setActivationPolicy(dockIconStatus)
if state == .off {
if sender.state == .off {
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
@objc func toggleLaunchAtLogin(_ sender: NSControl) {
var state: NSControl.StateValue? = nil
if #available(OSX 10.15, *) {
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
} else {
state = sender is NSButton ? (sender as! NSButton).state: nil
}
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
LaunchAtLogin.isEnabled = sender.state == .on
LaunchAtLogin.isEnabled = state! == NSControl.StateValue.on
if !store.exist(key: "runAtLoginInitialized") {
store.set(key: "runAtLoginInitialized", value: true)
}

332
Stats/Views/Dashboard.swift Normal file
View File

@@ -0,0 +1,332 @@
//
// Stats.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 24/12/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import StatsKit
import ModuleKit
import os.log
class Dashboard: NSScrollView {
private var uptimeField: NSTextField? = nil
init() {
super.init(frame: NSRect(
x: 0,
y: 0,
width: 540,
height: 480
))
self.drawsBackground = false
self.translatesAutoresizingMaskIntoConstraints = true
self.borderType = .noBorder
self.hasVerticalScroller = true
self.hasHorizontalScroller = false
self.autohidesScrollers = true
self.horizontalScrollElasticity = .none
let versionsView = self.versions()
let specsView = self.specs()
let grid: NSGridView = NSGridView(frame: NSRect(
x: 0,
y: 0,
width: self.frame.width,
height: versionsView.frame.height + specsView.frame.height
))
grid.rowSpacing = 0
grid.yPlacement = .fill
let separator = NSBox()
separator.boxType = .separator
grid.addRow(with: [versionsView])
grid.addRow(with: [separator])
grid.addRow(with: [specsView])
grid.row(at: 0).height = versionsView.frame.height
grid.row(at: 2).height = specsView.frame.height
self.documentView = grid
self.scroll(NSPoint(x: 0, y: grid.frame.size.height))
NotificationCenter.default.addObserver(self, selector: #selector(windowOpens), name: .openModuleSettings, object: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func versions() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 280))
let h: CGFloat = 120+60+18
let container: NSGridView = NSGridView(frame: NSRect(x: 0, y: (view.frame.height-h)/2, width: self.frame.width, height: h))
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let deviceImageView: NSImageView = NSImageView(image: SystemKit.shared.device.model.icon)
deviceImageView.frame = NSRect(x: (view.frame.width - 160)/2, y: 0, width: 160, height: 120)
let deviceNameField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
deviceNameField.alignment = .center
deviceNameField.font = NSFont.systemFont(ofSize: 14, weight: .regular)
deviceNameField.stringValue = SystemKit.shared.device.model.name
deviceNameField.isSelectable = true
deviceNameField.toolTip = SystemKit.shared.device.modelIdentifier
let osField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 18))
osField.alignment = .center
osField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
osField.stringValue = "macOS \(SystemKit.shared.device.os?.name ?? LocalizedString("Unknown")) (\(SystemKit.shared.device.os?.version.getFullVersion() ?? ""))"
osField.isSelectable = true
container.addRow(with: [deviceImageView])
container.addRow(with: [deviceNameField])
container.addRow(with: [osField])
container.column(at: 0).width = self.frame.width
container.row(at: 1).height = 22
container.row(at: 2).height = 20
view.addSubview(container)
return view
}
private func specs() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
let grid: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 0))
grid.rowSpacing = 10
grid.columnSpacing = 20
grid.xPlacement = .trailing
grid.rowAlignment = .firstBaseline
grid.translatesAutoresizingMaskIntoConstraints = false
let separator = NSBox()
separator.boxType = .separator
grid.addRow(with: self.processor())
grid.addRow(with: self.ram())
grid.addRow(with: self.gpu())
grid.addRow(with: self.disk())
grid.addRow(with: self.serialNumber())
grid.addRow(with: [separator])
grid.row(at: 5).mergeCells(in: NSRange(location: 0, length: 2))
grid.row(at: 5).topPadding = 5
grid.row(at: 5).bottomPadding = 5
grid.addRow(with: self.upTime())
view.addSubview(grid)
var height: CGFloat = grid.rowSpacing*2
for i in 0..<grid.numberOfRows {
let row = grid.row(at: i)
for a in 0..<row.numberOfCells {
height += row.cell(at: a).contentView?.frame.height ?? 0
}
}
view.setFrameSize(NSSize(width: view.frame.width, height: height))
NSLayoutConstraint.activate([
grid.centerXAnchor.constraint(equalTo: view.centerXAnchor),
grid.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
return view
}
@objc private func windowOpens(_ notification: Notification) {
guard notification.userInfo?["module"] as? String == "Stats" else {
return
}
let form = DateComponentsFormatter()
form.maximumUnitCount = 2
form.unitsStyle = .full
form.allowedUnits = [.day, .hour, .minute]
if let bootDate = SystemKit.shared.device.bootDate {
if let duration = form.string(from: bootDate, to: Date()) {
self.uptimeField?.stringValue = duration
}
}
}
// MARK: - Views
private func processor() -> [NSView] {
var value = ""
if let cpu = SystemKit.shared.device.info.cpu, cpu.name != nil || cpu.physicalCores != nil || cpu.logicalCores != nil {
if let name = cpu.name {
value += name
}
if cpu.physicalCores != nil || cpu.logicalCores != nil {
if value.count != 0 {
value += "\n"
}
var mini = ""
if let cores = cpu.physicalCores {
mini += "\(cores) cores"
}
if let threads = cpu.logicalCores {
if mini != "" {
mini += ", "
}
mini += "\(threads) threads"
}
value += "\(mini)"
}
} else {
value = LocalizedString("Unknown")
}
return [
self.titleView("\(LocalizedString("Processor")):"),
self.valueView(value),
]
}
private func ram() -> [NSView] {
let sizeFormatter = ByteCountFormatter()
sizeFormatter.allowedUnits = [.useGB]
sizeFormatter.countStyle = .memory
var value = ""
if let dimms = SystemKit.shared.device.info.ram?.dimms {
for i in 0..<dimms.count {
let dimm = dimms[i]
var row = ""
if let size = dimm.size {
row += size
}
if let speed = dimm.speed {
if row.count != 0 && row.last != " " {
row += " "
}
row += speed
}
if let type = dimm.type {
if row.count != 0 && row.last != " " {
row += " "
}
row += type
}
if dimm.bank != nil || dimm.channel != nil {
if row.count != 0 && row.last != " " {
row += " "
}
var mini = "("
if let bank = dimm.bank {
mini += "slot \(bank)"
}
if let ch = dimm.channel {
mini += "\(mini == "(" ? "" : "/")ch \(ch)"
}
row += "\(mini))"
}
value += "\(row)\(i == dimms.count-1 ? "" : "\n")"
}
} else {
value = LocalizedString("Unknown")
}
return [
self.titleView("\(LocalizedString("Memory")):"),
self.valueView("\(value)"),
]
}
private func gpu() -> [NSView] {
let gpus = SystemKit.shared.device.info.gpu
var gpu: String = LocalizedString("Unknown")
if gpus != nil {
if gpus?.count == 1 {
gpu = gpus![0].name
} else {
gpu = ""
gpus!.forEach{ gpu += "\($0.name)\n" }
}
}
return [
self.titleView("\(LocalizedString("Graphics")):"),
self.valueView(gpu),
]
}
private func disk() -> [NSView] {
return [
self.titleView("\(LocalizedString("Disk")):"),
self.valueView("\(SystemKit.shared.device.info.disk?.model ?? SystemKit.shared.device.info.disk?.name ?? LocalizedString("Unknown"))"),
]
}
private func serialNumber() -> [NSView] {
return [
self.titleView("\(LocalizedString("Serial number")):"),
self.valueView("\(SystemKit.shared.device.serialNumber ?? LocalizedString("Unknown"))"),
]
}
private func upTime() -> [NSView] {
let form = DateComponentsFormatter()
form.maximumUnitCount = 2
form.unitsStyle = .full
form.allowedUnits = [.day, .hour, .minute]
var value = LocalizedString("Unknown")
if let bootDate = SystemKit.shared.device.bootDate {
if let duration = form.string(from: bootDate, to: Date()) {
value = duration
}
}
let valueView = self.valueView(value)
self.uptimeField = valueView
return [
self.titleView("\(LocalizedString("Uptime")):"),
valueView,
]
}
// MARK: - Helpers
private func titleView(_ value: String) -> NSTextField {
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 120, height: 17))
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
field.textColor = .labelColor
field.stringValue = value
return field
}
private func valueView(_ value: String) -> NSTextField {
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 17))
field.font = NSFont.systemFont(ofSize: 13, weight: .light)
field.alignment = .right
field.stringValue = value
field.isSelectable = true
return field
}
}

View File

@@ -114,13 +114,14 @@ private class SettingsView: NSView {
private var navigationView: NSView = NSView()
private var mainView: NSView = NSView()
private var applicationSettings: NSView = ApplicationSettings()
private var dashboard: NSView = Dashboard()
private var settings: NSView = ApplicationSettings()
override init(frame: NSRect) {
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
self.wantsLayer = true
NotificationCenter.default.addObserver(self, selector: #selector(menuCallback), name: .openSettingsView, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(menuCallback), name: .openModuleSettings, object: nil)
let sidebar = NSVisualEffectView(frame: NSMakeRect(0, 0, self.sidebarWidth, self.frame.height))
sidebar.material = .sidebar
@@ -135,12 +136,12 @@ private class SettingsView: NSView {
)
self.menuView.wantsLayer = true
self.menuView.drawsBackground = false
self.menuView.addSubview(MenuView(n: 0, icon: NSImage(named: NSImage.Name("apps"))!, title: "Stats"))
self.menuView.addSubview(MenuView(n: 0, icon: NSImage(named: NSImage.Name("apps"))!, title: "Dashboard"))
self.navigationView.frame = NSRect(x: 0, y: 0, width: self.sidebarWidth, height: navigationHeight)
self.navigationView.wantsLayer = true
self.navigationView.addSubview(self.makeButton(4, title: LocalizedString("Open Activity Monitor"), image: "chart", action: #selector(openActivityMonitor)))
self.navigationView.addSubview(self.makeButton(4, title: LocalizedString("Open application settings"), image: "settings", action: #selector(openSettings)))
self.navigationView.addSubview(self.makeButton(3, title: LocalizedString("Report a bug"), image: "bug", action: #selector(reportBug)))
self.navigationView.addSubview(self.makeButton(2, title: LocalizedString("Support app"), image: "donate", action: #selector(donate)))
self.navigationView.addSubview(self.makeButton(1, title: LocalizedString("Close application"), image: "power", action: #selector(closeApp)))
@@ -160,7 +161,7 @@ private class SettingsView: NSView {
self.addSubview(self.navigationView)
self.addSubview(self.mainView)
self.openMenu("Stats")
self.openMenu("Dashboard")
}
required init?(coder: NSCoder) {
@@ -197,18 +198,20 @@ private class SettingsView: NSView {
self.menuView.addSubview(menu)
}
self.modules = list
// self.openMenu("CPU")
}
@objc private func menuCallback(_ notification: Notification) {
if let title = notification.userInfo?["module"] as? String {
var view: NSView = self.applicationSettings
var view: NSView = NSView()
let detectedModule = self.modules?.pointee.first{ $0.config.name == title }
if detectedModule != nil {
if let v = detectedModule?.settings {
if let detectedModule = self.modules?.pointee.first(where: { $0.config.name == title }) {
if let v = detectedModule.settings {
view = v
}
} else if title == "Dashboard" {
view = self.dashboard
} else if title == "settings" {
view = self.settings
}
self.mainView.subviews.forEach{ $0.removeFromSuperview() }
@@ -248,14 +251,8 @@ private class SettingsView: NSView {
return button
}
@objc private func openActivityMonitor(_ sender: Any) {
NSWorkspace.shared.launchApplication(
withBundleIdentifier: "com.apple.ActivityMonitor",
options: [.default],
additionalEventParamDescriptor: nil,
launchIdentifier: nil
)
self.window?.setIsVisible(false)
@objc private func openSettings(_ sender: Any) {
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": "settings"])
}
@objc private func reportBug(_ sender: Any) {
@@ -322,7 +319,7 @@ private class MenuView: NSView {
}
public func activate() {
NotificationCenter.default.post(name: .openSettingsView, object: nil, userInfo: ["module": self.title])
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": self.title])
self.layer?.backgroundColor = .init(gray: 0.1, alpha: 0.4)
self.active = true
}

View File

@@ -37,18 +37,21 @@ public struct os_s {
}
public struct cpu_s {
public let physicalCores: Int8
public let logicalCores: Int8
public let name: String
public var name: String? = nil
public var physicalCores: Int8? = nil
public var logicalCores: Int8? = nil
}
public struct dimm_s {
public var bank: Int? = nil
public var channel: String? = nil
public var type: String? = nil
public var size: String? = nil
public var speed: String? = nil
}
public struct ram_s {
public var active: Double
public var inactive: Double
public var wired: Double
public var compressed: Double
public var total: Double
public var used: Double
public var dimms: [dimm_s] = []
}
public struct gpu_s {
@@ -69,13 +72,18 @@ public struct info_s {
}
public struct device_s {
public var model: model_s = model_s(name: LocalizedString("Unknown"), year: 2020, type: .unknown)
public var model: model_s = model_s(name: LocalizedString("Unknown"), year: Calendar.current.component(.year, from: Date()), type: .unknown)
public var modelIdentifier: String? = nil
public var serialNumber: String? = nil
public var bootDate: Date? = nil
public var os: os_s? = nil
public var info: info_s? = info_s()
public var info: info_s = info_s()
}
public class SystemKit {
public static let shared = SystemKit()
public var device: device_s = device_s()
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SystemKit")
@@ -88,9 +96,15 @@ public class SystemKit {
os_log(.error, log: self.log, "unknown device %s", modelName)
}
}
if let id = self.modelID() {
self.device.modelIdentifier = id
let (modelID, serialNumber) = self.modelAndSerialNumber()
if modelID != nil {
self.device.modelIdentifier = modelID
}
if serialNumber != nil {
self.device.serialNumber = serialNumber
}
self.device.bootDate = self.bootDate()
let procInfo = ProcessInfo()
let systemVersion = procInfo.operatingSystemVersion
@@ -104,10 +118,10 @@ public class SystemKit {
let version = "\(systemVersion.majorVersion).\(systemVersion.minorVersion)"
self.device.os = os_s(name: osDict[version] ?? LocalizedString("Unknown"), version: systemVersion, build: build)
self.device.info?.cpu = self.getCPUInfo()
self.device.info?.ram = self.getRamInfo()
self.device.info?.gpu = self.getGPUInfo()
self.device.info?.disk = self.getDiskInfo()
self.device.info.cpu = self.getCPUInfo()
self.device.info.ram = self.getRamInfo()
self.device.info.gpu = self.getGPUInfo()
self.device.info.disk = self.getDiskInfo()
}
public func modelName() -> String? {
@@ -128,18 +142,40 @@ public class SystemKit {
return nil
}
func modelID() -> String? {
func modelAndSerialNumber() -> (String?, String?) {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
var modelIdentifier: String?
if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data {
modelIdentifier = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters)
}
var serialNumber: String?
if let serialString = IORegistryEntryCreateCFProperty(service, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String {
serialNumber = serialString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
IOObjectRelease(service)
return modelIdentifier
return (modelIdentifier, serialNumber)
}
func bootDate() -> Date? {
var mib = [CTL_KERN, KERN_BOOTTIME]
var bootTime = timeval()
var bootTimeSize = MemoryLayout<timeval>.size
let result = sysctl(&mib, UInt32(mib.count), &bootTime, &bootTimeSize, nil, 0)
if result == KERN_SUCCESS {
return Date(timeIntervalSince1970: Double(bootTime.tv_sec) + Double(bootTime.tv_usec) / 1_000_000.0)
}
os_log(.error, log: self.log, "error get boot time: %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
private func getCPUInfo() -> cpu_s? {
var cpu = cpu_s()
var sizeOfName = 0
sysctlbyname("machdep.cpu.brand_string", nil, &sizeOfName, nil, 0)
var nameCharts = [CChar](repeating: 0, count: sizeOfName)
@@ -150,6 +186,8 @@ public class SystemKit {
name = name.replacingOccurrences(of: "(R)", with: "")
name = name.replacingOccurrences(of: "CPU", with: "")
name = name.replacingOccurrences(of: " @ ", with: "")
cpu.name = name
}
var size = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
@@ -162,13 +200,16 @@ public class SystemKit {
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &size)
}
if result == KERN_SUCCESS {
let data = hostInfo.move()
return cpu_s(physicalCores: Int8(data.physical_cpu), logicalCores: Int8(data.logical_cpu), name: name)
if result != KERN_SUCCESS {
os_log(.error, log: self.log, "read cores number: %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
os_log(.error, log: self.log, "hostInfo.withMemoryRebound(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
let data = hostInfo.move()
cpu.physicalCores = Int8(data.physical_cpu)
cpu.logicalCores = Int8(data.logical_cpu)
return cpu
}
private func getGPUInfo() -> [gpu_s]? {
@@ -256,54 +297,71 @@ public class SystemKit {
}
public func getRamInfo() -> ram_s? {
var vmStats = host_basic_info()
var count = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
var totalSize: Double = 0
let task = Process()
task.launchPath = "/usr/sbin/system_profiler"
task.arguments = ["SPMemoryDataType", "-json"]
var result: kern_return_t = withUnsafeMutablePointer(to: &vmStats) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count)
}
}
let outputPipe = Pipe()
let errorPipe = Pipe()
if result == KERN_SUCCESS {
totalSize = Double(vmStats.max_mem)
} else {
os_log(.error, log: self.log, "host_basic_info(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let error {
os_log(.error, log: log, "system_profiler SPMemoryDataType: %s", "\(error.localizedDescription)")
return nil
}
var pageSize: vm_size_t = 0
result = withUnsafeMutablePointer(to: &pageSize) { (size) -> kern_return_t in
host_page_size(mach_host_self(), size)
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
if output.isEmpty {
return nil
}
var stats = vm_statistics64()
count = UInt32(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
result = withUnsafeMutablePointer(to: &stats) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
let data = Data(output.utf8)
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
var ram: ram_s = ram_s()
if let obj = json["SPMemoryDataType"] as? [[String:Any]], obj.count > 0 {
if let items = obj[0]["_items"] as? [[String: Any]], items.count > 0 {
for i in 0..<items.count {
let item = items[i]
if item["dimm_size"] as? String == "empty" {
continue
}
var dimm: dimm_s = dimm_s()
dimm.type = item["dimm_type"] as? String
dimm.speed = item["dimm_speed"] as? String
dimm.size = item["dimm_size"] as? String
if let nameValue = item["_name"] as? String {
let arr = nameValue.split(separator: "/")
if arr.indices.contains(0) {
dimm.bank = Int(arr[0].filter("0123456789.".contains))
}
if arr.indices.contains(1) {
dimm.channel = arr[1].split(separator: "-")[0].replacingOccurrences(of: "Channel", with: "")
}
}
ram.dimms.append(dimm)
}
}
}
return ram
}
} catch let error as NSError {
os_log(.error, log: self.log, "error to parse system_profiler SPMemoryDataType: %s", error.localizedDescription)
return nil
}
if result == KERN_SUCCESS {
let active = Double(stats.active_count) * Double(vm_page_size)
let inactive = Double(stats.inactive_count) * Double(vm_page_size)
let wired = Double(stats.wire_count) * Double(vm_page_size)
let compressed = Double(stats.compressor_page_count) * Double(vm_page_size)
return ram_s(
active: active,
inactive: inactive,
wired: wired,
compressed: compressed,
total: totalSize,
used: active + wired + compressed
)
}
os_log(.error, log: self.log, "host_statistics64(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}

View File

@@ -348,7 +348,20 @@ public extension NSView {
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
rowTitle.textColor = .textColor
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-26)/2, width: 50, height: 26))
let select: NSPopUpButton = SelectView(action: action, items: items, selected: selected)
select.sizeToFit()
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
row.addSubview(select)
row.addSubview(rowTitle)
return row
}
func SelectView(action: Selector, items: [KeyValue_t], selected: String) -> NSPopUpButton {
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 50, height: 26))
select.target = self
select.action = action
@@ -365,24 +378,17 @@ public extension NSView {
}
}
}
select.menu = menu
select.sizeToFit()
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
row.addSubview(select)
row.addSubview(rowTitle)
return row
return select
}
}
public extension Notification.Name {
static let toggleSettings = Notification.Name("toggleSettings")
static let toggleModule = Notification.Name("toggleModule")
static let openSettingsView = Notification.Name("openSettingsView")
static let openModuleSettings = Notification.Name("openModuleSettings")
static let settingsAppear = Notification.Name("settingsAppear")
static let switchWidget = Notification.Name("switchWidget")
static let checkForUpdates = Notification.Name("checkForUpdates")
static let changeCronInterval = Notification.Name("changeCronInterval")