From 72e38f426cfa376930073d4c7c6952f47ceda08e Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Sat, 30 Dec 2023 18:08:41 +0100 Subject: [PATCH] feat: added portal to the Clock module (#1748) --- Kit/extensions.swift | 26 +++++-- Modules/Clock/main.swift | 11 ++- Modules/Clock/portal.swift | 125 ++++++++++++++++++++++++++++++++ Stats.xcodeproj/project.pbxproj | 4 + 4 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 Modules/Clock/portal.swift diff --git a/Kit/extensions.swift b/Kit/extensions.swift index 78a13dcd..e966f8a2 100644 --- a/Kit/extensions.swift +++ b/Kit/extensions.swift @@ -484,19 +484,26 @@ public class ScrollableStackView: NSView { private let clipView: NSClipView = NSClipView() private let scrollView: NSScrollView = NSScrollView() - public override init(frame: NSRect) { + public init(frame: NSRect = NSRect.zero, orientation: NSUserInterfaceLayoutOrientation = .vertical) { super.init(frame: frame) self.clipView.drawsBackground = false - self.stackView.orientation = .vertical + self.stackView.orientation = orientation self.stackView.translatesAutoresizingMaskIntoConstraints = false self.scrollView.translatesAutoresizingMaskIntoConstraints = false - self.scrollView.hasVerticalScroller = true - self.scrollView.hasHorizontalScroller = false - self.scrollView.autohidesScrollers = true - self.scrollView.horizontalScrollElasticity = .none + if orientation == .vertical { + self.scrollView.hasVerticalScroller = true + self.scrollView.hasHorizontalScroller = false + self.scrollView.autohidesScrollers = true + self.scrollView.horizontalScrollElasticity = .none + } else { + self.scrollView.hasVerticalScroller = false + self.scrollView.hasHorizontalScroller = true + self.scrollView.autohidesScrollers = true + self.scrollView.verticalScrollElasticity = .none + } self.scrollView.drawsBackground = false self.scrollView.contentView = self.clipView self.scrollView.documentView = self.stackView @@ -510,9 +517,14 @@ public class ScrollableStackView: NSView { self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor), self.stackView.leftAnchor.constraint(equalTo: self.clipView.leftAnchor), - self.stackView.rightAnchor.constraint(equalTo: self.clipView.rightAnchor), self.stackView.topAnchor.constraint(equalTo: self.clipView.topAnchor) ]) + + if orientation == .vertical { + self.stackView.rightAnchor.constraint(equalTo: self.clipView.rightAnchor).isActive = true + } else { + self.stackView.bottomAnchor.constraint(equalTo: self.clipView.bottomAnchor).isActive = true + } } required public init?(coder: NSCoder) { diff --git a/Modules/Clock/main.swift b/Modules/Clock/main.swift index d21bec6b..4d7fc693 100644 --- a/Modules/Clock/main.swift +++ b/Modules/Clock/main.swift @@ -55,11 +55,12 @@ internal class ClockReader: Reader { public class Clock: Module { private let popupView: Popup = Popup() + private let portalView: Portal private let settingsView: Settings = Settings() private var reader: ClockReader = ClockReader(.clock) - private var list: [Clock_t] { + static var list: [Clock_t] { if let objects = Store.shared.data(key: "\(Clock.title)_list") { let decoder = JSONDecoder() if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [Clock_t] { @@ -70,9 +71,12 @@ public class Clock: Module { } public init() { + self.portalView = Portal("Clock", list: Clock.list) + super.init( popup: self.popupView, - settings: self.settingsView + settings: self.settingsView, + portal: self.portalView ) guard self.available else { return } @@ -88,7 +92,7 @@ public class Clock: Module { } private func callback(_ value: Date) { - var clocks: [Clock_t] = self.list + var clocks: [Clock_t] = Clock.list var widgetList: [Stack_t] = [] for (i, c) in clocks.enumerated() { @@ -100,6 +104,7 @@ public class Clock: Module { DispatchQueue.main.async(execute: { self.popupView.callback(clocks) + self.portalView.callback(clocks) }) self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in diff --git a/Modules/Clock/portal.swift b/Modules/Clock/portal.swift new file mode 100644 index 00000000..5980074a --- /dev/null +++ b/Modules/Clock/portal.swift @@ -0,0 +1,125 @@ +// +// portal.swift +// Clock +// +// Created by Serhiy Mytrovtsiy on 28/12/2023 +// Using Swift 5.0 +// Running on macOS 14.2 +// +// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved. +// + +import AppKit +import Kit + +public class Portal: NSStackView, Portal_p { + public var name: String + + private var initialized: Bool = false + + private var oneContainer: NSGridView = NSGridView() + private var multiplyContainer: ScrollableStackView = ScrollableStackView(orientation: .horizontal) + + init(_ name: String, list: [Clock_t]) { + self.name = name + + super.init(frame: NSRect.zero) + + self.wantsLayer = true + self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor + self.layer?.cornerRadius = 3 + + self.orientation = .vertical + self.distribution = .fillEqually + self.spacing = Constants.Popup.spacing*2 + self.edgeInsets = NSEdgeInsets( + top: Constants.Popup.spacing*2, + left: Constants.Popup.spacing*2, + bottom: Constants.Popup.spacing*2, + right: Constants.Popup.spacing*2 + ) + self.addArrangedSubview(PortalHeader(name)) + + self.oneContainer.rowSpacing = 0 + self.oneContainer.yPlacement = .center + self.oneContainer.xPlacement = .center + + self.addArrangedSubview(self.oneContainer) + self.addArrangedSubview(self.multiplyContainer) + + self.callback(list) + + self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func updateLayer() { + self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor + } + + public func callback(_ list: [Clock_t]) { + let list = list.filter({ $0.popupState }) + + if (self.window?.isVisible ?? false) || !self.initialized { + if list.count == 1, let c = list.first { + self.loadOne(c) + } else { + self.loadMultiply(list) + } + self.initialized = true + } + } + + private func loadOne(_ clock: Clock_t) { + self.addArrangedSubview(self.oneContainer) + self.multiplyContainer.removeFromSuperview() + + let views = self.oneContainer.subviews.compactMap{ $0 as? ClockChart } + if let view = views.first(where: { $0.identifier?.rawValue == clock.id }) { + if let value = clock.value { + view.setValue(value.convertToTimeZone(TimeZone(fromUTC: clock.tz))) + } + } else { + self.oneContainer.addRow(with: [self.clockView(clock)]) + } + } + + private func loadMultiply(_ list: [Clock_t]) { + self.addArrangedSubview(self.multiplyContainer) + self.oneContainer.removeFromSuperview() + + let sorted = list.sorted(by: { $0.popupIndex < $1.popupIndex }) + var views = self.multiplyContainer.stackView.subviews.compactMap{ $0 as? ClockChart } + + if sorted.count < views.count && !views.isEmpty { + views.forEach{ $0.removeFromSuperview() } + views = [] + } + + sorted.forEach { (c: Clock_t) in + if let view = views.first(where: { $0.identifier?.rawValue == c.id }) { + if let value = c.value { + view.setValue(value.convertToTimeZone(TimeZone(fromUTC: c.tz))) + } + } else { + self.multiplyContainer.stackView.addArrangedSubview(clockView(c)) + } + } + } + + private func clockView(_ clock: Clock_t) -> ClockChart { + let view = ClockChart(frame: NSRect(x: 0, y: 0, width: 57, height: 57)) + view.widthAnchor.constraint(equalToConstant: view.frame.width).isActive = true + view.heightAnchor.constraint(equalToConstant: view.frame.height).isActive = true + view.identifier = NSUserInterfaceItemIdentifier(clock.id) + + if let value = clock.value { + view.setValue(value.convertToTimeZone(TimeZone(fromUTC: clock.tz))) + } + + return view + } +} diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index a13d5a0a..5d7e8ec4 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C044F792B3DE6F3005F6951 /* portal.swift */; }; 5C0A2A8A292A5B4D009B4C1F /* SMJobBlessUtil.py in Resources */ = {isa = PBXBuildFile; fileRef = 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */; }; 5C21D80B296C7B81005BA16D /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C21D80A296C7B81005BA16D /* CombinedView.swift */; }; 5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; }; @@ -385,6 +386,7 @@ 40BE2B202745D63800AE9396 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 47665544298DC92F00F7B709 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; 4921436D25319699000A1C47 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; + 5C044F792B3DE6F3005F6951 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = SMJobBlessUtil.py; sourceTree = ""; }; 5C21D80A296C7B81005BA16D /* CombinedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = ""; }; 5C22299D29CCB3C400F00E69 /* Clock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Clock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -696,6 +698,7 @@ children = ( 5C2229A829CCB41900F00E69 /* main.swift */, 5C2229B729CE3F3300F00E69 /* popup.swift */, + 5C044F792B3DE6F3005F6951 /* portal.swift */, 5C2229AE29CDC08700F00E69 /* settings.swift */, 5C2229AA29CCB53E00F00E69 /* config.plist */, ); @@ -1697,6 +1700,7 @@ buildActionMask = 2147483647; files = ( 5C2229AF29CDC08700F00E69 /* settings.swift in Sources */, + 5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */, 5C2229B829CE3F3300F00E69 /* popup.swift in Sources */, 5C2229A929CCB41900F00E69 /* main.swift in Sources */, );