From 5a34aea3811a1a382192534e3161de7679c16a54 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 12 Apr 2024 20:28:02 +0200 Subject: [PATCH] feat: redesigned App Settings to new style (Ventura-like) --- Kit/extensions.swift | 34 +- Kit/helpers.swift | 93 ++++ .../cancel.imageset/Contents.json | 27 ++ .../baseline_cancel_black_24pt_1x.png | Bin 0 -> 223 bytes .../baseline_cancel_black_24pt_2x.png | Bin 0 -> 390 bytes .../baseline_cancel_black_24pt_3x.png | Bin 0 -> 541 bytes .../export.imageset/Contents.json | 27 ++ .../baseline_ios_share_black_24pt_1x.png | Bin 0 -> 167 bytes .../baseline_ios_share_black_24pt_2x.png | Bin 0 -> 266 bytes .../baseline_ios_share_black_24pt_3x.png | Bin 0 -> 362 bytes .../import.imageset/Contents.json | 27 ++ .../baseline_download_black_24pt_1x.png | Bin 0 -> 116 bytes .../baseline_download_black_24pt_2x.png | Bin 0 -> 156 bytes .../baseline_download_black_24pt_3x.png | Bin 0 -> 185 bytes .../trash.imageset/Contents.json | 27 ++ .../baseline_delete_black_24pt_1x.png | Bin 0 -> 113 bytes .../baseline_delete_black_24pt_2x.png | Bin 0 -> 149 bytes .../baseline_delete_black_24pt_3x.png | Bin 0 -> 193 bytes Stats/Views/AppSettings.swift | 439 ++++++------------ Stats/Views/CombinedView.swift | 80 +++- 20 files changed, 446 insertions(+), 308 deletions(-) create mode 100644 Stats/Supporting Files/Assets.xcassets/cancel.imageset/Contents.json create mode 100644 Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_1x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_2x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_3x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/export.imageset/Contents.json create mode 100644 Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_1x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_2x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_3x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/import.imageset/Contents.json create mode 100644 Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_1x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_2x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_3x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/trash.imageset/Contents.json create mode 100644 Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_1x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_2x.png create mode 100644 Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_3x.png diff --git a/Kit/extensions.swift b/Kit/extensions.swift index bdfa816c..ff82a6b9 100644 --- a/Kit/extensions.swift +++ b/Kit/extensions.swift @@ -451,6 +451,38 @@ public extension NSView { return select } + + func switchView(action: Selector, state: Bool) -> NSSwitch { + let s = NSSwitch() + s.controlSize = .mini + s.state = state ? .on : .off + s.action = action + s.target = self + return s + } + + func buttonIconView(_ action: Selector, icon: NSImage) -> NSButton { + let button = NSButton() + button.heightAnchor.constraint(equalToConstant: 22).isActive = true + button.bezelStyle = .regularSquare + button.translatesAutoresizingMaskIntoConstraints = false + button.imageScaling = .scaleNone + button.image = icon + button.contentTintColor = .labelColor + button.isBordered = false + button.action = action + button.target = self + button.focusRingType = .none + return button + } + + func textView(_ value: String) -> NSTextField { + let field: NSTextField = TextView() + field.font = NSFont.systemFont(ofSize: 13, weight: .regular) + field.stringValue = value + field.isSelectable = true + return field + } } public class NSButtonWithPadding: NSButton { @@ -466,7 +498,7 @@ public class NSButtonWithPadding: NSButton { } public class TextView: NSTextField { - public override init(frame: NSRect) { + public override init(frame: NSRect = .zero) { super.init(frame: frame) self.isEditable = false diff --git a/Kit/helpers.swift b/Kit/helpers.swift index 933eb517..df151549 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -8,6 +8,7 @@ // // Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. // +// swiftlint:disable file_length import Cocoa import ServiceManagement @@ -1348,3 +1349,95 @@ var isDarkMode: Bool { return false } } + +public class PreferencesSection: NSStackView { + public init(_ components: [PreferencesRow] = []) { + super.init(frame: .zero) + + self.orientation = .vertical + + self.wantsLayer = true + self.layer?.cornerRadius = 5 + self.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor + self.layer?.borderWidth = 1 + self.layer?.backgroundColor = NSColor.quaternaryLabelColor.withAlphaComponent(0.025).cgColor + + self.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin/2, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin/2, + right: Constants.Settings.margin + ) + self.spacing = Constants.Settings.margin/2 + + for item in components { + self.add(item) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func updateLayer() { + self.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor + self.layer?.backgroundColor = NSColor.quaternaryLabelColor.withAlphaComponent(0.025).cgColor + } + + public func add(_ view: NSView) { + if !self.subviews.isEmpty { + self.addArrangedSubview(PreferencesSeparator()) + } + self.addArrangedSubview(view) + } + + public func toggleVisibility(_ at: Int, newState: Bool) { + for i in self.subviews.indices where i/2 == at && Double(i).remainder(dividingBy: 2) == 0 { + self.subviews[i-1].isHidden = !newState + self.subviews[i].isHidden = !newState + } + } +} + +public class PreferencesSeparator: NSView { + public init() { + super.init(frame: .zero) + self.wantsLayer = true + self.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor + self.heightAnchor.constraint(equalToConstant: 1).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func updateLayer() { + self.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor + } +} + +public class PreferencesRow: NSStackView { + public init(_ title: String? = nil, component: NSView) { + super.init(frame: .zero) + + self.orientation = .horizontal + self.distribution = .fill + self.alignment = .top + self.edgeInsets = NSEdgeInsets(top: Constants.Settings.margin/2, left: 0, bottom: Constants.Settings.margin/2, right: 0) + self.spacing = 0 + + let field: NSTextField = TextView() + field.font = NSFont.systemFont(ofSize: 13, weight: .regular) + if let title { + field.stringValue = title + self.addArrangedSubview(field) + } + + self.addArrangedSubview(NSView()) + self.addArrangedSubview(component) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Stats/Supporting Files/Assets.xcassets/cancel.imageset/Contents.json b/Stats/Supporting Files/Assets.xcassets/cancel.imageset/Contents.json new file mode 100644 index 00000000..14c95d8c --- /dev/null +++ b/Stats/Supporting Files/Assets.xcassets/cancel.imageset/Contents.json @@ -0,0 +1,27 @@ +{ + "images": [ + { + "filename": "baseline_cancel_black_24pt_1x.png", + "idiom": "universal", + "scale": "1x", + "size": "24x24" + }, + { + "filename": "baseline_cancel_black_24pt_2x.png", + "idiom": "universal", + "scale": "2x", + "size": "24x24" + }, + { + "filename": "baseline_cancel_black_24pt_3x.png", + "idiom": "universal", + "scale": "3x", + "size": "24x24" + } + ], + "info": { + "author": "xcode", + "template-rendering-intent": "template", + "version": 1 + } +} \ No newline at end of file diff --git a/Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_1x.png b/Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..f01769213798bb3af8f0e032e0fed0fa448851c8 GIT binary patch literal 223 zcmV<503iQ~P)KYA1ru$`HnbuyCfWd3Er2~Jps>4UFuTl!g!jlF9pK-qq@`zOV`HYL zrTm81EZC?P>i2L#m%y$%id6WF?@AKJ-qa;n<+G1yjE4-G(f3jXq49_Y)S(y@p#51B zgyD3jI+U<;XG0LCQ&tDYxMd2$dTDQ0V%)Mm5X6zhO^ZLP_{WJyBV+h7##{V=vYzNK Z)(bCO3czG41zrFE002ovPDHLkV1g&UT|)o> literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_2x.png b/Stats/Supporting Files/Assets.xcassets/cancel.imageset/baseline_cancel_black_24pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9f7ca9b3029bb2d5f3bd09b02c07646bc6743978 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(F#3ABIEGX(zBz9wbTB}qHF5HV z49(5_W{WDyCdsC-Z)iSqVH5wf13gT8dmjBL-Sygc-I59Q&t6Y5|Ndpyu3a%+2CjX& zd^;Qj^ehs(Wr9^Q_VTcv3s_>zm6EYXaN3Fmv$S*!os?PcD5jUQ{8?jn?LzrnH({EyoN`P*d(n(sh4q4Y$C;&P$SQ&v?lWQ`jPMz@3hjdXjx+b~m-vu%-Y$cI zp+SJ{S9z@LbP-Dr#j9~zyt0@Qrf?dZ3W>*qGKo^h!h-3!r5&{N;042QVfx!XZ-cDc5_wWM< zf9mV|@%el{pUKLn9>{P(j4i&hJYRF7#L|A+Qa)={20xYs4T8LvUj#x|^q>zP4v4PWeC?N|~V(0ScenJ-R>Ef4Q z=YAaZNV_?|IVEIe%DKKtDP*P6IgWB!xg-9i04-$YS_GK99P%Y{Oj-mP>4pRyWFj3B z+#n-Gn5^0`&gbMfpqpr%!{Q*N~I_RZxv&f0o_}qpKipMw($9pu(uO=phb9GLNIsV+(aLM9Dsq zVH|@P>aGw6Bk9MnyFy(CaWIm89J>ta?hpqf>Bq6VLtO)LK9XV_g9hr}5a%N)#xZz9 zT?cVKl42Z#&JSc9$iHIx{72d!^NM5M|0pW+R}K4R)n8VB+qpvn000n%VgG-P!6Pa# z$r)oq+_3~^2a#(aw0HLD91fkHJLLuSmrj971+P=$R0CEd?@SSlSvqqlgF2JUFGjs) f;T{WkO#uK5VgYVsJd5JG00000NkvXXu0mjfGWp># literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/export.imageset/Contents.json b/Stats/Supporting Files/Assets.xcassets/export.imageset/Contents.json new file mode 100644 index 00000000..c438d6ea --- /dev/null +++ b/Stats/Supporting Files/Assets.xcassets/export.imageset/Contents.json @@ -0,0 +1,27 @@ +{ + "images": [ + { + "filename": "baseline_ios_share_black_24pt_1x.png", + "idiom": "universal", + "scale": "1x", + "size": "24x24" + }, + { + "filename": "baseline_ios_share_black_24pt_2x.png", + "idiom": "universal", + "scale": "2x", + "size": "24x24" + }, + { + "filename": "baseline_ios_share_black_24pt_3x.png", + "idiom": "universal", + "scale": "3x", + "size": "24x24" + } + ], + "info": { + "author": "xcode", + "template-rendering-intent": "template", + "version": 1 + } +} \ No newline at end of file diff --git a/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_1x.png b/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7f7ed2df9802d3d79dca911b321ab4c31b324fb3 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iJWm(L5R22v2@8Z)uscnf@}K=s z)uk!IYgw24;XiDz@W*-1w5kP%G!)tFM}ur$o)$Bcu`ZyvEQG|9CV?f>2P Q3TQKfr>mdKI;Vst05@toe*gdg literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_2x.png b/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9531a62b80cfce790a1fa142cb74be072ebcc4fd GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Di=HlyArXh)Uial|G7xZhSkC`Z zX|JTVpR=Xpj)}h0RZ?Oyeh5x5^zrS;-TtMv@e<2}`)-bc=K3Bsk7o9}_59R0Vw3Nu z^Lg2ks(U>WpBv>IUvN%yF?=E`+jQX}+o3mdOnht&;)&b1E&0+IEQ)?_Heg-UvzZ}W zD{u4nTJ9Nfmmi!g`B_%Dz@$N8i`1eh%Z*VEAAUR3a;*5?_ln7Rk( OIR;NxKbLh*2~7ZAnrmYK literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_3x.png b/Stats/Supporting Files/Assets.xcassets/export.imageset/baseline_ios_share_black_24pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b1043806ee1f486fbedcf42b611590595ad22e3b GIT binary patch literal 362 zcmV-w0hRuVP)a66$<^axx53kwS@7*T#D%Vd+$w`syKB;RDRWF`wR{xo95 zesM~a{*Wq;H>5Ym3sRHgoYdtwCG~Thk%l==NaGxJ(szz3X&Oh3)aK-P)cpr?eNx=w zgt*sFcZ4mH@*3p`fd&W-lJYL9IBn3Iq?~6jP8U>>)c-`wy4MALzI70NAP__l03g5t z32r@y!W2;Vs~ZwT2&9}1u7jtP3{e7}glheFy73Hr?UM3UjZM(5^@jR2K0=i$Dbt4x$zsitU#lWDpqoC8`sjn;0 O7zR&QKbLh*2~7Yruq4+2 literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_2x.png b/Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..19d7fc63f2bbae1847ce549ea1d4ee6ad7451d15 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtWKS2zkP61P=Qr{;Fz_64F#G-@ z;4g!SLQhT5t3CZHKYKqv6Af@+WEBAt%Nm$&d=}>`W;uS?pkMyXWA@KK4%*aVVLr5e lJb8|OJR_@(>;#pQtdqJA*$Mke@Bpo2@O1TaS?83{1OQ1QH{t*Q literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_3x.png b/Stats/Supporting Files/Assets.xcassets/import.imageset/baseline_download_black_24pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3aefc3b24650130d94b8804b8be43e0413b8ad4b GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!4W2HJAr*{oFEp|qaA06LSh8=a zx~dGz|1$-wckJc><-bqA>#5MlBIJQdWXV`2cA7kzqfof*$W5Vi?>V}-7}>`c%C?uu mSgGFVdQ&MBb@08le1NdN!< literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_2x.png b/Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9e5bc7a943d37cc5d6ab5c6b2624461a1b5963c4 GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D7*7|+kch)?FC64Opuod&@O5+2 z%416pcFQej^f<_;Ao%uIy>g_IRd87Sjy>i7gfBL;Z{Yjke)~Q%P%j7+a0bMDKFU9X t+rWOpm1WOoPZCk|`C7W?RMKac&AeYDWt-j~`LP|O(9_k=Wt~$(69A9nHMsx) literal 0 HcmV?d00001 diff --git a/Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_3x.png b/Stats/Supporting Files/Assets.xcassets/trash.imageset/baseline_delete_black_24pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..377038618440cebafc29c5ee2730dd26ba9a83b4 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawIy_w*Lo)8Yy>yVbK|#RfqGe!< z`mf1WUs-f7F!Ls`AA6Az@P9sAoSC+u;KZdXR_)uo%0lRQ;w8azxl8H}GcYo_$Y<<$ z&&a|dpy1E|?VkD&8-1 gJy|WQv?zWn;~pL6I>}fTkRus9UHx3vIVCg!0I#$`-T(jq literal 0 HcmV?d00001 diff --git a/Stats/Views/AppSettings.swift b/Stats/Views/AppSettings.swift index 7bc64231..b40c649e 100644 --- a/Stats/Views/AppSettings.swift +++ b/Stats/Views/AppSettings.swift @@ -35,58 +35,135 @@ class ApplicationSettings: NSStackView { set { Store.shared.set(key: "CombinedModules_popup", value: newValue) } } + private var importIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "square.and.arrow.down", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("import"))! + } + private var exportIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "square.and.arrow.up", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("export"))! + } + private var resetIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "trash", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("trash"))! + } + private var uninstallIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "xmark.circle", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("cancel"))! + } + + private var updateSelector: NSPopUpButton? + private var startAtLoginBtn: NSSwitch? + private var telemetryBtn: NSSwitch? + + private var combinedModulesView: PreferencesSection? + private var fanHelperView: PreferencesSection? + private let updateWindow: UpdateWindow = UpdateWindow() private let moduleSelector: ModuleSelectorView = ModuleSelectorView() - private var updateSelector: NSPopUpButton? - private var startAtLoginBtn: NSButton? - private var uninstallHelperButton: NSButton? - private var buttonsContainer: NSStackView? - private var telemetryBtn: NSButton? - - private var combinedModules: NSView? - private var combinedModulesSeparator: NSView? - - private var buttons: NSView? - private var buttonsSeparator: NSView? init() { super.init(frame: NSRect(x: 0, y: 0, width: Constants.Settings.width, height: Constants.Settings.height)) - self.translatesAutoresizingMaskIntoConstraints = false - let scrollView = ScrollableStackView() - scrollView.stackView.spacing = 0 - - let settings = self.settingsView() - let appSettings = self.appSettingsView() + let scrollView = ScrollableStackView(orientation: .vertical) + scrollView.stackView.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin, + right: Constants.Settings.margin + ) + scrollView.stackView.spacing = Constants.Settings.margin scrollView.stackView.addArrangedSubview(self.informationView()) - scrollView.stackView.addArrangedSubview(self.separatorView()) - scrollView.stackView.addArrangedSubview(settings) - scrollView.stackView.addArrangedSubview(self.separatorView()) - scrollView.stackView.addArrangedSubview(self.combinedModulesView()) - var separator = self.separatorView() - self.combinedModulesSeparator = separator - scrollView.stackView.addArrangedSubview(separator) - scrollView.stackView.addArrangedSubview(appSettings) - separator = self.separatorView() - self.buttonsSeparator = separator - scrollView.stackView.addArrangedSubview(separator) - scrollView.stackView.addArrangedSubview(self.buttonsView()) - self.toggleCombinedModulesView() - self.toggleButtonsView() + self.updateSelector = selectView( + action: #selector(self.toggleUpdateInterval), + items: AppUpdateIntervals, + selected: self.updateIntervalValue + ) + self.startAtLoginBtn = switchView( + action: #selector(self.toggleLaunchAtLogin), + state: LaunchAtLogin.isEnabled + ) + self.telemetryBtn = switchView( + action: #selector(self.toggleTelemetry), + state: telemetry.isEnabled + ) + + scrollView.stackView.addArrangedSubview(PreferencesSection([ + PreferencesRow(localizedString("Check for updates"), component: self.updateSelector!), + PreferencesRow(localizedString("Temperature"), component: selectView( + action: #selector(self.toggleTemperatureUnits), + items: TemperatureUnits, + selected: self.temperatureUnitsValue + )), + PreferencesRow(localizedString("Show icon in dock"), component: switchView( + action: #selector(self.toggleDock), + state: Store.shared.bool(key: "dockIcon", defaultValue: false) + )), + PreferencesRow(localizedString("Start at login"), component: self.startAtLoginBtn!), + PreferencesRow(localizedString("Share anonymous telemetry"), component: self.telemetryBtn!) + ])) + + self.combinedModulesView = PreferencesSection([ + PreferencesRow(localizedString("Combined modules"), component: switchView( + action: #selector(self.toggleCombinedModules), + state: self.combinedModulesState + )), + PreferencesRow(component: self.moduleSelector), + PreferencesRow(localizedString("Spacing"), component: selectView( + action: #selector(self.toggleCombinedModulesSpacing), + items: CombinedModulesSpacings, + selected: self.combinedModulesSpacing + )), + PreferencesRow(localizedString("Combined details"), component: switchView( + action: #selector(self.toggleCombinedModulesPopup), + state: self.combinedModulesPopup + )) + ]) + scrollView.stackView.addArrangedSubview(self.combinedModulesView!) + self.combinedModulesView?.toggleVisibility(1, newState: self.combinedModulesState) + self.combinedModulesView?.toggleVisibility(2, newState: self.combinedModulesState) + self.combinedModulesView?.toggleVisibility(3, newState: self.combinedModulesState) + + scrollView.stackView.addArrangedSubview(PreferencesSection([ + PreferencesRow( + localizedString("Import settings"), + component: buttonIconView(#selector(self.importSettings), icon: self.importIcon) + ), + PreferencesRow( + localizedString("Export settings"), + component: buttonIconView(#selector(self.exportSettings), icon: self.exportIcon) + ), + PreferencesRow( + localizedString("Reset settings"), + component: buttonIconView(#selector(self.resetSettings), icon: self.resetIcon) + ) + ])) + + self.fanHelperView = PreferencesSection([ + PreferencesRow( + localizedString("Uninstall fan helper"), + component: buttonIconView(#selector(self.uninstallHelper), icon: self.uninstallIcon) + ) + ]) + scrollView.stackView.addArrangedSubview(self.fanHelperView!) self.addArrangedSubview(scrollView) - if let settingsGrid = settings.subviews.first { - appSettings.subviews.first?.widthAnchor.constraint(equalTo: settingsGrid.widthAnchor).isActive = true - } - NotificationCenter.default.addObserver(self, selector: #selector(toggleUninstallHelperButton), name: .fanHelperState, object: nil) } - required public init?(coder: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -111,7 +188,7 @@ class ApplicationSettings: NSStackView { private func informationView() -> NSView { let view = NSStackView() - view.heightAnchor.constraint(equalToConstant: 240).isActive = true + view.heightAnchor.constraint(equalToConstant: 220).isActive = true view.orientation = .vertical view.distribution = .fill view.alignment = .centerY @@ -161,241 +238,6 @@ class ApplicationSettings: NSStackView { return view } - private func settingsView() -> NSView { - let view: NSStackView = NSStackView() - view.orientation = .vertical - view.edgeInsets = NSEdgeInsets( - top: Constants.Settings.margin, - left: Constants.Settings.margin, - bottom: Constants.Settings.margin, - right: Constants.Settings.margin - ) - view.spacing = 10 - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: self.frame.width - 15).isActive = true - - 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 - grid.setContentHuggingPriority(.defaultHigh, for: .horizontal) - grid.setContentHuggingPriority(.defaultHigh, for: .vertical) - - self.updateSelector = selectView( - action: #selector(self.toggleUpdateInterval), - items: AppUpdateIntervals, - selected: self.updateIntervalValue - ) - grid.addRow(with: [ - self.titleView(localizedString("Check for updates")), - self.updateSelector! - ]) - grid.addRow(with: [ - self.titleView(localizedString("Temperature")), - selectView( - action: #selector(self.toggleTemperatureUnits), - items: TemperatureUnits, - selected: self.temperatureUnitsValue - ) - ]) - grid.addRow(with: [NSGridCell.emptyContentView, self.toggleView( - action: #selector(self.toggleDock), - state: Store.shared.bool(key: "dockIcon", defaultValue: false), - text: localizedString("Show icon in dock") - )]) - self.startAtLoginBtn = self.toggleView( - action: #selector(self.toggleLaunchAtLogin), - state: LaunchAtLogin.isEnabled, - text: localizedString("Start at login") - ) - grid.addRow(with: [NSGridCell.emptyContentView, self.startAtLoginBtn!]) - - self.telemetryBtn = self.toggleView( - action: #selector(self.toggleTelemetry), - state: telemetry.isEnabled, - text: localizedString("Share anonymous telemetry") - ) - grid.addRow(with: [NSGridCell.emptyContentView, self.telemetryBtn!]) - - grid.addRow(with: [NSGridCell.emptyContentView, self.toggleView( - action: #selector(self.toggleCombinedModules), - state: self.combinedModulesState, - text: localizedString("Combined modules") - )]) - - view.addArrangedSubview(grid) - - return view - } - - private func combinedModulesView() -> NSView { - let view: NSStackView = NSStackView() - view.orientation = .vertical - view.edgeInsets = NSEdgeInsets( - top: Constants.Settings.margin, - left: Constants.Settings.margin, - bottom: Constants.Settings.margin, - right: Constants.Settings.margin - ) - view.spacing = 10 - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: self.frame.width - 15).isActive = true - - 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 - grid.setContentHuggingPriority(.defaultHigh, for: .horizontal) - grid.setContentHuggingPriority(.defaultHigh, for: .vertical) - - grid.addRow(with: [ - self.titleView(localizedString("Spacing")), - selectView( - action: #selector(self.toggleCombinedModulesSpacing), - items: CombinedModulesSpacings, - selected: self.combinedModulesSpacing - ) - ]) - grid.addRow(with: [NSGridCell.emptyContentView, self.toggleView( - action: #selector(self.toggleCombinedModulesPopup), - state: self.combinedModulesPopup, - text: localizedString("Combined details") - )]) - - view.addArrangedSubview(self.moduleSelector) - view.addArrangedSubview(grid) - - self.combinedModules = view - return view - } - - private func appSettingsView() -> NSView { - let view: NSStackView = NSStackView() - view.orientation = .vertical - view.edgeInsets = NSEdgeInsets( - top: Constants.Settings.margin, - left: Constants.Settings.margin, - bottom: Constants.Settings.margin, - right: Constants.Settings.margin - ) - view.spacing = 10 - view.translatesAutoresizingMaskIntoConstraints = false - view.widthAnchor.constraint(equalToConstant: self.frame.width - 15).isActive = true - - 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 - grid.setContentHuggingPriority(.defaultHigh, for: .horizontal) - grid.setContentHuggingPriority(.defaultHigh, for: .vertical) - - let importBtn: NSButton = NSButton() - importBtn.title = localizedString("Import") - importBtn.bezelStyle = .rounded - importBtn.target = self - importBtn.action = #selector(self.importSettings) - - let exportBtn: NSButton = NSButton() - exportBtn.title = localizedString("Export") - exportBtn.bezelStyle = .rounded - exportBtn.target = self - exportBtn.action = #selector(self.exportSettings) - - let resetBtn: NSButton = NSButton() - resetBtn.title = localizedString("Reset") - resetBtn.bezelStyle = .rounded - resetBtn.target = self - resetBtn.action = #selector(self.resetSettings) - resetBtn.widthAnchor.constraint(equalToConstant: 225).isActive = true - - grid.addRow(with: [ - self.titleView(localizedString("Settings")), - importBtn - ]) - grid.addRow(with: [NSGridCell.emptyContentView, exportBtn]) - grid.addRow(with: [NSGridCell.emptyContentView, resetBtn]) - - view.addArrangedSubview(grid) - - return view - } - - private func buttonsView() -> NSView { - let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 60)) - view.heightAnchor.constraint(equalToConstant: 60).isActive = true - - let row = NSStackView() - row.translatesAutoresizingMaskIntoConstraints = false - row.orientation = .vertical - row.alignment = .centerY - row.distribution = .fill - self.buttonsContainer = row - - let uninstall: NSButton = NSButton() - uninstall.title = localizedString("Uninstall fan helper") - uninstall.bezelStyle = .rounded - uninstall.target = self - uninstall.action = #selector(self.uninstallHelper) - self.uninstallHelperButton = uninstall - - row.addArrangedSubview(uninstall) - view.addSubview(row) - - NSLayoutConstraint.activate([ - row.centerXAnchor.constraint(equalTo: view.centerXAnchor), - row.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - - self.buttons = view - return view - } - - // MARK: - helpers - - private func separatorView() -> NSBox { - let view = NSBox() - view.boxType = .separator - return view - } - - 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, text: String) -> NSButton { - let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 20)) - button.setButtonType(.switch) - button.state = state ? .on : .off - button.title = text - button.action = action - button.isBordered = false - button.isTransparent = false - button.target = self - - return button - } - - private func toggleCombinedModulesView() { - self.combinedModules?.isHidden = !self.combinedModulesState - self.combinedModulesSeparator?.isHidden = !self.combinedModulesState - } - - private func toggleButtonsView() { - self.buttons?.isHidden = !SMCHelper.shared.isInstalled - self.buttonsSeparator?.isHidden = !SMCHelper.shared.isInstalled - } - // MARK: - actions @objc private func updateAction(_ sender: NSObject) { @@ -444,6 +286,29 @@ class ApplicationSettings: NSStackView { } } + @objc private func toggleTelemetry(_ sender: NSButton) { + telemetry.isEnabled = sender.state == NSControl.StateValue.on + } + + @objc private func toggleCombinedModules(_ sender: NSButton) { + self.combinedModulesState = sender.state == NSControl.StateValue.on + self.combinedModulesView?.toggleVisibility(1, newState: self.combinedModulesState) + self.combinedModulesView?.toggleVisibility(2, newState: self.combinedModulesState) + self.combinedModulesView?.toggleVisibility(3, newState: self.combinedModulesState) + NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: nil) + } + + @objc private func toggleCombinedModulesSpacing(_ sender: NSMenuItem) { + guard let key = sender.representedObject as? String else { return } + self.combinedModulesSpacing = key + NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: nil) + } + + @objc private func toggleCombinedModulesPopup(_ sender: NSButton) { + self.combinedModulesPopup = sender.state == NSControl.StateValue.on + NotificationCenter.default.post(name: .combinedModulesPopup, object: nil, userInfo: nil) + } + @objc private func importSettings(_ sender: NSObject) { let panel = NSOpenPanel() panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow))) @@ -486,40 +351,15 @@ class ApplicationSettings: NSStackView { } @objc private func toggleUninstallHelperButton(_ notification: Notification) { - guard let state = notification.userInfo?["state"] as? Bool, let v = self.uninstallHelperButton else { + guard let state = notification.userInfo?["state"] as? Bool, let v = self.fanHelperView else { return } - if state && v.superview == nil { - self.buttonsContainer?.addArrangedSubview(v) - } else if !state && v.superview != nil { - v.removeFromSuperview() - } + v.isHidden = !state } @objc private func uninstallHelper() { SMCHelper.shared.uninstall() } - - @objc private func toggleCombinedModules(_ sender: NSButton) { - self.combinedModulesState = sender.state == NSControl.StateValue.on - self.toggleCombinedModulesView() - NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: nil) - } - - @objc private func toggleCombinedModulesSpacing(_ sender: NSMenuItem) { - guard let key = sender.representedObject as? String else { return } - self.combinedModulesSpacing = key - NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: nil) - } - - @objc private func toggleTelemetry(_ sender: NSButton) { - telemetry.isEnabled = sender.state == NSControl.StateValue.on - } - - @objc private func toggleCombinedModulesPopup(_ sender: NSButton) { - self.combinedModulesPopup = sender.state == NSControl.StateValue.on - NotificationCenter.default.post(name: .combinedModulesPopup, object: nil, userInfo: nil) - } } private class ModuleSelectorView: NSStackView { @@ -562,6 +402,7 @@ private class ModuleSelectorView: NSStackView { background.setFrameSize(NSSize(width: w, height: self.frame.height)) self.widthAnchor.constraint(equalToConstant: w).isActive = true + self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true } required init?(coder: NSCoder) { diff --git a/Stats/Views/CombinedView.swift b/Stats/Views/CombinedView.swift index 564a625c..53c76938 100644 --- a/Stats/Views/CombinedView.swift +++ b/Stats/Views/CombinedView.swift @@ -12,9 +12,9 @@ import Cocoa import Kit -class CombinedView { +class CombinedView: NSObject, NSGestureRecognizerDelegate { private var menuBarItem: NSStatusItem? = nil - private var view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: Constants.Widget.height)) + private var view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height)) private var popup: PopupWindow? = nil private var status: Bool { @@ -24,7 +24,18 @@ class CombinedView { CGFloat(Int(Store.shared.string(key: "CombinedModules_spacing", defaultValue: "")) ?? 0) } - init() { + private var activeModules: [Module] { + modules.filter({ $0.enabled }).sorted(by: { $0.combinedPosition < $1.combinedPosition }) + } + + private var combinedModulesPopup: Bool { + get { Store.shared.bool(key: "CombinedModules_popup", defaultValue: true) } + set { Store.shared.set(key: "CombinedModules_popup", value: newValue) } + } + + override init() { + super.init() + modules.forEach { (m: Module) in m.menuBar.callback = { [weak self] in if let s = self?.status, s { @@ -43,6 +54,7 @@ class CombinedView { NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(listenForModuleRearrrange), name: .moduleRearrange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(listenCombinedModulesPopup), name: .combinedModulesPopup, object: nil) } deinit { @@ -54,9 +66,26 @@ class CombinedView { self.menuBarItem?.autosaveName = "CombinedModules" self.menuBarItem?.button?.addSubview(self.view) - self.menuBarItem?.button?.target = self - self.menuBarItem?.button?.action = #selector(self.togglePopup) - self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) + if !self.combinedModulesPopup { + self.activeModules.forEach { (m: Module) in + m.menuBar.widgets.forEach { w in + w.item.onClick = { + if let window = w.item.window { + NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ + "module": m.name, + "widget": w.type, + "origin": window.frame.origin, + "center": window.frame.width/2 + ]) + } + } + } + } + } else { + self.menuBarItem?.button?.target = self + self.menuBarItem?.button?.action = #selector(self.togglePopup) + self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) + } DispatchQueue.main.async(execute: { self.recalculate() @@ -64,6 +93,11 @@ class CombinedView { } public func disable() { + self.activeModules.forEach { (m: Module) in + m.menuBar.widgets.forEach { w in + w.item.onClick = nil + } + } if let item = self.menuBarItem { NSStatusBar.system.removeStatusItem(item) } @@ -75,7 +109,7 @@ class CombinedView { var w: CGFloat = 0 var i: Int = 0 - modules.filter({ $0.enabled }).sorted(by: { $0.combinedPosition < $1.combinedPosition }).forEach { (m: Module) in + self.activeModules.forEach { (m: Module) in self.view.addSubview(m.menuBar.view) self.view.subviews[i].setFrameOrigin(NSPoint(x: w, y: 0)) w += m.menuBar.view.frame.width + self.spacing @@ -88,7 +122,7 @@ class CombinedView { // call when popup appear/disappear private func visibilityCallback(_ state: Bool) {} - @objc private func togglePopup(_ sender: Any) { + @objc private func togglePopup(_ sender: NSButton) { guard let popup = self.popup, let item = self.menuBarItem, let window = item.button?.window else { return } let openedWindows = NSApplication.shared.windows.filter{ $0 is NSPanel } openedWindows.forEach{ $0.setIsVisible(false) } @@ -127,6 +161,36 @@ class CombinedView { @objc private func listenForModuleRearrrange() { self.recalculate() } + + @objc private func listenCombinedModulesPopup() { + if !self.combinedModulesPopup { + self.activeModules.forEach { (m: Module) in + m.menuBar.widgets.forEach { w in + w.item.onClick = { + if let window = w.item.window { + NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ + "module": m.name, + "widget": w.type, + "origin": window.frame.origin, + "center": window.frame.width/2 + ]) + } + } + } + } + self.menuBarItem?.button?.action = nil + } else { + self.activeModules.forEach { (m: Module) in + m.menuBar.widgets.forEach { w in + w.item.onClick = nil + } + } + + self.menuBarItem?.button?.target = self + self.menuBarItem?.button?.action = #selector(self.togglePopup) + self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) + } + } } private class Popup: NSStackView, Popup_p {