From fdfbb0aac42cd77540bbb2e95a6abeb4cb095ef4 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 1 Feb 2023 17:57:41 +0100 Subject: [PATCH] feat: added a view that allows rearranging modules in the OneView widget (#1084) --- Kit/helpers.swift | 4 +- Kit/module/module.swift | 14 +- Kit/types.swift | 1 + Stats/Views/AppSettings.swift | 258 +++++++++++++++++++++++++++++++--- Stats/Views/OneView.swift | 7 +- 5 files changed, 259 insertions(+), 25 deletions(-) diff --git a/Kit/helpers.swift b/Kit/helpers.swift index 958c9e6a..6ae7ebfc 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -1179,8 +1179,8 @@ internal func grayscaleImage(_ image: NSImage) -> NSImage? { return greyImage } -internal class ViewCopy: CALayer { - init(_ view: NSView) { +public class ViewCopy: CALayer { + public init(_ view: NSView) { super.init() guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return } diff --git a/Kit/module/module.swift b/Kit/module/module.swift index 6b868485..58e5df75 100644 --- a/Kit/module/module.swift +++ b/Kit/module/module.swift @@ -79,6 +79,18 @@ open class Module: Module_p { public var menuBar: MenuBar public var settings: Settings_p? = nil + public var name: String { + config.name + } + public var oneViewPosition: Int { + get { + Store.shared.int(key: "\(self.name)_position", defaultValue: 0) + } + set { + Store.shared.set(key: "\(self.name)_position", value: newValue) + } + } + private var settingsView: Settings_v? = nil private var popup: PopupWindow? = nil private var popupView: Popup_p? = nil @@ -88,7 +100,7 @@ open class Module: Module_p { private var pauseState: Bool { get { - return Store.shared.bool(key: "pause", defaultValue: false) + Store.shared.bool(key: "pause", defaultValue: false) } set { Store.shared.set(key: "pause", value: newValue) diff --git a/Kit/types.swift b/Kit/types.swift index a39936fe..acc2d47a 100644 --- a/Kit/types.swift +++ b/Kit/types.swift @@ -216,6 +216,7 @@ public extension Notification.Name { static let fanHelperState = Notification.Name("fanHelperState") static let toggleOneView = Notification.Name("toggleOneView") static let widgetRearrange = Notification.Name("widgetRearrange") + static let moduleRearrange = Notification.Name("moduleRearrange") static let pause = Notification.Name("pause") } diff --git a/Stats/Views/AppSettings.swift b/Stats/Views/AppSettings.swift index 83137615..fbaae7cf 100644 --- a/Stats/Views/AppSettings.swift +++ b/Stats/Views/AppSettings.swift @@ -26,7 +26,17 @@ class ApplicationSettings: NSStackView { } } + private var oneViewState: Bool { + get { + Store.shared.bool(key: "OneView", defaultValue: false) + } + set { + Store.shared.set(key: "OneView", value: newValue) + } + } + private let updateWindow: UpdateWindow = UpdateWindow() + private let moduleSelector: ModuleSelectorView = ModuleSelectorView() private var updateSelector: NSPopUpButton? private var startAtLoginBtn: NSButton? private var uninstallHelperButton: NSButton? @@ -126,14 +136,24 @@ class ApplicationSettings: NSStackView { } private func settingsView() -> NSView { - let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0)) + 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 = 20 + 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) @@ -167,28 +187,14 @@ class ApplicationSettings: NSStackView { grid.addRow(with: [NSGridCell.emptyContentView, self.startAtLoginBtn!]) grid.addRow(with: [NSGridCell.emptyContentView, self.toggleView( action: #selector(self.toggleOneView), - state: Store.shared.bool(key: "OneView", defaultValue: false), + state: self.oneViewState, text: localizedString("OneView") )]) - view.addSubview(grid) + view.addArrangedSubview(self.moduleSelector) + view.addArrangedSubview(grid) - var height: CGFloat = (CGFloat(grid.numberOfRows)-2) * grid.rowSpacing - for i in 0.. 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 + + grid.setContentHuggingPriority(.defaultHigh, for: .horizontal) + grid.setContentHuggingPriority(.defaultHigh, for: .vertical) + + grid.addRow(with: [self.moduleSelector]) + + view.addSubview(grid) + + var height: CGFloat = grid.rowSpacing + for i in 0.. NSBox { @@ -349,7 +391,181 @@ class ApplicationSettings: NSStackView { } @objc private func toggleOneView(_ sender: NSButton) { - Store.shared.set(key: "OneView", value: sender.state == NSControl.StateValue.on) + self.oneViewState = sender.state == NSControl.StateValue.on + self.moduleSelector.isHidden = !self.oneViewState NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: nil) } } + +private class ModuleSelectorView: NSStackView { + init() { + super.init(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height + (Constants.Settings.margin*2))) + + self.translatesAutoresizingMaskIntoConstraints = false + self.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin, + right: Constants.Settings.margin + ) + self.spacing = Constants.Settings.margin + + let background: NSVisualEffectView = { + let view = NSVisualEffectView(frame: NSRect.zero) + view.blendingMode = .withinWindow + view.material = .contentBackground + view.state = .active + view.wantsLayer = true + view.layer?.cornerRadius = 5 + return view + }() + + var w = self.spacing + modules.filter({ $0.available }).sorted(by: { $0.oneViewPosition < $1.oneViewPosition }).forEach { (m: Module) in + let v = ModulePreview(id: m.name, icon: m.config.icon) + self.addArrangedSubview(v) + w += v.frame.width + self.spacing + } + + self.addSubview(background, positioned: .below, relativeTo: .none) + + self.setFrameSize(NSSize(width: w, height: self.frame.height)) + background.setFrameSize(NSSize(width: w, height: self.frame.height)) + + self.widthAnchor.constraint(equalToConstant: w).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseDown(with event: NSEvent) { + let location = convert(event.locationInWindow, from: nil) + guard let targetIdx = self.views.firstIndex(where: { $0.hitTest(location) != nil }), + let window = self.window, self.views[targetIdx].identifier != nil else { + super.mouseDragged(with: event) + return + } + + let view = self.views[targetIdx] + let copy = ViewCopy(view) + copy.zPosition = 2 + copy.transform = CATransform3DMakeScale(0.9, 0.9, 1) + + // hide the original view, show the copy + view.subviews.forEach({ $0.isHidden = true }) + self.layer?.addSublayer(copy) + + // hide the copy view, show the original + defer { + copy.removeFromSuperlayer() + view.subviews.forEach({ $0.isHidden = false }) + } + + var newIdx = -1 + let originCenter = view.frame.midX + let originX = view.frame.origin.x + let p0 = convert(event.locationInWindow, from: nil).x + + window.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 1e6, mode: .eventTracking) { event, stop in + guard let event = event else { + stop.pointee = true + return + } + + if event.type == .leftMouseDragged { + let p1 = self.convert(event.locationInWindow, from: nil).x + let diff = p1 - p0 + + CATransaction.begin() + CATransaction.setDisableActions(true) + copy.frame.origin.x = originX + diff + CATransaction.commit() + + let reordered = self.views.map{ + (view: $0, x: $0 !== view ? $0.frame.midX : originCenter + diff) + }.sorted{ $0.x < $1.x }.map { $0.view } + + guard let nextIndex = reordered.firstIndex(of: view), + let prevIndex = self.views.firstIndex(of: view) else { + stop.pointee = true + return + } + + if nextIndex != prevIndex { + newIdx = nextIndex + view.removeFromSuperviewWithoutNeedingDisplay() + self.insertArrangedSubview(view, at: newIdx) + self.layoutSubtreeIfNeeded() + + for (i, v) in self.views(in: .leading).compactMap({$0 as? ModulePreview}).enumerated() { + if let m = modules.first(where: { $0.name == v.identifier?.rawValue }) { + m.oneViewPosition = i + } + } + } + } else { + if newIdx != -1, let view = self.views[newIdx] as? ModulePreview, let id = view.identifier?.rawValue { + NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: ["id": id]) + } + view.mouseUp(with: event) + stop.pointee = true + } + } + } +} + +internal class ModulePreview: NSStackView { + private let id: String + private let imageView: NSImageView + + public init(id: String, icon: NSImage?) { + self.id = id + self.imageView = NSImageView(frame: NSRect(origin: .zero, size: NSSize(width: Constants.Widget.height, height: Constants.Widget.height))) + + let size: CGSize = CGSize(width: Constants.Widget.height + (Constants.Widget.spacing * 2), height: Constants.Widget.height) + super.init(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height)) + + self.wantsLayer = true + self.layer?.cornerRadius = 2 + self.layer?.borderColor = NSColor(hexString: "#dddddd").cgColor + self.layer?.borderWidth = 1 + self.layer?.backgroundColor = NSColor.white.cgColor + + self.identifier = NSUserInterfaceItemIdentifier(rawValue: id) + self.toolTip = localizedString("Move module", id) + + self.orientation = .vertical + self.distribution = .fill + self.alignment = .centerY + self.spacing = 0 + + self.imageView.image = icon + + self.addArrangedSubview(self.imageView) + + self.addTrackingArea(NSTrackingArea( + rect: NSRect(x: 0, y: 0, width: size.width, height: size.height), + options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], + owner: self, + userInfo: nil + )) + + NSLayoutConstraint.activate([ + self.widthAnchor.constraint(equalToConstant: size.width), + self.heightAnchor.constraint(equalToConstant: size.height) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseEntered(with: NSEvent) { + NSCursor.pointingHand.set() + } + + override func mouseExited(with: NSEvent) { + NSCursor.arrow.set() + } +} diff --git a/Stats/Views/OneView.swift b/Stats/Views/OneView.swift index 0ff2662a..490213bf 100644 --- a/Stats/Views/OneView.swift +++ b/Stats/Views/OneView.swift @@ -36,6 +36,7 @@ class OneView { } NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(listenForModuleRearrrange), name: .moduleRearrange, object: nil) } deinit { @@ -68,7 +69,7 @@ class OneView { var w: CGFloat = 0 var i: Int = 0 - modules.filter({ $0.enabled }).forEach { (m: Module) in + modules.filter({ $0.enabled }).sorted(by: { $0.oneViewPosition < $1.oneViewPosition }).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 @@ -91,4 +92,8 @@ class OneView { self.disable() } } + + @objc private func listenForModuleRearrrange() { + self.recalculate() + } }