From d682bd327c4dbbdaa4964d11c4a0b2f6c1d8299e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Mon, 30 Oct 2023 12:27:34 +0100 Subject: [PATCH 01/20] fix SwiftUI Previews see also https://github.com/yonaskolb/XcodeGen/issues/1411 The issue here was that the `MastonautTests` project implicitly had build types set to `all` instead of `test`, and while that works fine for building and testing, it does not for Previews. --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index f1c3162..94bcc87 100644 --- a/project.yml +++ b/project.yml @@ -222,7 +222,7 @@ schemes: build: targets: Mastonaut: all - MastonautTests: test + MastonautTests: [test] run: config: Debug test: From 5daebf328b36faca379d7e8c317b9672b454f999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Mon, 30 Oct 2023 12:45:36 +0100 Subject: [PATCH 02/20] features/rearrange-columns: early prototype --- .../ArrangeColumns/ArrangeColumnsView.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift new file mode 100644 index 0000000..aa83777 --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift @@ -0,0 +1,30 @@ +// +// ArrangeColumnsView.swift +// Mastonaut +// +// Created by Sören Kuklau on 30.10.23. +// + +import SwiftUI + +struct ArrangeColumnsView: View { + var body: some View { + HStack { + HStack { + Image(systemName: "house") + Text("Home") + }.frame(minWidth: 160, minHeight: 100) + .border(.secondary) + + HStack { + Image(systemName: "bell") + Text("Notifications") + }.frame(minWidth: 160, minHeight: 100) + .border(.secondary) + }.padding(10) + } +} + +#Preview { + ArrangeColumnsView() +} From 55579a7efafd3af9a0f298dc1cee9616abb46cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Mon, 30 Oct 2023 13:02:26 +0100 Subject: [PATCH 03/20] features/rearrange-columns: refactor ArrangeableColumn into its own view --- .../ArrangeColumns/ArrangeColumnsView.swift | 14 +++-------- .../ArrangeColumns/ArrangeableColumn.swift | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift index aa83777..f8b160a 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift @@ -10,17 +10,11 @@ import SwiftUI struct ArrangeColumnsView: View { var body: some View { HStack { - HStack { - Image(systemName: "house") - Text("Home") - }.frame(minWidth: 160, minHeight: 100) - .border(.secondary) + ArrangeableColumn(icon: "house", text: "Home") - HStack { - Image(systemName: "bell") - Text("Notifications") - }.frame(minWidth: 160, minHeight: 100) - .border(.secondary) + ArrangeableColumn(icon: "bell", text: "Notifications") + + ArrangeableColumn(icon: "star", text: "Favorites") }.padding(10) } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift b/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift new file mode 100644 index 0000000..bca861a --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift @@ -0,0 +1,25 @@ +// +// ArrangeableColumn.swift +// Mastonaut +// +// Created by Sören Kuklau on 30.10.23. +// + +import SwiftUI + +struct ArrangeableColumn: View { + @State var icon: String + @State var text: String + + var body: some View { + HStack { + Image(systemName: icon) + Text(text) + }.frame(minWidth: 160, minHeight: 100) + .border(.secondary) + } +} + +#Preview { + ArrangeableColumn(icon: "house", text: "Home") +} From ca2d66d218dd8467d4b1072b669eb642e49d3867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Tue, 31 Oct 2023 00:46:37 +0100 Subject: [PATCH 04/20] features/rearrange-columns: start rewriting this as an AppKit Collection View --- .../ArrangeColumnsViewItem.swift | 17 ++++ .../ArrangeColumns/ArrangeColumnsViewItem.xib | 41 ++++++++++ .../ArrangeColumns/ArrangeColumnsWindow.xib | 80 +++++++++++++++++++ .../ArrangeColumnsWindowController.swift | 29 +++++++ .../TimelinesWindowController.swift | 31 ++++++- 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib create mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift new file mode 100644 index 0000000..78f2750 --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -0,0 +1,17 @@ +// +// ArrangeColumnsViewItem.swift +// Mastonaut +// +// Created by Sören Kuklau on 30.10.23. +// + +import Cocoa + +class ArrangeColumnsViewItem: NSCollectionViewItem { + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + } + +} diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib new file mode 100644 index 0000000..dae4164 --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib new file mode 100644 index 0000000..7452364 --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift new file mode 100644 index 0000000..48e6b2d --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -0,0 +1,29 @@ +// +// ArrangeColumnsViewController.swift +// Mastonaut +// +// Created by Sören Kuklau on 30.10.23. +// + +import Foundation + +class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelegate, NSCollectionViewDataSource { + + override var windowNibName: NSNib.Name? { + return "ArrangeColumnsWindow" + } + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + 10 + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { +// for i in 1..<10 { + ArrangeColumnsViewItem() +// } + } + + @IBAction func done(_ sender: Any) { + close() + } +} diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 615fcfa..d74e744 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -698,17 +698,25 @@ class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, } items.append(.separator()) + items.append(.sectionHeader(🔠("This column"))) let reloadColumnItem = NSMenuItem() - reloadColumnItem.title = 🔠("Reload this Column") + reloadColumnItem.title = 🔠("Reload") reloadColumnItem.target = self reloadColumnItem.representedObject = index reloadColumnItem.action = #selector(TimelinesWindowController.reloadColumn(_:)) items.append(reloadColumnItem) if index > 0 { + let rearrangeColumnsItem = NSMenuItem() + rearrangeColumnsItem.title = 🔠("Rearrange…") + rearrangeColumnsItem.target = self + rearrangeColumnsItem.representedObject = index + rearrangeColumnsItem.action = #selector(TimelinesWindowController.rearrangeColumns(_:)) + items.append(rearrangeColumnsItem) + let removeColumnItem = NSMenuItem() - removeColumnItem.title = 🔠("Remove this Column") + removeColumnItem.title = 🔠("Remove") removeColumnItem.target = self removeColumnItem.representedObject = index removeColumnItem.action = #selector(TimelinesWindowController.removeColumn(_:)) @@ -1103,6 +1111,25 @@ extension TimelinesWindowController // IBActions replaceColumn(at: columnIndex, with: newModel.makeViewController()) } + @IBAction private func rearrangeColumns(_ sender: Any?) { +// guard +// let menuItem = sender as? NSMenuItem, +// let columnIndex = menuItem.representedObject as? Int +// else { +// return +// } +// +// removeColumn(at: columnIndex, contract: true) + + let wc = ArrangeColumnsWindowController() + + if let childWindow = wc.window, + let parentWindow = window + { + parentWindow.beginSheet(childWindow) + } + } + @IBAction private func removeColumn(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem, From f5939ae37e277cacdf1f50163c499ff7e76b141a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Tue, 31 Oct 2023 21:39:53 +0100 Subject: [PATCH 05/20] features/rearrange-columns: refactor `ColumnMode` so we can ask its `title` and `image` separately --- Mastonaut/Models/ColumnMode.swift | 456 ++++++++++++++++-------------- 1 file changed, 242 insertions(+), 214 deletions(-) diff --git a/Mastonaut/Models/ColumnMode.swift b/Mastonaut/Models/ColumnMode.swift index 588b8b4..fe34cbc 100644 --- a/Mastonaut/Models/ColumnMode.swift +++ b/Mastonaut/Models/ColumnMode.swift @@ -23,223 +23,251 @@ import MastodonKit enum ColumnMode: RawRepresentable, ColumnModel, Equatable, Comparable { - typealias RawValue = String - - case timeline - case localTimeline - case publicTimeline - case notifications - - case favorites - case bookmarks - - case list(list: FollowedList) - case tag(name: String) - - var rawValue: RawValue - { - switch self - { - case .timeline: return "timeline" - case .localTimeline: return "localTimeline" - case .publicTimeline: return "publicTimeline" - case .notifications: return "notifications" - - case .favorites: return "favorites" - case .bookmarks: return "bookmarks" - - case .list(let list): return "list:\(list.title ?? "")" - case .tag(let name): return "tag:\(name)" - } - } - - init?(rawValue: RawValue) - { - switch rawValue - { - case "timeline": self = .timeline - case "localTimeline": self = .localTimeline - case "publicTimeline": self = .publicTimeline - case "notifications": self = .notifications - - case "favorites": self = .favorites - case "bookmarks": self = .bookmarks + typealias RawValue = String + + case timeline + case localTimeline + case publicTimeline + case notifications + + case favorites + case bookmarks + + case list(list: FollowedList) + case tag(name: String) + + var rawValue: RawValue + { + switch self + { + case .timeline: return "timeline" + case .localTimeline: return "localTimeline" + case .publicTimeline: return "publicTimeline" + case .notifications: return "notifications" + + case .favorites: return "favorites" + case .bookmarks: return "bookmarks" + + case .list(let list): return "list:\(list.title ?? "")" + case .tag(let name): return "tag:\(name)" + } + } + + init?(rawValue: RawValue) + { + switch rawValue + { + case "timeline": self = .timeline + case "localTimeline": self = .localTimeline + case "publicTimeline": self = .publicTimeline + case "notifications": self = .notifications + + case "favorites": self = .favorites + case "bookmarks": self = .bookmarks // case let rawValue where rawValue.hasPrefix("list:"): // let id = rawValue.suffix(from: rawValue.index(after: rawValue.range(of: "list:")!.upperBound)) // self = .list(list: List(from: <#T##Decoder#>) String(name)) - case let rawValue where rawValue.hasPrefix("tag:"): - let name = rawValue.suffix(from: rawValue.index(after: rawValue.range(of: "tag:")!.upperBound)) - self = .tag(name: String(name)) - - default: - return nil - } - } - - var weight: Int - { - switch self - { - case .timeline: return -7 - case .localTimeline: return -6 - case .publicTimeline: return -5 - case .notifications: return -4 - - case .favorites: return -3 - case .bookmarks: return -2 - - case .list: return -1 - case .tag: return 0 - } - } - - func makeViewController() -> ColumnViewController - { - switch self - { - case .timeline: return TimelineViewController(source: .timeline) - case .localTimeline: return TimelineViewController(source: .localTimeline) - case .publicTimeline: return TimelineViewController(source: .publicTimeline) - case .notifications: return NotificationListViewController() - - case .favorites: return TimelineViewController(source: .favorites) - case .bookmarks: return TimelineViewController(source: .bookmarks) - - case .list(let list): return TimelineViewController(source: .list(list: list)) - case .tag(let name): return TimelineViewController(source: .tag(name: name)) - } - } - - private func makeMenuItem() -> NSMenuItem - { - let menuItem = NSMenuItem() - menuItem.representedObject = self - - switch self - { - case .timeline: - menuItem.title = 🔠("Home") - menuItem.image = #imageLiteral(resourceName: "home") - - case .localTimeline: - menuItem.title = 🔠("Local Timeline") - menuItem.image = #imageLiteral(resourceName: "group") - - case .publicTimeline: - menuItem.title = 🔠("Public Timeline") - menuItem.image = NSImage.CoreTootin.globe - - case .notifications: - menuItem.title = 🔠("Notifications") - menuItem.image = #imageLiteral(resourceName: "bell") - - case .favorites: - menuItem.title = 🔠("Favorites") - menuItem.image = NSImage(systemSymbolName: "star", accessibilityDescription: "Favorites") - - case .bookmarks: - menuItem.title = 🔠("Bookmarks") - menuItem.image = NSImage(systemSymbolName: "bookmark", accessibilityDescription: "Bookmarks") - - case .list(let list): - menuItem.title = 🔠(list.title!) - menuItem.image = NSImage(systemSymbolName: "list.bullet", accessibilityDescription: "List") - - case .tag(let name): - menuItem.title = 🔠("Tag: %@", name) - menuItem.image = #imageLiteral(resourceName: "bell") // FIXME: I don't think that's the right image? - } - - return menuItem - } - - var menuItemSection: Int - { - switch self - { - case .timeline, .localTimeline, .publicTimeline, .notifications: return 0 - - case .favorites: return 1 - case .bookmarks: return 1 - - case .list: return 2 - - case .tag: return 3 - } - } - - func makeMenuItemForAdding(with target: AnyObject) -> NSMenuItem - { - let menuItem = self.makeMenuItem() - menuItem.target = target - menuItem.action = #selector(TimelinesWindowController.addColumnMode(_:)) - return menuItem - } - - func makeMenuItemForChanging(with target: AnyObject, columnId: Int) -> NSMenuItem - { - let menuItem = self.makeMenuItem() - menuItem.tag = columnId - menuItem.target = target - menuItem.action = #selector(TimelinesWindowController.changeColumnMode(_:)) - return menuItem - } - - /// Instance-wide column modes - static var staticItems: [ColumnMode] - { - return [.timeline, .localTimeline, .publicTimeline, .notifications] - } - - /// Personal column modes that are always available (as opposed to lists, hashtags, etc.) - static var staticPersonalItems: [ColumnMode] - { - return [.favorites, .bookmarks] - } - - static func == (lhs: ColumnMode, rhs: ColumnMode) -> Bool - { - switch (lhs, rhs) - { - case (.timeline, .timeline): - return true - case (.localTimeline, .localTimeline): - return true - case (.publicTimeline, .publicTimeline): - return true - case (.notifications, .notifications): - return true - - case (.favorites, .favorites): - return true - case (.bookmarks, .bookmarks): - return true - - case (.list(let leftList), .list(let rightList)): - return leftList.id == rightList.id - case (.tag(let leftTag), .tag(let righTag)): - return leftTag == righTag - default: - return false - } - } - - static func < (lhs: ColumnMode, rhs: ColumnMode) -> Bool - { - if lhs.weight != rhs.weight - { - return lhs.weight < rhs.weight - } - - switch (lhs, rhs) - { - case (.tag(let leftTag), .tag(let rightTag)): - return leftTag < rightTag - - default: - return false - } - } + case let rawValue where rawValue.hasPrefix("tag:"): + let name = rawValue.suffix(from: rawValue.index(after: rawValue.range(of: "tag:")!.upperBound)) + self = .tag(name: String(name)) + + default: + return nil + } + } + + var weight: Int + { + switch self + { + case .timeline: return -7 + case .localTimeline: return -6 + case .publicTimeline: return -5 + case .notifications: return -4 + + case .favorites: return -3 + case .bookmarks: return -2 + + case .list: return -1 + case .tag: return 0 + } + } + + func makeViewController() -> ColumnViewController + { + switch self + { + case .timeline: return TimelineViewController(source: .timeline) + case .localTimeline: return TimelineViewController(source: .localTimeline) + case .publicTimeline: return TimelineViewController(source: .publicTimeline) + case .notifications: return NotificationListViewController() + + case .favorites: return TimelineViewController(source: .favorites) + case .bookmarks: return TimelineViewController(source: .bookmarks) + + case .list(let list): return TimelineViewController(source: .list(list: list)) + case .tag(let name): return TimelineViewController(source: .tag(name: name)) + } + } + + func getTitle() -> String + { + switch self + { + case .timeline: + return 🔠("Home") + + case .localTimeline: + return 🔠("Local Timeline") + + case .publicTimeline: + return 🔠("Public Timeline") + + case .notifications: + return 🔠("Notifications") + + case .favorites: + return 🔠("Favorites") + + case .bookmarks: + return 🔠("Bookmarks") + + case .list(let list): + return 🔠(list.title!) + + case .tag(let name): + return 🔠("Tag: %@", name) + } + } + + func getImage() -> NSImage? + { + switch self + { + case .timeline: + return #imageLiteral(resourceName: "home") + + case .localTimeline: + return #imageLiteral(resourceName: "group") + + case .publicTimeline: + return NSImage.CoreTootin.globe + + case .notifications: + return #imageLiteral(resourceName: "bell") + + case .favorites: + return NSImage(systemSymbolName: "star", accessibilityDescription: "Favorites") + + case .bookmarks: + return NSImage(systemSymbolName: "bookmark", accessibilityDescription: "Bookmarks") + + case .list: + return NSImage(systemSymbolName: "list.bullet", accessibilityDescription: "List") + + case .tag: + return #imageLiteral(resourceName: "bell") // FIXME: I don't think that's the right image? + } + } + + private func makeMenuItem() -> NSMenuItem + { + let menuItem = NSMenuItem() + menuItem.representedObject = self + + menuItem.title = self.getTitle() + menuItem.image = self.getImage() + + return menuItem + } + + var menuItemSection: Int + { + switch self + { + case .timeline, .localTimeline, .publicTimeline, .notifications: return 0 + + case .favorites: return 1 + case .bookmarks: return 1 + + case .list: return 2 + + case .tag: return 3 + } + } + + func makeMenuItemForAdding(with target: AnyObject) -> NSMenuItem + { + let menuItem = self.makeMenuItem() + menuItem.target = target + menuItem.action = #selector(TimelinesWindowController.addColumnMode(_:)) + return menuItem + } + + func makeMenuItemForChanging(with target: AnyObject, columnId: Int) -> NSMenuItem + { + let menuItem = self.makeMenuItem() + menuItem.tag = columnId + menuItem.target = target + menuItem.action = #selector(TimelinesWindowController.changeColumnMode(_:)) + return menuItem + } + + /// Instance-wide column modes + static var staticItems: [ColumnMode] + { + return [.timeline, .localTimeline, .publicTimeline, .notifications] + } + + /// Personal column modes that are always available (as opposed to lists, hashtags, etc.) + static var staticPersonalItems: [ColumnMode] + { + return [.favorites, .bookmarks] + } + + static func == (lhs: ColumnMode, rhs: ColumnMode) -> Bool + { + switch (lhs, rhs) + { + case (.timeline, .timeline): + return true + case (.localTimeline, .localTimeline): + return true + case (.publicTimeline, .publicTimeline): + return true + case (.notifications, .notifications): + return true + + case (.favorites, .favorites): + return true + case (.bookmarks, .bookmarks): + return true + + case (.list(let leftList), .list(let rightList)): + return leftList.id == rightList.id + case (.tag(let leftTag), .tag(let righTag)): + return leftTag == righTag + default: + return false + } + } + + static func < (lhs: ColumnMode, rhs: ColumnMode) -> Bool + { + if lhs.weight != rhs.weight + { + return lhs.weight < rhs.weight + } + + switch (lhs, rhs) + { + case (.tag(let leftTag), .tag(let rightTag)): + return leftTag < rightTag + + default: + return false + } + } } From 33add3498121a331bb4cb24ffa70928741d6165d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Tue, 31 Oct 2023 21:40:37 +0100 Subject: [PATCH 06/20] features/rearrange-columns: pass actual columns from window --- .../ArrangeColumnsViewItem.swift | 17 +++++++ .../ArrangeColumns/ArrangeColumnsViewItem.xib | 46 +++++++++++++++---- .../ArrangeColumns/ArrangeColumnsWindow.xib | 13 +++--- .../ArrangeColumnsWindowController.swift | 43 ++++++++++++++--- .../TimelinesWindowController.swift | 2 + 5 files changed, 98 insertions(+), 23 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index 78f2750..025af45 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -8,10 +8,27 @@ import Cocoa class ArrangeColumnsViewItem: NSCollectionViewItem { + @IBOutlet var box: NSBox! + + @IBOutlet var label: NSTextField! + @IBOutlet var image: NSImageView! + + private var columnViewController: ColumnViewController? override func viewDidLoad() { super.viewDidLoad() // Do view setup here. } + func set(columnViewController: ColumnViewController) { + guard let label, + let columnMode = columnViewController.modelRepresentation as? ColumnMode + else { return } + + label.stringValue = columnMode.getTitle() + image.image = columnMode.getImage() + + // TODO: MAYBE aspect ratio +// box.frame = NSRect(x: 0, y: 0, width: 100, height: 100) + } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib index dae4164..c8043d0 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib @@ -8,26 +8,52 @@ + + + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index 7452364..41ceb3a 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -8,6 +8,7 @@ + @@ -32,20 +33,20 @@ DQ - + - + - + - - + + @@ -60,7 +61,7 @@ DQ diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index 48e6b2d..de40884 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -8,22 +8,51 @@ import Foundation class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelegate, NSCollectionViewDataSource { - + @IBOutlet private unowned var collectionView: NSCollectionView! + + @IBOutlet private(set) unowned var button: NSButton! + override var windowNibName: NSNib.Name? { return "ArrangeColumnsWindow" } + private enum ReuseIdentifiers { + static let item = NSUserInterfaceItemIdentifier(rawValue: "item") + } + + override func awakeFromNib() { + super.awakeFromNib() + + collectionView.register(ArrangeColumnsViewItem.self, + forItemWithIdentifier: ReuseIdentifiers.item) + } + + var columnViewControllers: [ColumnViewController]? + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { - 10 + columnViewControllers?.count ?? 0 } - func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { -// for i in 1..<10 { - ArrangeColumnsViewItem() -// } + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem + { + let identifier = ReuseIdentifiers.item + let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) + + let index = indexPath.item + + guard let viewItem = item as? ArrangeColumnsViewItem, + let columnViewControllers, + columnViewControllers.count >= index + else { return item } + + viewItem.set(columnViewController: columnViewControllers[index]) + + return viewItem } - + @IBAction func done(_ sender: Any) { close() } + + @IBAction func what(_ sender: Any) {} } diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index d74e744..4f3e234 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1123,6 +1123,8 @@ extension TimelinesWindowController // IBActions let wc = ArrangeColumnsWindowController() + wc.columnViewControllers = timelinesViewController.columnViewControllers + if let childWindow = wc.window, let parentWindow = window { From 7938f4408351ee9f8cef1175c38829a35c1001dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 20 Jan 2024 20:27:06 +0100 Subject: [PATCH 07/20] features/rearrange-columns: wip --- .../ArrangeColumnsViewItem.swift | 11 +- .../ArrangeColumns/ArrangeColumnsViewItem.xib | 53 +- .../ArrangeColumns/ArrangeColumnsWindow.xib | 6 +- .../ArrangeColumnsWindowController.swift | 58 + .../ArrangeColumns/UnclickableBox.swift | 14 + .../TimelinesWindowController.swift | 2268 ++++++++--------- 6 files changed, 1232 insertions(+), 1178 deletions(-) create mode 100644 Mastonaut/Features/ArrangeColumns/UnclickableBox.swift diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index 025af45..5e6f424 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -21,14 +21,19 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { } func set(columnViewController: ColumnViewController) { - guard let label, + guard //let label, let columnMode = columnViewController.modelRepresentation as? ColumnMode else { return } - label.stringValue = columnMode.getTitle() - image.image = columnMode.getImage() +// label.stringValue = columnMode.getTitle() +// image.image = columnMode.getImage() // TODO: MAYBE aspect ratio // box.frame = NSRect(x: 0, y: 0, width: 100, height: 100) } + + func setHighlighted(_ highlighted: Bool) { + view.layer?.borderColor = highlighted ? NSColor.systemRed.cgColor : NSColor.systemBlue.cgColor + view.layer?.borderWidth = highlighted ? 3.0 : 0.0 + } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib index c8043d0..b0497bb 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib @@ -8,50 +8,30 @@ - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + @@ -61,7 +41,4 @@ - - - diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index 41ceb3a..65a0910 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -18,7 +18,7 @@ - + @@ -42,9 +42,9 @@ DQ - + - + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index de40884..e2ae68c 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -6,6 +6,7 @@ // import Foundation +import UniformTypeIdentifiers class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelegate, NSCollectionViewDataSource { @IBOutlet private unowned var collectionView: NSCollectionView! @@ -25,14 +26,47 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega collectionView.register(ArrangeColumnsViewItem.self, forItemWithIdentifier: ReuseIdentifiers.item) + +// collectionView.registerForDraggedTypes([NSPasteboard.PasteboardType(UTType.item.identifier)]) +// collectionView.setDraggingSourceOperationMask(.move, forLocal: true) + + collectionView.registerForDraggedTypes([.string]) + collectionView.setDraggingSourceOperationMask(.every, forLocal: true) + collectionView.setDraggingSourceOperationMask(.every, forLocal: false) } var columnViewControllers: [ColumnViewController]? + func numberOfSections(in collectionView: NSCollectionView) -> Int { + 1 + } + + func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { + highlightItems(true, at: indexPaths) + } + + func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { + highlightItems(false, at: indexPaths) + } + + func highlightItems(_ highlighted: Bool, at indexPaths: Set) { + for indexPath in indexPaths { + guard let item = collectionView.item(at: indexPath) as? ArrangeColumnsViewItem else { continue } + item.setHighlighted(highlighted) + } + } + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { columnViewControllers?.count ?? 0 } + func collectionView(_ collectionView: NSCollectionView, + pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? + { + print("pasteboardWriterForItemAt") + return nil + } + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let identifier = ReuseIdentifiers.item @@ -46,10 +80,34 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega else { return item } viewItem.set(columnViewController: columnViewControllers[index]) + + viewItem.setHighlighted(false) return viewItem } + func collectionView(_ collectionView: NSCollectionView, writeItemsAt indexes: IndexSet, to pasteboard: NSPasteboard) -> Bool { + print("writeItemsAt") + return true + } + + func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool { + print("canDragItemsAt") + return true + } + + func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, index: Int, dropOperation: NSCollectionView.DropOperation) -> Bool { + print("acceptDrop") + // TODO: check type + + return true + } + + func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndex proposedDropIndex: UnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { + print("validateDrop") + return NSDragOperation.move + } + @IBAction func done(_ sender: Any) { close() } diff --git a/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift b/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift new file mode 100644 index 0000000..585a9d5 --- /dev/null +++ b/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift @@ -0,0 +1,14 @@ +// +// UnclickableBoxView.swift +// Mastonaut +// +// Created by Sören Kuklau on 04.11.23. +// + +import Foundation + +class UnclickableBox: NSBox { + override func hitTest(_ point: NSPoint) -> NSView? { + return nil + } +} diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 4f3e234..5701580 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -25,689 +25,689 @@ import PullRefreshableScrollView class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, ToolbarWindowController { - private var logger: Logger! + private var logger: Logger! - // MARK: Outlets + // MARK: Outlets - @IBOutlet private var newColumnMenu: NSMenu! + @IBOutlet private var newColumnMenu: NSMenu! - // MARK: Services + // MARK: Services - private unowned let accountsService = AppDelegate.shared.accountsService - private unowned let instanceService = AppDelegate.shared.instanceService + private unowned let accountsService = AppDelegate.shared.accountsService + private unowned let instanceService = AppDelegate.shared.instanceService - // MARK: KVO Observers + // MARK: KVO Observers - private var observations = [NSKeyValueObservation]() - private var accountObservations = [NSKeyValueObservation]() + private var observations = [NSKeyValueObservation]() + private var accountObservations = [NSKeyValueObservation]() - // MARK: Toolbar Buttons + // MARK: Toolbar Buttons - internal lazy var toolbarContainerView: NSView? = makeToolbarContainerView() - internal var currentUserPopUpButton: NSPopUpButton = makeAccountsPopUpButton() - private var searchSegmentedControl: NSSegmentedControl = makeSearchSegmentedControl() - private var statusComposerSegmentedControl: NSSegmentedControl = makeStatusComposerSegmentedControl() - private var newColumnSegmentedControl: NSSegmentedControl = makeNewColumnSegmentedControl() - private var userPopUpButtonController: UserPopUpButtonSubcontroller! - private var popUpButtonConstraints = [NSLayoutConstraint]() - private let columnPopUpButtonMap = NSMapTable(keyOptions: .weakMemory, - valueOptions: .weakMemory) - - // MARK: Toolbar Sidebar Controls - - private var sidebarNavigationSegmentedControl: NSSegmentedControl = makeSidebarNavigationSegmentedControl() - private var sidebarTitleViewController = SidebarTitleViewController() - private var sidebarTitleViewCenterXConstraint: NSLayoutConstraint? - private var closeSidebarSegmentedControl: NSSegmentedControl? - - // MARK: Sidebar - - private lazy var sidebarSubcontroller = SidebarSubcontroller(sidebarContainer: self, - navigationControl: sidebarNavigationSegmentedControl, - navigationStack: nil) - - internal var sidebarViewController: SidebarViewController? { - get { return timelinesSplitViewController.sidebarViewController } - set { timelinesSplitViewController.sidebarViewController = newValue } - } - - // MARK: Child View Controllers - - private var placeholderViewController: NSViewController? - private var searchWindowController: NSWindowController? - - // MARK: Lifecycle Support - - private var preservedWindowFrameStack: Stack = [] - - var currentInstance: Instance? { - didSet { - if let sidebarMode = sidebarSubcontroller.navigationStack?.currentItem, - sidebarViewController == nil - { - let oldStack = preservedWindowFrameStack - sidebarSubcontroller.installSidebar(mode: sidebarMode) - preservedWindowFrameStack = oldStack - } - } - } - - var firstColumnFrame: NSRect? { - if timelinesViewController.columnViewControllers.count == 0 { - return nil - } - - return timelinesViewController.columnViewControllers[0].view.frame - } - - private(set) var client: ClientType? { - didSet { - guard AppDelegate.shared.appIsReady else { return } - - timelinesViewController.columnViewControllers.forEach { $0.client = client } - revalidateSidebarAccountReference() - } - } - - internal var currentUser: UUID? { - get { return currentAccount?.uuid } - set { - currentAccount = newValue.flatMap { accountsService.account(with: $0) } - - updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) - } - } - - internal var currentAccount: AuthorizedAccount? { - didSet { - let hasUser: Bool - - if let currentAccount = currentAccount { - hasUser = true - let client = Client.create(for: currentAccount) - - if let window = window { - if let instance = currentAccount.baseDomain { - window.title = "@\(currentAccount.username!) — \(instance)" - } - else { - window.title = "@\(currentAccount.username!)" - } - } - - removePlaceholderIfInstalled() - updateUserPopUpButton() - - instanceService.instance(for: currentAccount) { - [weak self] instance in - DispatchQueue.main.async { - self?.client = client - self?.currentInstance = instance - } - } - - accountObservations.observe(currentAccount, \.bookmarkedTags) { - _, _ in AppDelegate.shared.updateAccountsMenu() - } - } - else { - hasUser = false - client = nil - currentInstance = nil - accountObservations.removeAll() - sidebarSubcontroller.uninstallSidebar() - installPlaceholder() - window?.title = 🔠("Mastonaut — No Account Selected") - } - - searchSegmentedControl.isHidden = !hasUser - statusComposerSegmentedControl.isHidden = !hasUser - newColumnSegmentedControl.isHidden = !hasUser - timelinesViewController.columnViewControllers.forEach { columnPopUpButtonMap.object(forKey: $0)?.isHidden = !hasUser } - columnPopUpButtonMap.objectEnumerator()?.forEach { ($0 as? NSControl)?.isHidden = !hasUser } - - invalidateRestorableState() - - if window?.isKeyWindow == true { - AppDelegate.shared.updateAccountsMenu() - } - } - } - - var hasNotificationsColumn: Bool { - for controller in timelinesViewController.columnViewControllers { - if case .some(ColumnMode.notifications) = controller.modelRepresentation { - return true - } - } - - return false - } - - private var timelinesSplitViewController: TimelinesSplitViewController { - return contentViewController as! TimelinesSplitViewController - } - - private var timelinesViewController: TimelinesViewController { - return timelinesSplitViewController.children.first as! TimelinesViewController - } - - private lazy var accountMenuItems: [NSMenuItem] = { - [ - NSMenuItem(title: 🔠("View Profile"), - action: #selector(showUserProfile(_:)), - keyEquivalent: ""), - NSMenuItem(title: 🔠("Open Profile in Browser"), - action: #selector(openUserProfileInBrowser(_:)), - keyEquivalent: ""), - NSMenuItem(title: 🔠("View Favorites"), - action: #selector(showUserFavorites(_:)), - keyEquivalent: "F").with(modifierMask: [.command, .shift]), - .separator() - ] - }() - - // MARK: Window Controller Lifecycle - - func prepareAsEmptyWindow() { - if Preferences.newWindowAccountMode == .pickFirstOne, let account = accounts.first { - currentAccount = account - } - else { - currentAccount = nil - } - - appendColumnIfFitting(model: ColumnMode.timeline) - } - - override func encodeRestorableState(with coder: NSCoder) { - let columnModels = timelinesViewController.columnViewControllers.compactMap { $0.modelRepresentation } - let encodedColumnModels = columnModels.map { $0.rawValue }.joined(separator: ";") - - coder.encode(currentAccount?.uuid, forKey: CodingKeys.currentUser) - coder.encode(encodedColumnModels, forKey: CodingKeys.columns) - coder.encode(preservedWindowFrameStack.map { NSValue(rect: $0) }, forKey: CodingKeys.preservedWindowFrameStack) - - if let navigationStack = sidebarSubcontroller.navigationStack { - // HOTFIX: Swift classes with parameter types do not encode properly in *Release* - // Since we know the type in advance, we use a separate archiver for the navigation stack which skips - // the class name level and encodes only the internals. - let encoder = NSKeyedArchiver(requiringSecureCoding: false) - navigationStack.encode(with: encoder) - coder.encode(encoder.encodedData, forKey: CodingKeys.sidebarNavigationStack) - -// coder.encode(navigationStack, forKey: CodingKeys.sidebarNavigationStack) - } - } - - override func restoreState(with coder: NSCoder) { - if let uuid: UUID = coder.decodeObject(forKey: CodingKeys.currentUser), - let account = accountsService.account(with: uuid) - { - currentAccount = account - } - else { - currentAccount = nil - } - - if let encodedColumnModels: String = coder.decodeObject(forKey: CodingKeys.columns) { - let columnModels = encodedColumnModels.split(separator: ";") - .compactMap { ColumnMode(rawValue: String($0)) } - - for model in columnModels { - appendColumnIfFitting(model: model, expand: false) - } - } - - if let frameStack: [NSValue] = coder.decodeObject(forKey: CodingKeys.preservedWindowFrameStack) { - preservedWindowFrameStack = Stack(frameStack.compactMap { $0.rectValue }) - } - else { - preservedWindowFrameStack = [] - } - - if timelinesViewController.columnViewControllers.isEmpty { - // Fallback if no columns were installed from decoding - appendColumnIfFitting(model: ColumnMode.timeline) - } - - // HOTFIX: Swift classes with parameter types do not encode properly in *Release* - // Since we know the type in advance, we use a separate archiver for the navigation stack which skips - // the class name level and encodes only the internals. -// if let stack: NavigationStack = coder.decodeObject(forKey: CodingKeys.sidebarNavigationStack) - if let stackEncodedData: Data = coder.decodeObject(forKey: CodingKeys.sidebarNavigationStack) { - let decoder = NSKeyedUnarchiver(forReadingWith: stackEncodedData) - if let stack = NavigationStack(coder: decoder) { - timelinesSplitViewController.preserveSplitViewSizeForNextSidebarInstall = true - sidebarSubcontroller = SidebarSubcontroller(sidebarContainer: self, - navigationControl: sidebarNavigationSegmentedControl, - navigationStack: stack) - } - } - - updateUserPopUpButton() - } - - override func windowDidLoad() { - super.windowDidLoad() - - logger = Logger(subsystemType: self) - - shouldCascadeWindows = true - - window?.restorationClass = TimelinesWindowRestoration.self - - newColumnSegmentedControl.setMenu(newColumnMenu, forSegment: 0) - - userPopUpButtonController = UserPopUpButtonSubcontroller(display: self) - - observations.observe(on: .main, timelinesViewController, \TimelinesViewController.columnViewControllersCount) { - [weak self] timelinesViewController, _ in - self?.updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) - self?.newColumnSegmentedControl.setEnabled(timelinesViewController.canAppendStatusList, forSegment: 0) - self?.invalidateRestorableState() - } - - observations.observe(AppDelegate.shared, \AppDelegate.appIsReady) { - [weak self] appDelegate, _ in - - if appDelegate.appIsReady, let client = self?.client { - self?.timelinesViewController.columnViewControllers.forEach { $0.client = client } - self?.revalidateSidebarAccountReference() - } - } - - guard let window = window else { return } - - window.backgroundColor = .timelineBackground - } - - func handleDetach() { - for _ in 0 ..< timelinesViewController.columnViewControllersCount { - removeColumn(at: 0, contract: false) - } - } - - // MARK: UI Handling - - func updateUserPopUpButton() { - userPopUpButtonController.updateUserPopUpButton() - } - - func shouldChangeCurrentUser(to userUUID: UUID) -> Bool { - return true - } - - func redraft(status: Status) { - AppDelegate.shared.composeStatus(self) - guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } - composerWindowController.showWindow(nil) - composerWindowController.setUpAsRedraft(of: status, using: currentAccount) - } - - func edit(status: Status) { - AppDelegate.shared.composeStatus(self) - guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } - composerWindowController.showWindow(nil) - composerWindowController.setUpAsEdit(of: status, using: currentAccount) - } - - func composeReply(for status: Status, sender: Any?) { - AppDelegate.shared.composeStatus(self) - guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } - composerWindowController.showWindow(sender) - composerWindowController.setupAsReply(to: status, using: currentAccount, senderWindowController: self) - } - - func composeMention(userHandle: String, directMessage: Bool) { - AppDelegate.shared.composeStatus(self) - guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } - composerWindowController.showWindow(nil) - - composerWindowController.setupAsMention(handle: userHandle, using: currentAccount, directMessage: directMessage) - } - - func installPlaceholder() { - guard let contentView = contentViewController?.view, placeholderViewController == nil else { return } - - let accounts = accountsService.authorizedAccounts - let viewController: NSViewController - - if accounts.isEmpty { - // Show welcome placeholder - viewController = WelcomePlaceholderController() - } - else { - // Show account picker placeholder - viewController = AccountsPlaceholderController() - } - - contentView.subviews.forEach { $0.isHidden = true } - - contentViewController?.addChild(viewController) - contentView.addSubview(viewController.view) - placeholderViewController = viewController - - NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), - contentView.topAnchor.constraint(equalTo: viewController.view.topAnchor), - contentView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) - ]) - } - - func removePlaceholderIfInstalled() { - placeholderViewController?.view.removeFromSuperview() - placeholderViewController?.removeFromParent() - placeholderViewController = nil - - contentViewController?.view.subviews.forEach { $0.isHidden = false } - } - - func presentSearchWindow() { - let storyboard = NSStoryboard(name: "Search", bundle: .main) - - guard - currentInstance != nil, - let account = currentAccount, - let client = client, - let searchWindowController = storyboard.instantiateInitialController() as? SearchWindowController, - let searchWindow = searchWindowController.window, - let timelinesWindow = window - else { - return - } - - searchWindowController.set(client: client) - searchWindowController.set(searchDelegate: self) - - AppDelegate.shared.instanceService.instance(for: account) { - [weak self] instance in - - guard let instance = instance else { return } - - searchWindowController.set(instance: instance) - self?.searchWindowController = searchWindowController - - timelinesWindow.beginSheet(searchWindow) { - _ in self?.searchWindowController = nil - } - } - } - - func adjustWindowFrame(adjustment: WindowSizeAdjustment) { - guard - let window = window, - window.styleMask.contains(.fullScreen) == false, - let screen = window.screen - else { return } - - let originalWindowFrame = window.frame - var frame = originalWindowFrame - - switch adjustment { - case .nudgeIfClipped: - let excessWidth = window.frame.maxX - screen.frame.maxX - guard excessWidth > 0 else { return } - frame.origin.x -= excessWidth - - case .expand(by: let extraWidth): - preservedWindowFrameStack.push(originalWindowFrame) - - if originalWindowFrame.maxX + extraWidth <= screen.frame.maxX { - frame.size.width = originalWindowFrame.width + extraWidth - } - else { - frame.size.width = screen.frame.maxX - window.frame.origin.x - } - - if let contentView = window.contentView { - let contentFrame = window.contentRect(forFrameRect: frame) - contentView.setFrameSize(contentFrame.size) - contentView.layoutSubtreeIfNeeded() - - let difference = contentView.frame.width - contentFrame.width - - if difference > 0 { - frame.origin.x -= difference - } - } - - case .contract(by: let extraWidth, let tryPoppingPreservedFrame): - if tryPoppingPreservedFrame, let preservedFrame = preservedWindowFrameStack.popIfNotEmpty() { - frame = preservedFrame - } - else { - frame.size.width -= extraWidth - } - - case .restorePreservedOriginIfPossible: - if let preservedFrame = preservedWindowFrameStack.popIfNotEmpty() { - frame.origin = preservedFrame.origin - } - } - - window.animator().setFrame(frame, display: false) - } - - override func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) - - if preservedWindowFrameStack.isEmpty == false { - preservedWindowFrameStack = [] - } - } - - enum WindowSizeAdjustment { - case expand(by: CGFloat) - case nudgeIfClipped - case contract(by: CGFloat, poppingPreservedFrameIfPossible: Bool) - case restorePreservedOriginIfPossible - } - - // MARK: ToolbarWindowController - - func didToggleToolbarShown(_ window: ToolbarWindow) { - if window.toolbar?.isVisible == true { - updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) - } - } - - // MARK: Internal helper methods - - private func revalidateSidebarAccountReference() { - if let accountBoundSidebar = timelinesSplitViewController.sidebarViewController as? AccountBound, - let currentAccount = accountBoundSidebar.account, - let instance = currentInstance, - let client = client - { - ResolverService(client: client).resolve(account: currentAccount, activeInstance: instance) { - [weak self] result in - - DispatchQueue.main.async { - switch result { - case .success(let account): - if AppDelegate.shared.appIsReady { - self?.timelinesSplitViewController.sidebarViewController?.client = client - } - accountBoundSidebar.setRecreatedAccount(account) - self?.invalidateRestorableState() - - case .failure(let error): - self?.displayError(error) - self?.sidebarSubcontroller.uninstallSidebar() - } - } - } - } - else { - timelinesSplitViewController.sidebarViewController?.client = client - } - } - - private func installPersistentToolbarButtons(toolbarView: NSView) { - var constraints: [NSLayoutConstraint] = [] - let contentView = timelinesViewController.mainContentView - - [currentUserPopUpButton, statusComposerSegmentedControl, searchSegmentedControl, newColumnSegmentedControl].forEach { - toolbarView.addSubview($0) - let referenceView = toolbarView.superview ?? toolbarView - constraints.append(referenceView.centerYAnchor.constraint(equalTo: $0.centerYAnchor)) - } - - constraints.append(TrackingLayoutConstraint.constraint(trackingMaxXOf: contentView, - targetView: newColumnSegmentedControl, - containerView: toolbarView, - targetAttribute: .trailing, - containerAttribute: .leading) - .with(priority: .defaultLow)) - - constraints.append(contentsOf: [ - currentUserPopUpButton.leadingAnchor.constraint(equalTo: toolbarView.leadingAnchor, constant: 6), - searchSegmentedControl.leadingAnchor.constraint(equalTo: statusComposerSegmentedControl.trailingAnchor, - constant: 8), - newColumnSegmentedControl.leadingAnchor.constraint(equalTo: searchSegmentedControl.trailingAnchor, - constant: 8), - toolbarView.trailingAnchor.constraint(greaterThanOrEqualTo: newColumnSegmentedControl.trailingAnchor, constant: 6) - ]) - - NSLayoutConstraint.activate(constraints) - } - - private func updateColumnsPopUpButtons(for columnViewControllers: [ColumnViewController]) { - guard let toolbarView = toolbarContainerView?.superview else { return } - - NSLayoutConstraint.deactivate(popUpButtonConstraints) - popUpButtonConstraints.removeAll() - - let takenModes = columnViewControllers.compactMap { $0.modelRepresentation as? ColumnMode } - - var previousButton = currentUserPopUpButton - - // Install column buttons - for (index, column) in columnViewControllers.enumerated() { - guard let popUpButton = columnPopUpButtonMap.object(forKey: column) else { - continue - } - - guard let currentModel = column.modelRepresentation as? ColumnMode else { - continue - } - - let popupButtonMenu = buildColumnsPopupButtonMenu(currentColumnMode: currentModel, - takenModes: takenModes, - index: index) - - popUpButton.menu = popupButtonMenu - popUpButton.tag = index - popUpButton.select(popupButtonMenu.item(withRepresentedObject: currentModel)) - - popUpButtonConstraints.append(TrackingLayoutConstraint - .constraint(trackingMidXOf: column.view, - targetView: popUpButton, - containerView: toolbarView, - targetAttribute: .centerX, - containerAttribute: .leading) - .with(priority: .defaultLow + 248)) - - popUpButtonConstraints.append(popUpButton.leadingAnchor.constraint( - greaterThanOrEqualTo: previousButton.trailingAnchor, - constant: 8 - )) - - previousButton = popUpButton - } - - if previousButton != currentUserPopUpButton { - popUpButtonConstraints.append(statusComposerSegmentedControl.leadingAnchor.constraint( - greaterThanOrEqualTo: previousButton.trailingAnchor, - constant: 8 - )) - } - - let newColumnMenuItems = buildNewColumnMenuItems(takenModes: takenModes) - - newColumnMenu.setItems(newColumnMenuItems) + lazy var toolbarContainerView: NSView? = makeToolbarContainerView() + var currentUserPopUpButton: NSPopUpButton = makeAccountsPopUpButton() + private var searchSegmentedControl: NSSegmentedControl = makeSearchSegmentedControl() + private var statusComposerSegmentedControl: NSSegmentedControl = makeStatusComposerSegmentedControl() + private var newColumnSegmentedControl: NSSegmentedControl = makeNewColumnSegmentedControl() + private var userPopUpButtonController: UserPopUpButtonSubcontroller! + private var popUpButtonConstraints = [NSLayoutConstraint]() + private let columnPopUpButtonMap = NSMapTable(keyOptions: .weakMemory, + valueOptions: .weakMemory) - newColumnSegmentedControl.setEnabled(!newColumnMenu.items.isEmpty, forSegment: 0) + // MARK: Toolbar Sidebar Controls - NSLayoutConstraint.activate(popUpButtonConstraints) - } + private var sidebarNavigationSegmentedControl: NSSegmentedControl = makeSidebarNavigationSegmentedControl() + private var sidebarTitleViewController = SidebarTitleViewController() + private var sidebarTitleViewCenterXConstraint: NSLayoutConstraint? + private var closeSidebarSegmentedControl: NSSegmentedControl? - func buildColumnsPopupButtonMenu(currentColumnMode: ColumnMode, - takenModes: [ColumnMode], - index: Int) -> NSMenu - { - let followedLists = currentAccount?.followedLists + // MARK: Sidebar - logger.debug2("Building columns popup menu. Followed lists: \(followedLists?.count ?? 0)") + private lazy var sidebarSubcontroller = SidebarSubcontroller(sidebarContainer: self, + navigationControl: sidebarNavigationSegmentedControl, + navigationStack: nil) - var menuItemSection = 0 + var sidebarViewController: SidebarViewController? { + get { return timelinesSplitViewController.sidebarViewController } + set { timelinesSplitViewController.sidebarViewController = newValue } + } + + // MARK: Child View Controllers + + private var placeholderViewController: NSViewController? + private var searchWindowController: NSWindowController? + + // MARK: Lifecycle Support + + private var preservedWindowFrameStack: Stack = [] + + var currentInstance: Instance? { + didSet { + if let sidebarMode = sidebarSubcontroller.navigationStack?.currentItem, + sidebarViewController == nil + { + let oldStack = preservedWindowFrameStack + sidebarSubcontroller.installSidebar(mode: sidebarMode) + preservedWindowFrameStack = oldStack + } + } + } + + var firstColumnFrame: NSRect? { + if timelinesViewController.columnViewControllers.count == 0 { + return nil + } + + return timelinesViewController.columnViewControllers[0].view.frame + } + + private(set) var client: ClientType? { + didSet { + guard AppDelegate.shared.appIsReady else { return } + + timelinesViewController.columnViewControllers.forEach { $0.client = client } + revalidateSidebarAccountReference() + } + } + + var currentUser: UUID? { + get { return currentAccount?.uuid } + set { + currentAccount = newValue.flatMap { accountsService.account(with: $0) } + + updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) + } + } + + var currentAccount: AuthorizedAccount? { + didSet { + let hasUser: Bool + + if let currentAccount = currentAccount { + hasUser = true + let client = Client.create(for: currentAccount) + + if let window = window { + if let instance = currentAccount.baseDomain { + window.title = "@\(currentAccount.username!) — \(instance)" + } + else { + window.title = "@\(currentAccount.username!)" + } + } + + removePlaceholderIfInstalled() + updateUserPopUpButton() + + instanceService.instance(for: currentAccount) { + [weak self] instance in + DispatchQueue.main.async { + self?.client = client + self?.currentInstance = instance + } + } + + accountObservations.observe(currentAccount, \.bookmarkedTags) { + _, _ in AppDelegate.shared.updateAccountsMenu() + } + } + else { + hasUser = false + client = nil + currentInstance = nil + accountObservations.removeAll() + sidebarSubcontroller.uninstallSidebar() + installPlaceholder() + window?.title = 🔠("Mastonaut — No Account Selected") + } + + searchSegmentedControl.isHidden = !hasUser + statusComposerSegmentedControl.isHidden = !hasUser + newColumnSegmentedControl.isHidden = !hasUser + timelinesViewController.columnViewControllers.forEach { columnPopUpButtonMap.object(forKey: $0)?.isHidden = !hasUser } + columnPopUpButtonMap.objectEnumerator()?.forEach { ($0 as? NSControl)?.isHidden = !hasUser } + + invalidateRestorableState() + + if window?.isKeyWindow == true { + AppDelegate.shared.updateAccountsMenu() + } + } + } + + var hasNotificationsColumn: Bool { + for controller in timelinesViewController.columnViewControllers { + if case .some(ColumnMode.notifications) = controller.modelRepresentation { + return true + } + } + + return false + } + + private var timelinesSplitViewController: TimelinesSplitViewController { + return contentViewController as! TimelinesSplitViewController + } + + private var timelinesViewController: TimelinesViewController { + return timelinesSplitViewController.children.first as! TimelinesViewController + } + + private lazy var accountMenuItems: [NSMenuItem] = { + [ + NSMenuItem(title: 🔠("View Profile"), + action: #selector(showUserProfile(_:)), + keyEquivalent: ""), + NSMenuItem(title: 🔠("Open Profile in Browser"), + action: #selector(openUserProfileInBrowser(_:)), + keyEquivalent: ""), + NSMenuItem(title: 🔠("View Favorites"), + action: #selector(showUserFavorites(_:)), + keyEquivalent: "F").with(modifierMask: [.command, .shift]), + .separator() + ] + }() + + // MARK: Window Controller Lifecycle + + func prepareAsEmptyWindow() { + if Preferences.newWindowAccountMode == .pickFirstOne, let account = accounts.first { + currentAccount = account + } + else { + currentAccount = nil + } + + appendColumnIfFitting(model: ColumnMode.timeline) + } + + override func encodeRestorableState(with coder: NSCoder) { + let columnModels = timelinesViewController.columnViewControllers.compactMap { $0.modelRepresentation } + let encodedColumnModels = columnModels.map { $0.rawValue }.joined(separator: ";") + + coder.encode(currentAccount?.uuid, forKey: CodingKeys.currentUser) + coder.encode(encodedColumnModels, forKey: CodingKeys.columns) + coder.encode(preservedWindowFrameStack.map { NSValue(rect: $0) }, forKey: CodingKeys.preservedWindowFrameStack) + + if let navigationStack = sidebarSubcontroller.navigationStack { + // HOTFIX: Swift classes with parameter types do not encode properly in *Release* + // Since we know the type in advance, we use a separate archiver for the navigation stack which skips + // the class name level and encodes only the internals. + let encoder = NSKeyedArchiver(requiringSecureCoding: false) + navigationStack.encode(with: encoder) + coder.encode(encoder.encodedData, forKey: CodingKeys.sidebarNavigationStack) + + // coder.encode(navigationStack, forKey: CodingKeys.sidebarNavigationStack) + } + } + + override func restoreState(with coder: NSCoder) { + if let uuid: UUID = coder.decodeObject(forKey: CodingKeys.currentUser), + let account = accountsService.account(with: uuid) + { + currentAccount = account + } + else { + currentAccount = nil + } + + if let encodedColumnModels: String = coder.decodeObject(forKey: CodingKeys.columns) { + let columnModels = encodedColumnModels.split(separator: ";") + .compactMap { ColumnMode(rawValue: String($0)) } + + for model in columnModels { + appendColumnIfFitting(model: model, expand: false) + } + } + + if let frameStack: [NSValue] = coder.decodeObject(forKey: CodingKeys.preservedWindowFrameStack) { + preservedWindowFrameStack = Stack(frameStack.compactMap { $0.rectValue }) + } + else { + preservedWindowFrameStack = [] + } + + if timelinesViewController.columnViewControllers.isEmpty { + // Fallback if no columns were installed from decoding + appendColumnIfFitting(model: ColumnMode.timeline) + } + + // HOTFIX: Swift classes with parameter types do not encode properly in *Release* + // Since we know the type in advance, we use a separate archiver for the navigation stack which skips + // the class name level and encodes only the internals. + // if let stack: NavigationStack = coder.decodeObject(forKey: CodingKeys.sidebarNavigationStack) + if let stackEncodedData: Data = coder.decodeObject(forKey: CodingKeys.sidebarNavigationStack) { + let decoder = NSKeyedUnarchiver(forReadingWith: stackEncodedData) + if let stack = NavigationStack(coder: decoder) { + timelinesSplitViewController.preserveSplitViewSizeForNextSidebarInstall = true + sidebarSubcontroller = SidebarSubcontroller(sidebarContainer: self, + navigationControl: sidebarNavigationSegmentedControl, + navigationStack: stack) + } + } + + updateUserPopUpButton() + } + + override func windowDidLoad() { + super.windowDidLoad() + + logger = Logger(subsystemType: self) + + shouldCascadeWindows = true + + window?.restorationClass = TimelinesWindowRestoration.self + + newColumnSegmentedControl.setMenu(newColumnMenu, forSegment: 0) + + userPopUpButtonController = UserPopUpButtonSubcontroller(display: self) + + observations.observe(on: .main, timelinesViewController, \TimelinesViewController.columnViewControllersCount) { + [weak self] timelinesViewController, _ in + self?.updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) + self?.newColumnSegmentedControl.setEnabled(timelinesViewController.canAppendStatusList, forSegment: 0) + self?.invalidateRestorableState() + } + + observations.observe(AppDelegate.shared, \AppDelegate.appIsReady) { + [weak self] appDelegate, _ in + + if appDelegate.appIsReady, let client = self?.client { + self?.timelinesViewController.columnViewControllers.forEach { $0.client = client } + self?.revalidateSidebarAccountReference() + } + } + + guard let window = window else { return } + + window.backgroundColor = .timelineBackground + } + + func handleDetach() { + for _ in 0 ..< timelinesViewController.columnViewControllersCount { + removeColumn(at: 0, contract: false) + } + } + + // MARK: UI Handling + + func updateUserPopUpButton() { + userPopUpButtonController.updateUserPopUpButton() + } + + func shouldChangeCurrentUser(to userUUID: UUID) -> Bool { + return true + } + + func redraft(status: Status) { + AppDelegate.shared.composeStatus(self) + guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } + composerWindowController.showWindow(nil) + composerWindowController.setUpAsRedraft(of: status, using: currentAccount) + } + + func edit(status: Status) { + AppDelegate.shared.composeStatus(self) + guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } + composerWindowController.showWindow(nil) + composerWindowController.setUpAsEdit(of: status, using: currentAccount) + } + + func composeReply(for status: Status, sender: Any?) { + AppDelegate.shared.composeStatus(self) + guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } + composerWindowController.showWindow(sender) + composerWindowController.setupAsReply(to: status, using: currentAccount, senderWindowController: self) + } + + func composeMention(userHandle: String, directMessage: Bool) { + AppDelegate.shared.composeStatus(self) + guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } + composerWindowController.showWindow(nil) + + composerWindowController.setupAsMention(handle: userHandle, using: currentAccount, directMessage: directMessage) + } + + func installPlaceholder() { + guard let contentView = contentViewController?.view, placeholderViewController == nil else { return } + + let accounts = accountsService.authorizedAccounts + let viewController: NSViewController + + if accounts.isEmpty { + // Show welcome placeholder + viewController = WelcomePlaceholderController() + } + else { + // Show account picker placeholder + viewController = AccountsPlaceholderController() + } + + contentView.subviews.forEach { $0.isHidden = true } + + contentViewController?.addChild(viewController) + contentView.addSubview(viewController.view) + placeholderViewController = viewController + + NSLayoutConstraint.activate([ + contentView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), + contentView.topAnchor.constraint(equalTo: viewController.view.topAnchor), + contentView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) + ]) + } + + func removePlaceholderIfInstalled() { + placeholderViewController?.view.removeFromSuperview() + placeholderViewController?.removeFromParent() + placeholderViewController = nil + + contentViewController?.view.subviews.forEach { $0.isHidden = false } + } + + func presentSearchWindow() { + let storyboard = NSStoryboard(name: "Search", bundle: .main) + + guard + currentInstance != nil, + let account = currentAccount, + let client = client, + let searchWindowController = storyboard.instantiateInitialController() as? SearchWindowController, + let searchWindow = searchWindowController.window, + let timelinesWindow = window + else { + return + } + + searchWindowController.set(client: client) + searchWindowController.set(searchDelegate: self) + + AppDelegate.shared.instanceService.instance(for: account) { + [weak self] instance in + + guard let instance = instance else { return } + + searchWindowController.set(instance: instance) + self?.searchWindowController = searchWindowController + + timelinesWindow.beginSheet(searchWindow) { + _ in self?.searchWindowController = nil + } + } + } + + func adjustWindowFrame(adjustment: WindowSizeAdjustment) { + guard + let window = window, + window.styleMask.contains(.fullScreen) == false, + let screen = window.screen + else { return } + + let originalWindowFrame = window.frame + var frame = originalWindowFrame + + switch adjustment { + case .nudgeIfClipped: + let excessWidth = window.frame.maxX - screen.frame.maxX + guard excessWidth > 0 else { return } + frame.origin.x -= excessWidth + + case .expand(by: let extraWidth): + preservedWindowFrameStack.push(originalWindowFrame) + + if originalWindowFrame.maxX + extraWidth <= screen.frame.maxX { + frame.size.width = originalWindowFrame.width + extraWidth + } + else { + frame.size.width = screen.frame.maxX - window.frame.origin.x + } + + if let contentView = window.contentView { + let contentFrame = window.contentRect(forFrameRect: frame) + contentView.setFrameSize(contentFrame.size) + contentView.layoutSubtreeIfNeeded() + + let difference = contentView.frame.width - contentFrame.width + + if difference > 0 { + frame.origin.x -= difference + } + } + + case .contract(by: let extraWidth, let tryPoppingPreservedFrame): + if tryPoppingPreservedFrame, let preservedFrame = preservedWindowFrameStack.popIfNotEmpty() { + frame = preservedFrame + } + else { + frame.size.width -= extraWidth + } + + case .restorePreservedOriginIfPossible: + if let preservedFrame = preservedWindowFrameStack.popIfNotEmpty() { + frame.origin = preservedFrame.origin + } + } + + window.animator().setFrame(frame, display: false) + } + + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + + if preservedWindowFrameStack.isEmpty == false { + preservedWindowFrameStack = [] + } + } + + enum WindowSizeAdjustment { + case expand(by: CGFloat) + case nudgeIfClipped + case contract(by: CGFloat, poppingPreservedFrameIfPossible: Bool) + case restorePreservedOriginIfPossible + } + + // MARK: ToolbarWindowController + + func didToggleToolbarShown(_ window: ToolbarWindow) { + if window.toolbar?.isVisible == true { + updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) + } + } + + // MARK: Internal helper methods + + private func revalidateSidebarAccountReference() { + if let accountBoundSidebar = timelinesSplitViewController.sidebarViewController as? AccountBound, + let currentAccount = accountBoundSidebar.account, + let instance = currentInstance, + let client = client + { + ResolverService(client: client).resolve(account: currentAccount, activeInstance: instance) { + [weak self] result in + + DispatchQueue.main.async { + switch result { + case .success(let account): + if AppDelegate.shared.appIsReady { + self?.timelinesSplitViewController.sidebarViewController?.client = client + } + accountBoundSidebar.setRecreatedAccount(account) + self?.invalidateRestorableState() + + case .failure(let error): + self?.displayError(error) + self?.sidebarSubcontroller.uninstallSidebar() + } + } + } + } + else { + timelinesSplitViewController.sidebarViewController?.client = client + } + } + + private func installPersistentToolbarButtons(toolbarView: NSView) { + var constraints: [NSLayoutConstraint] = [] + let contentView = timelinesViewController.mainContentView + + [currentUserPopUpButton, statusComposerSegmentedControl, searchSegmentedControl, newColumnSegmentedControl].forEach { + toolbarView.addSubview($0) + let referenceView = toolbarView.superview ?? toolbarView + constraints.append(referenceView.centerYAnchor.constraint(equalTo: $0.centerYAnchor)) + } + + constraints.append(TrackingLayoutConstraint.constraint(trackingMaxXOf: contentView, + targetView: newColumnSegmentedControl, + containerView: toolbarView, + targetAttribute: .trailing, + containerAttribute: .leading) + .with(priority: .defaultLow)) + + constraints.append(contentsOf: [ + currentUserPopUpButton.leadingAnchor.constraint(equalTo: toolbarView.leadingAnchor, constant: 6), + searchSegmentedControl.leadingAnchor.constraint(equalTo: statusComposerSegmentedControl.trailingAnchor, + constant: 8), + newColumnSegmentedControl.leadingAnchor.constraint(equalTo: searchSegmentedControl.trailingAnchor, + constant: 8), + toolbarView.trailingAnchor.constraint(greaterThanOrEqualTo: newColumnSegmentedControl.trailingAnchor, constant: 6) + ]) + + NSLayoutConstraint.activate(constraints) + } + + private func updateColumnsPopUpButtons(for columnViewControllers: [ColumnViewController]) { + guard let toolbarView = toolbarContainerView?.superview else { return } + + NSLayoutConstraint.deactivate(popUpButtonConstraints) + popUpButtonConstraints.removeAll() + + let takenModes = columnViewControllers.compactMap { $0.modelRepresentation as? ColumnMode } + + var previousButton = currentUserPopUpButton + + // Install column buttons + for (index, column) in columnViewControllers.enumerated() { + guard let popUpButton = columnPopUpButtonMap.object(forKey: column) else { + continue + } + + guard let currentModel = column.modelRepresentation as? ColumnMode else { + continue + } + + let popupButtonMenu = buildColumnsPopupButtonMenu(currentColumnMode: currentModel, + takenModes: takenModes, + index: index) + + popUpButton.menu = popupButtonMenu + popUpButton.tag = index + popUpButton.select(popupButtonMenu.item(withRepresentedObject: currentModel)) + + popUpButtonConstraints.append(TrackingLayoutConstraint + .constraint(trackingMidXOf: column.view, + targetView: popUpButton, + containerView: toolbarView, + targetAttribute: .centerX, + containerAttribute: .leading) + .with(priority: .defaultLow + 248)) + + popUpButtonConstraints.append(popUpButton.leadingAnchor.constraint( + greaterThanOrEqualTo: previousButton.trailingAnchor, + constant: 8 + )) + + previousButton = popUpButton + } + + if previousButton != currentUserPopUpButton { + popUpButtonConstraints.append(statusComposerSegmentedControl.leadingAnchor.constraint( + greaterThanOrEqualTo: previousButton.trailingAnchor, + constant: 8 + )) + } + + let newColumnMenuItems = buildNewColumnMenuItems(takenModes: takenModes) + + newColumnMenu.setItems(newColumnMenuItems) + + newColumnSegmentedControl.setEnabled(!newColumnMenu.items.isEmpty, forSegment: 0) - let staticColumnModes = ColumnMode.staticItems + NSLayoutConstraint.activate(popUpButtonConstraints) + } + + func buildColumnsPopupButtonMenu(currentColumnMode: ColumnMode, + takenModes: [ColumnMode], + index: Int) -> NSMenu + { + let followedLists = currentAccount?.followedLists + + logger.debug2("Building columns popup menu. Followed lists: \(followedLists?.count ?? 0)") + + var menuItemSection = 0 + + let staticColumnModes = ColumnMode.staticItems - let menu = NSMenu(title: "") + let menu = NSMenu(title: "") - menu.autoenablesItems = false + menu.autoenablesItems = false - var items: [NSMenuItem] = staticColumnModes.filter { !takenModes.contains($0) } - .map { $0.makeMenuItemForChanging(with: self, columnId: index) } + var items: [NSMenuItem] = staticColumnModes.filter { !takenModes.contains($0) } + .map { $0.makeMenuItemForChanging(with: self, columnId: index) } - if currentColumnMode.menuItemSection == menuItemSection { - items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index)) - } + if currentColumnMode.menuItemSection == menuItemSection { + items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index)) + } - items.sort(by: { $0.columnModel! < $1.columnModel! }) + items.sort(by: { $0.columnModel! < $1.columnModel! }) - menuItemSection = 1 + menuItemSection = 1 - items.append(.separator()) + items.append(.separator()) - let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) } - .map { $0.makeMenuItemForChanging(with: self, columnId: index) } + let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) } + .map { $0.makeMenuItemForChanging(with: self, columnId: index) } - for _item in personalItems { - items.append(_item) - } + for _item in personalItems { + items.append(_item) + } - if currentColumnMode.menuItemSection == menuItemSection { - items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index)) - } + if currentColumnMode.menuItemSection == menuItemSection { + items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index)) + } - var listItems: [NSMenuItem] = [] - var haveAtLeastOneList = false + var listItems: [NSMenuItem] = [] + var haveAtLeastOneList = false - if let followedLists = followedLists { - if followedLists.count > 0 { - listItems.append(.separator()) - listItems.append(.sectionHeader(🔠("Lists"))) + if let followedLists = followedLists { + if followedLists.count > 0 { + listItems.append(.separator()) + listItems.append(.sectionHeader(🔠("Lists"))) - for _list in followedLists { - if let list = _list as? FollowedList { - let columnMode = ColumnMode.list(list: list) + for _list in followedLists { + if let list = _list as? FollowedList { + let columnMode = ColumnMode.list(list: list) - listItems.append(columnMode.makeMenuItemForChanging(with: self, columnId: index)) - haveAtLeastOneList = true - } - } - } - } + listItems.append(columnMode.makeMenuItemForChanging(with: self, columnId: index)) + haveAtLeastOneList = true + } + } + } + } - if haveAtLeastOneList { - items.append(contentsOf: listItems) - } + if haveAtLeastOneList { + items.append(contentsOf: listItems) + } - items.append(.separator()) + items.append(.separator()) items.append(.sectionHeader(🔠("This column"))) - let reloadColumnItem = NSMenuItem() + let reloadColumnItem = NSMenuItem() reloadColumnItem.title = 🔠("Reload") - reloadColumnItem.target = self - reloadColumnItem.representedObject = index - reloadColumnItem.action = #selector(TimelinesWindowController.reloadColumn(_:)) - items.append(reloadColumnItem) + reloadColumnItem.target = self + reloadColumnItem.representedObject = index + reloadColumnItem.action = #selector(TimelinesWindowController.reloadColumn(_:)) + items.append(reloadColumnItem) - if index > 0 { + if index > 0 { let rearrangeColumnsItem = NSMenuItem() rearrangeColumnsItem.title = 🔠("Rearrange…") rearrangeColumnsItem.target = self @@ -715,401 +715,401 @@ class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, rearrangeColumnsItem.action = #selector(TimelinesWindowController.rearrangeColumns(_:)) items.append(rearrangeColumnsItem) - let removeColumnItem = NSMenuItem() + let removeColumnItem = NSMenuItem() removeColumnItem.title = 🔠("Remove") - removeColumnItem.target = self - removeColumnItem.representedObject = index - removeColumnItem.action = #selector(TimelinesWindowController.removeColumn(_:)) - items.append(removeColumnItem) - } + removeColumnItem.target = self + removeColumnItem.representedObject = index + removeColumnItem.action = #selector(TimelinesWindowController.removeColumn(_:)) + items.append(removeColumnItem) + } - menu.setItems(items) + menu.setItems(items) - return menu - } + return menu + } - func buildNewColumnMenuItems(takenModes: [ColumnMode]) -> [NSMenuItem] { - var items: [NSMenuItem] = ColumnMode.staticItems.filter { !takenModes.contains($0) } - .map { $0.makeMenuItemForAdding(with: self) } + func buildNewColumnMenuItems(takenModes: [ColumnMode]) -> [NSMenuItem] { + var items: [NSMenuItem] = ColumnMode.staticItems.filter { !takenModes.contains($0) } + .map { $0.makeMenuItemForAdding(with: self) } - items.append(.separator()) + items.append(.separator()) - let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) } - .map { $0.makeMenuItemForAdding(with: self) } + let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) } + .map { $0.makeMenuItemForAdding(with: self) } - for _item in personalItems { - items.append(_item) - } + for _item in personalItems { + items.append(_item) + } - let followedLists = currentAccount?.followedLists + let followedLists = currentAccount?.followedLists - logger.debug2("Building new column menu. Followed lists: \(followedLists?.count ?? 0)") + logger.debug2("Building new column menu. Followed lists: \(followedLists?.count ?? 0)") - var listItems: [NSMenuItem] = [] - var haveAtLeastOneList = false + var listItems: [NSMenuItem] = [] + var haveAtLeastOneList = false - if let followedLists = followedLists { - if followedLists.count > 0 { - listItems.append(.separator()) - listItems.append(.sectionHeader(🔠("Lists"))) + if let followedLists = followedLists { + if followedLists.count > 0 { + listItems.append(.separator()) + listItems.append(.sectionHeader(🔠("Lists"))) - for _list in followedLists { - if let list = _list as? FollowedList { - let columnMode = ColumnMode.list(list: list) + for _list in followedLists { + if let list = _list as? FollowedList { + let columnMode = ColumnMode.list(list: list) - if !takenModes.contains(columnMode) { - listItems.append(columnMode.makeMenuItemForAdding(with: self)) - haveAtLeastOneList = true - } - } - } - } - } + if !takenModes.contains(columnMode) { + listItems.append(columnMode.makeMenuItemForAdding(with: self)) + haveAtLeastOneList = true + } + } + } + } + } - if haveAtLeastOneList { - items.append(contentsOf: listItems) - } + if haveAtLeastOneList { + items.append(contentsOf: listItems) + } - return items - } + return items + } - // MARK: - Keyboard Navigation + // MARK: - Keyboard Navigation - override func moveRight(_ sender: Any?) { - timelinesViewController.makeNextColumnFirstResponder() - } + override func moveRight(_ sender: Any?) { + timelinesViewController.makeNextColumnFirstResponder() + } - override func moveDown(_ sender: Any?) { - timelinesViewController.makeNextColumnFirstResponder() - } + override func moveDown(_ sender: Any?) { + timelinesViewController.makeNextColumnFirstResponder() + } - override func moveUp(_ sender: Any?) { - timelinesViewController.makeNextColumnFirstResponder() - } + override func moveUp(_ sender: Any?) { + timelinesViewController.makeNextColumnFirstResponder() + } - override func moveLeft(_ sender: Any?) { - timelinesViewController.makePreviousColumnFirstResponder() - } + override func moveLeft(_ sender: Any?) { + timelinesViewController.makePreviousColumnFirstResponder() + } } extension TimelinesWindowController: SidebarContainer { - func willInstallSidebar(viewController: NSViewController) { - if let currentWindowFrame = window?.frame { - preservedWindowFrameStack.push(currentWindowFrame) - } - - contentViewController?.addChild(viewController) - } - - func didInstallSidebar(viewController: NSViewController, with mode: SidebarMode) { - guard - let toolbarView = toolbarContainerView, - closeSidebarSegmentedControl?.superview == nil, - sidebarNavigationSegmentedControl.superview == nil, - let titleMode = sidebarViewController?.titleMode - else { - invalidateRestorableState() - return - } - - let navigationControl = sidebarNavigationSegmentedControl - - let closeSidebarButton = makeCloseSidebarButton() - toolbarView.addSubview(closeSidebarButton) - toolbarView.addSubview(navigationControl) - - let titleView = sidebarTitleViewController.view - sidebarTitleViewController.titleMode = titleMode - toolbarView.addSubview(titleView) - - let leadingConstraint = TrackingLayoutConstraint.constraint( - trackingMaxXOf: timelinesViewController.mainContentView, - offset: timelinesSplitViewController.splitView.dividerThickness, - targetView: navigationControl, - containerView: toolbarView, - targetAttribute: .leading, - containerAttribute: .leading - ).with(priority: .defaultLow + 1) - - let centerConstraint = TrackingLayoutConstraint.constraint( - trackingMidXOf: sidebarViewController!.view.firstParentViewInsideSplitView(), - offset: timelinesSplitViewController.splitView.dividerThickness, - targetView: titleView, - containerView: toolbarView, - targetAttribute: .centerX, - containerAttribute: .leading - ).with(priority: .defaultLow + 1) - - sidebarTitleViewCenterXConstraint = centerConstraint - - NSLayoutConstraint.activate([ - toolbarView.trailingAnchor.constraint(equalTo: closeSidebarButton.trailingAnchor, constant: 8), - leadingConstraint, - navigationControl.leadingAnchor.constraint(greaterThanOrEqualTo: newColumnSegmentedControl.trailingAnchor, - constant: 10), - - titleView.leadingAnchor.constraint(greaterThanOrEqualTo: navigationControl.trailingAnchor, constant: 8), - closeSidebarButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleView.trailingAnchor, constant: 8), - centerConstraint, - - toolbarView.centerYAnchor.constraint(equalTo: closeSidebarButton.centerYAnchor), - toolbarView.centerYAnchor.constraint(equalTo: navigationControl.centerYAnchor), - toolbarView.centerYAnchor.constraint(equalTo: titleView.centerYAnchor) - ]) - - closeSidebarSegmentedControl = closeSidebarButton - - invalidateRestorableState() - } - - func didUpdateSidebar(viewController: NSViewController, previousViewController: NSViewController, with mode: SidebarMode) { - guard let toolbarView = toolbarContainerView, - let sidebarViewController = sidebarViewController - else { return } - - sidebarTitleViewController.titleMode = sidebarViewController.titleMode - - sidebarTitleViewCenterXConstraint?.isActive = false - - let centerConstraint = TrackingLayoutConstraint.constraint( - trackingMidXOf: sidebarViewController.view.firstParentViewInsideSplitView(), - offset: timelinesSplitViewController.splitView.dividerThickness, - targetView: sidebarTitleViewController.view, - containerView: toolbarView, - targetAttribute: .centerX, - containerAttribute: .leading - ).with(priority: .defaultLow + 1) - centerConstraint.isActive = true - - sidebarTitleViewCenterXConstraint = centerConstraint - - previousViewController.removeFromParent() - - invalidateRestorableState() - } - - func willUninstallSidebar(viewController: NSViewController) { - adjustWindowFrame(adjustment: .restorePreservedOriginIfPossible) - } - - func didUninstallSidebar(viewController: NSViewController) { - sidebarNavigationSegmentedControl.removeFromSuperview() - sidebarTitleViewController.view.removeFromSuperview() - closeSidebarSegmentedControl?.removeFromSuperview() - closeSidebarSegmentedControl = nil - - viewController.removeFromParent() - - invalidateRestorableState() - } - - func reloadSidebarTitleMode() { - sidebarTitleViewController.titleMode = sidebarViewController?.titleMode ?? .none - } - - private enum CodingKeys: String { - case currentUser - case columns - case sidebarNavigationStack - case preservedWindowFrameStack - } + func willInstallSidebar(viewController: NSViewController) { + if let currentWindowFrame = window?.frame { + preservedWindowFrameStack.push(currentWindowFrame) + } + + contentViewController?.addChild(viewController) + } + + func didInstallSidebar(viewController: NSViewController, with mode: SidebarMode) { + guard + let toolbarView = toolbarContainerView, + closeSidebarSegmentedControl?.superview == nil, + sidebarNavigationSegmentedControl.superview == nil, + let titleMode = sidebarViewController?.titleMode + else { + invalidateRestorableState() + return + } + + let navigationControl = sidebarNavigationSegmentedControl + + let closeSidebarButton = makeCloseSidebarButton() + toolbarView.addSubview(closeSidebarButton) + toolbarView.addSubview(navigationControl) + + let titleView = sidebarTitleViewController.view + sidebarTitleViewController.titleMode = titleMode + toolbarView.addSubview(titleView) + + let leadingConstraint = TrackingLayoutConstraint.constraint( + trackingMaxXOf: timelinesViewController.mainContentView, + offset: timelinesSplitViewController.splitView.dividerThickness, + targetView: navigationControl, + containerView: toolbarView, + targetAttribute: .leading, + containerAttribute: .leading + ).with(priority: .defaultLow + 1) + + let centerConstraint = TrackingLayoutConstraint.constraint( + trackingMidXOf: sidebarViewController!.view.firstParentViewInsideSplitView(), + offset: timelinesSplitViewController.splitView.dividerThickness, + targetView: titleView, + containerView: toolbarView, + targetAttribute: .centerX, + containerAttribute: .leading + ).with(priority: .defaultLow + 1) + + sidebarTitleViewCenterXConstraint = centerConstraint + + NSLayoutConstraint.activate([ + toolbarView.trailingAnchor.constraint(equalTo: closeSidebarButton.trailingAnchor, constant: 8), + leadingConstraint, + navigationControl.leadingAnchor.constraint(greaterThanOrEqualTo: newColumnSegmentedControl.trailingAnchor, + constant: 10), + + titleView.leadingAnchor.constraint(greaterThanOrEqualTo: navigationControl.trailingAnchor, constant: 8), + closeSidebarButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleView.trailingAnchor, constant: 8), + centerConstraint, + + toolbarView.centerYAnchor.constraint(equalTo: closeSidebarButton.centerYAnchor), + toolbarView.centerYAnchor.constraint(equalTo: navigationControl.centerYAnchor), + toolbarView.centerYAnchor.constraint(equalTo: titleView.centerYAnchor) + ]) + + closeSidebarSegmentedControl = closeSidebarButton + + invalidateRestorableState() + } + + func didUpdateSidebar(viewController: NSViewController, previousViewController: NSViewController, with mode: SidebarMode) { + guard let toolbarView = toolbarContainerView, + let sidebarViewController = sidebarViewController + else { return } + + sidebarTitleViewController.titleMode = sidebarViewController.titleMode + + sidebarTitleViewCenterXConstraint?.isActive = false + + let centerConstraint = TrackingLayoutConstraint.constraint( + trackingMidXOf: sidebarViewController.view.firstParentViewInsideSplitView(), + offset: timelinesSplitViewController.splitView.dividerThickness, + targetView: sidebarTitleViewController.view, + containerView: toolbarView, + targetAttribute: .centerX, + containerAttribute: .leading + ).with(priority: .defaultLow + 1) + centerConstraint.isActive = true + + sidebarTitleViewCenterXConstraint = centerConstraint + + previousViewController.removeFromParent() + + invalidateRestorableState() + } + + func willUninstallSidebar(viewController: NSViewController) { + adjustWindowFrame(adjustment: .restorePreservedOriginIfPossible) + } + + func didUninstallSidebar(viewController: NSViewController) { + sidebarNavigationSegmentedControl.removeFromSuperview() + sidebarTitleViewController.view.removeFromSuperview() + closeSidebarSegmentedControl?.removeFromSuperview() + closeSidebarSegmentedControl = nil + + viewController.removeFromParent() + + invalidateRestorableState() + } + + func reloadSidebarTitleMode() { + sidebarTitleViewController.titleMode = sidebarViewController?.titleMode ?? .none + } + + private enum CodingKeys: String { + case currentUser + case columns + case sidebarNavigationStack + case preservedWindowFrameStack + } } extension TimelinesWindowController { - func appendColumnIfFitting(model: ColumnModel, expand: Bool = true) { - guard - let columnViewController = timelinesViewController.appendColumnIfFitting(model: model, expand: expand) - else { - return - } - - columnViewController.client = AppDelegate.shared.appIsReady ? client : nil - - guard let toolbarView = toolbarContainerView else { - return - } - - let popUpButton = NSPopUpButton(frame: .zero, pullsDown: false) - popUpButton.bezelStyle = .texturedRounded - popUpButton.translatesAutoresizingMaskIntoConstraints = false - popUpButton.isHidden = currentUser == nil - popUpButton.setContentCompressionResistancePriority(.defaultLow + 249, for: .horizontal) - toolbarView.addSubview(popUpButton) - - let anyColumnPopUpButton = columnPopUpButtonMap.objectEnumerator()?.nextObject() as? NSPopUpButton - - columnPopUpButtonMap.setObject(popUpButton, forKey: columnViewController) - - NSLayoutConstraint.activate([ - toolbarView.centerYAnchor.constraint(equalTo: popUpButton.centerYAnchor), - popUpButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 40).with(priority: .defaultHigh) - ]) - - if let otherPopUpButton = anyColumnPopUpButton { - otherPopUpButton.widthAnchor.constraint(equalTo: popUpButton.widthAnchor).isActive = true - } - } - - func replaceColumn(at columnIndex: Int, with newViewController: ColumnViewController) { - let oldViewController = timelinesViewController.replaceColumn(at: columnIndex, with: newViewController) - let popUpButton = columnPopUpButtonMap.object(forKey: oldViewController)! - - columnPopUpButtonMap.removeObject(forKey: oldViewController) - columnPopUpButtonMap.setObject(popUpButton, forKey: newViewController) - - newViewController.client = client - } - - func removeColumn(at columnIndex: Int, contract: Bool) { - let columnViewController = timelinesViewController.removeColumn(at: columnIndex, contract: true) - let popUpButton = columnPopUpButtonMap.object(forKey: columnViewController)! - - columnPopUpButtonMap.removeObject(forKey: columnViewController) - popUpButton.removeFromSuperview() - } - - func reloadColumn(at columnIndex: Int) { - timelinesViewController.reloadColumn(at: columnIndex) - updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) - } + func appendColumnIfFitting(model: ColumnModel, expand: Bool = true) { + guard + let columnViewController = timelinesViewController.appendColumnIfFitting(model: model, expand: expand) + else { + return + } + + columnViewController.client = AppDelegate.shared.appIsReady ? client : nil + + guard let toolbarView = toolbarContainerView else { + return + } + + let popUpButton = NSPopUpButton(frame: .zero, pullsDown: false) + popUpButton.bezelStyle = .texturedRounded + popUpButton.translatesAutoresizingMaskIntoConstraints = false + popUpButton.isHidden = currentUser == nil + popUpButton.setContentCompressionResistancePriority(.defaultLow + 249, for: .horizontal) + toolbarView.addSubview(popUpButton) + + let anyColumnPopUpButton = columnPopUpButtonMap.objectEnumerator()?.nextObject() as? NSPopUpButton + + columnPopUpButtonMap.setObject(popUpButton, forKey: columnViewController) + + NSLayoutConstraint.activate([ + toolbarView.centerYAnchor.constraint(equalTo: popUpButton.centerYAnchor), + popUpButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 40).with(priority: .defaultHigh) + ]) + + if let otherPopUpButton = anyColumnPopUpButton { + otherPopUpButton.widthAnchor.constraint(equalTo: popUpButton.widthAnchor).isActive = true + } + } + + func replaceColumn(at columnIndex: Int, with newViewController: ColumnViewController) { + let oldViewController = timelinesViewController.replaceColumn(at: columnIndex, with: newViewController) + let popUpButton = columnPopUpButtonMap.object(forKey: oldViewController)! + + columnPopUpButtonMap.removeObject(forKey: oldViewController) + columnPopUpButtonMap.setObject(popUpButton, forKey: newViewController) + + newViewController.client = client + } + + func removeColumn(at columnIndex: Int, contract: Bool) { + let columnViewController = timelinesViewController.removeColumn(at: columnIndex, contract: true) + let popUpButton = columnPopUpButtonMap.object(forKey: columnViewController)! + + columnPopUpButtonMap.removeObject(forKey: columnViewController) + popUpButton.removeFromSuperview() + } + + func reloadColumn(at columnIndex: Int) { + timelinesViewController.reloadColumn(at: columnIndex) + updateColumnsPopUpButtons(for: timelinesViewController.columnViewControllers) + } } extension TimelinesWindowController: NSWindowDelegate { - func windowDidChangeOcclusionState(_ notification: Foundation.Notification) { - guard let occlusionState = window?.occlusionState else { return } - timelinesViewController.columnViewControllers.forEach { $0.containerWindowOcclusionStateDidChange(occlusionState) } - } - - func windowWillClose(_ notification: Foundation.Notification) { - sidebarSubcontroller.uninstallSidebar() - AppDelegate.shared.detachTimelinesWindow(for: self) - } + func windowDidChangeOcclusionState(_ notification: Foundation.Notification) { + guard let occlusionState = window?.occlusionState else { return } + timelinesViewController.columnViewControllers.forEach { $0.containerWindowOcclusionStateDidChange(occlusionState) } + } + + func windowWillClose(_ notification: Foundation.Notification) { + sidebarSubcontroller.uninstallSidebar() + AppDelegate.shared.detachTimelinesWindow(for: self) + } } extension TimelinesWindowController: NSMenuItemValidation { - func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { - if menuItem.action == #selector(TimelinesWindowController.dismissSidebar(_:)) { - return sidebarSubcontroller.sidebarMode != nil - } + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.action == #selector(TimelinesWindowController.dismissSidebar(_:)) { + return sidebarSubcontroller.sidebarMode != nil + } - return true - } + return true + } } extension TimelinesWindowController: SearchViewDelegate { - func searchView(_ searchView: SearchViewController, userDidSelect selection: SearchResultSelection) { - guard let instance = currentInstance else { return } + func searchView(_ searchView: SearchViewController, userDidSelect selection: SearchResultSelection) { + guard let instance = currentInstance else { return } - switch selection { - case .account(let account): - presentInSidebar(SidebarMode.profile(uri: account.uri(in: instance), account: account)) + switch selection { + case .account(let account): + presentInSidebar(SidebarMode.profile(uri: account.uri(in: instance), account: account)) - case .status(let status): - presentInSidebar(SidebarMode.status(uri: status.resolvableURI, status: status)) + case .status(let status): + presentInSidebar(SidebarMode.status(uri: status.resolvableURI, status: status)) - case .tag(let tagName): - presentInSidebar(SidebarMode.tag(tagName)) - } - } + case .tag(let tagName): + presentInSidebar(SidebarMode.tag(tagName)) + } + } } extension TimelinesWindowController: AccountAuthorizationSource { - var sourceWindow: NSWindow? { - return window - } + var sourceWindow: NSWindow? { + return window + } - func prepareForAuthorization() {} + func prepareForAuthorization() {} - func successfullyAuthenticatedUser(with userUUID: UUID) { - currentAccount = accountsService.account(with: userUUID) - } + func successfullyAuthenticatedUser(with userUUID: UUID) { + currentAccount = accountsService.account(with: userUUID) + } - func finalizeAuthorization() { - updateUserPopUpButton() - } + func finalizeAuthorization() { + updateUserPopUpButton() + } } extension TimelinesWindowController: AuthorizedAccountProviding { - var attachmentPresenter: AttachmentPresenting { - return timelinesViewController - } - - func presentInSidebar(_ mode: SidebarModel) { - (mode as? SidebarMode).map { sidebarSubcontroller.installSidebar(mode: $0) } - } - - func handle(linkURL: URL) { - // wrapping these in `Task{}` is potentially dangerous, but we're just opening URLs, so it's fire and forget - Task { - await MastodonURLResolver.resolve(using: client, url: linkURL, knownTags: nil, source: self) - } - } - - func handle(linkURL: URL, knownTags: [Tag]?) { - Task { - await MastodonURLResolver.resolve(using: client, url: linkURL, knownTags: knownTags, source: self) - } - } + var attachmentPresenter: AttachmentPresenting { + return timelinesViewController + } + + func presentInSidebar(_ mode: SidebarModel) { + (mode as? SidebarMode).map { sidebarSubcontroller.installSidebar(mode: $0) } + } + + func handle(linkURL: URL) { + // wrapping these in `Task{}` is potentially dangerous, but we're just opening URLs, so it's fire and forget + Task { + await MastodonURLResolver.resolve(using: client, url: linkURL, knownTags: nil, source: self) + } + } + + func handle(linkURL: URL, knownTags: [Tag]?) { + Task { + await MastodonURLResolver.resolve(using: client, url: linkURL, knownTags: knownTags, source: self) + } + } } extension TimelinesWindowController // IBActions { - @IBAction func composeStatus(_ sender: Any?) { - AppDelegate.shared.composeStatus(sender) - guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } - guard let composerWindow = composerWindowController.window else { return } - - if let composerScreen = composerWindow.screen, let timelinesScreen = window?.screen, - composerScreen !== timelinesScreen - { - // Move window to the inside of the screen where the current timelines window is - composerWindow.setFrameOrigin(timelinesScreen.visibleFrame.origin) - } - - composerWindowController.showWindow(sender) - composerWindow.center() - - composerWindowController.currentAccount = currentAccount - } - - @IBAction func showSearch(_ sender: Any?) { - presentSearchWindow() - } - - @IBAction func addColumnMode(_ sender: Any?) { - if let menuItem = sender as? NSMenuItem, let newModel = menuItem.representedObject as? ColumnMode { - appendColumnIfFitting(model: newModel) - } - else if let control = sender as? NSSegmentedControl, let event = NSApp.currentEvent { - control.menu(forSegment: 0).map { NSMenu.popUpContextMenu($0, with: event, for: control) } - } - } - - @IBAction func changeColumnMode(_ sender: Any?) { - guard - let menuItem = sender as? NSMenuItem, - let newModel = menuItem.representedObject as? ColumnMode - else { - return - } - - let columnIndex = menuItem.tag - let columnViewControllers = timelinesViewController.columnViewControllers - - guard columnIndex >= 0, columnIndex < columnViewControllers.count else { - return - } - - guard - let selectableCurrentModel = columnViewControllers[columnIndex].modelRepresentation as? ColumnMode, - selectableCurrentModel != newModel - else { - // Nothing to change - return - } - - replaceColumn(at: columnIndex, with: newModel.makeViewController()) - } + @IBAction func composeStatus(_ sender: Any?) { + AppDelegate.shared.composeStatus(sender) + guard let composerWindowController = AppDelegate.shared.statusComposerWindowControllers.last else { return } + guard let composerWindow = composerWindowController.window else { return } + + if let composerScreen = composerWindow.screen, let timelinesScreen = window?.screen, + composerScreen !== timelinesScreen + { + // Move window to the inside of the screen where the current timelines window is + composerWindow.setFrameOrigin(timelinesScreen.visibleFrame.origin) + } + + composerWindowController.showWindow(sender) + composerWindow.center() + + composerWindowController.currentAccount = currentAccount + } + + @IBAction func showSearch(_ sender: Any?) { + presentSearchWindow() + } + + @IBAction func addColumnMode(_ sender: Any?) { + if let menuItem = sender as? NSMenuItem, let newModel = menuItem.representedObject as? ColumnMode { + appendColumnIfFitting(model: newModel) + } + else if let control = sender as? NSSegmentedControl, let event = NSApp.currentEvent { + control.menu(forSegment: 0).map { NSMenu.popUpContextMenu($0, with: event, for: control) } + } + } + + @IBAction func changeColumnMode(_ sender: Any?) { + guard + let menuItem = sender as? NSMenuItem, + let newModel = menuItem.representedObject as? ColumnMode + else { + return + } + + let columnIndex = menuItem.tag + let columnViewControllers = timelinesViewController.columnViewControllers + + guard columnIndex >= 0, columnIndex < columnViewControllers.count else { + return + } + + guard + let selectableCurrentModel = columnViewControllers[columnIndex].modelRepresentation as? ColumnMode, + selectableCurrentModel != newModel + else { + // Nothing to change + return + } + + replaceColumn(at: columnIndex, with: newModel.makeViewController()) + } @IBAction private func rearrangeColumns(_ sender: Any?) { // guard @@ -1132,152 +1132,152 @@ extension TimelinesWindowController // IBActions } } - @IBAction private func removeColumn(_ sender: Any?) { - guard - let menuItem = sender as? NSMenuItem, - let columnIndex = menuItem.representedObject as? Int - else { - return - } - - removeColumn(at: columnIndex, contract: true) - } - - @IBAction private func reloadColumn(_ sender: Any?) { - guard - let menuItem = sender as? NSMenuItem, - let columnIndex = menuItem.representedObject as? Int - else { - return - } - - reloadColumn(at: columnIndex) - } - - @IBAction private func dismissSidebar(_ sender: Any?) { - sidebarSubcontroller.uninstallSidebar() - } - - @IBAction private func showUserProfile(_ sender: Any?) { - guard let accountURI = currentAccount?.uri else { return } - sidebarSubcontroller.installSidebar(mode: SidebarMode.profile(uri: accountURI)) - } - - @IBAction private func showUserFavorites(_ sender: Any?) { - sidebarSubcontroller.installSidebar(mode: SidebarMode.favorites) - } - - @IBAction private func openUserProfileInBrowser(_ sender: Any?) { - guard let account = currentAccount else { return } - accountsService.details(for: account) { - if case .success(let details) = $0 { - DispatchQueue.main.async { NSWorkspace.shared.open(details.account.url) } - } - } - } - - @IBAction func showTag(_ sender: Any?) { - if let menuItem = sender as? NSMenuItem, let tagName = menuItem.representedObject as? String { - presentInSidebar(SidebarMode.tag(tagName)) - } - } + @IBAction private func removeColumn(_ sender: Any?) { + guard + let menuItem = sender as? NSMenuItem, + let columnIndex = menuItem.representedObject as? Int + else { + return + } + + removeColumn(at: columnIndex, contract: true) + } + + @IBAction private func reloadColumn(_ sender: Any?) { + guard + let menuItem = sender as? NSMenuItem, + let columnIndex = menuItem.representedObject as? Int + else { + return + } + + reloadColumn(at: columnIndex) + } + + @IBAction private func dismissSidebar(_ sender: Any?) { + sidebarSubcontroller.uninstallSidebar() + } + + @IBAction private func showUserProfile(_ sender: Any?) { + guard let accountURI = currentAccount?.uri else { return } + sidebarSubcontroller.installSidebar(mode: SidebarMode.profile(uri: accountURI)) + } + + @IBAction private func showUserFavorites(_ sender: Any?) { + sidebarSubcontroller.installSidebar(mode: SidebarMode.favorites) + } + + @IBAction private func openUserProfileInBrowser(_ sender: Any?) { + guard let account = currentAccount else { return } + accountsService.details(for: account) { + if case .success(let details) = $0 { + DispatchQueue.main.async { NSWorkspace.shared.open(details.account.url) } + } + } + } + + @IBAction func showTag(_ sender: Any?) { + if let menuItem = sender as? NSMenuItem, let tagName = menuItem.representedObject as? String { + presentInSidebar(SidebarMode.tag(tagName)) + } + } } extension TimelinesWindowController: AccountsMenuProvider { - private var accounts: [AuthorizedAccount] { - return accountsService.authorizedAccounts - } - - var accountsMenuItems: [NSMenuItem] { - let accountItems = accounts.makeMenuItems(currentUser: currentAccount?.uuid, - action: #selector(UserPopUpButtonSubcontroller.selectAccount(_:)), - target: userPopUpButtonController, - emojiContainer: nil, - setKeyEquivalents: true).menuItems - - let bookmarkedTags = currentAccount?.bookmarkedTagsList ?? [] - var tagMenuItems: [NSMenuItem] = [] - - if bookmarkedTags.isEmpty == false { - tagMenuItems.append(.separator()) - - let bookmarkedTagItems = MenuItemFactory.makeMenuItems(forTags: bookmarkedTags, - action: #selector(showTag(_:)), - target: self) - - let menu = NSMenu(title: "") - menu.setItems(bookmarkedTagItems) - tagMenuItems.append(NSMenuItem(title: 🔠("Bookmarked Tags"), submenu: menu)) - tagMenuItems.append(.separator()) - } - - return accountMenuItems + tagMenuItems + accountItems - } + private var accounts: [AuthorizedAccount] { + return accountsService.authorizedAccounts + } + + var accountsMenuItems: [NSMenuItem] { + let accountItems = accounts.makeMenuItems(currentUser: currentAccount?.uuid, + action: #selector(UserPopUpButtonSubcontroller.selectAccount(_:)), + target: userPopUpButtonController, + emojiContainer: nil, + setKeyEquivalents: true).menuItems + + let bookmarkedTags = currentAccount?.bookmarkedTagsList ?? [] + var tagMenuItems: [NSMenuItem] = [] + + if bookmarkedTags.isEmpty == false { + tagMenuItems.append(.separator()) + + let bookmarkedTagItems = MenuItemFactory.makeMenuItems(forTags: bookmarkedTags, + action: #selector(showTag(_:)), + target: self) + + let menu = NSMenu(title: "") + menu.setItems(bookmarkedTagItems) + tagMenuItems.append(NSMenuItem(title: 🔠("Bookmarked Tags"), submenu: menu)) + tagMenuItems.append(.separator()) + } + + return accountMenuItems + tagMenuItems + accountItems + } } private extension TimelinesWindowController { - func makeCloseSidebarButton() -> NSSegmentedControl { - let button = NSSegmentedControl(images: [#imageLiteral(resourceName: "close_sidebar")], trackingMode: .momentary, - target: self, action: #selector(dismissSidebar(_:))) - button.translatesAutoresizingMaskIntoConstraints = false - return button - } - - static func makeSidebarNavigationSegmentedControl() -> NSSegmentedControl { - let segmentedControl = NSSegmentedControl(images: [NSImage(named: NSImage.goBackTemplateName)!, - NSImage(named: NSImage.goForwardTemplateName)!], - trackingMode: .momentary, target: nil, action: nil) - segmentedControl.translatesAutoresizingMaskIntoConstraints = false - return segmentedControl - } - - static func makeAccountsPopUpButton() -> NSPopUpButton { - let popUpButton = NonVibrantPopUpButton() - popUpButton.bezelStyle = .texturedRounded - popUpButton.translatesAutoresizingMaskIntoConstraints = false - popUpButton.setAccessibilityLabel("Choose account") - return popUpButton - } - - static func makeNewColumnSegmentedControl() -> NSSegmentedControl { - let image = #imageLiteral(resourceName: "add_panel") - image.accessibilityDescription = "Add new column" - - let segmentedControl = NSSegmentedControl(images: [image], trackingMode: .momentary, - target: nil, action: #selector(addColumnMode(_:))) - segmentedControl.translatesAutoresizingMaskIntoConstraints = false - return segmentedControl - } - - static func makeStatusComposerSegmentedControl() -> NSSegmentedControl { - let image = #imageLiteral(resourceName: "compose") - image.accessibilityDescription = "Compose new status" - - let segmentedControl = NSSegmentedControl(images: [image], trackingMode: .momentary, - target: nil, action: #selector(composeStatus(_:))) - segmentedControl.translatesAutoresizingMaskIntoConstraints = false - return segmentedControl - } - - static func makeSearchSegmentedControl() -> NSSegmentedControl { - let segmentedControl = NSSegmentedControl(images: [NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search")!], - trackingMode: .momentary, - target: nil, action: #selector(showSearch(_:))) - segmentedControl.translatesAutoresizingMaskIntoConstraints = false - return segmentedControl - } - - func makeToolbarContainerView() -> NSView? { - guard let toolbarView: NSView = (window as? ToolbarWindow)?.toolbarView else { - return nil - } - - guard let toolbarItemsContainer = toolbarView.findSubview(withClassName: "NSToolbarItemViewer") else { - installPersistentToolbarButtons(toolbarView: toolbarView) - return toolbarView - } - - installPersistentToolbarButtons(toolbarView: toolbarItemsContainer) - return toolbarItemsContainer.superview! - } + func makeCloseSidebarButton() -> NSSegmentedControl { + let button = NSSegmentedControl(images: [#imageLiteral(resourceName: "close_sidebar")], trackingMode: .momentary, + target: self, action: #selector(dismissSidebar(_:))) + button.translatesAutoresizingMaskIntoConstraints = false + return button + } + + static func makeSidebarNavigationSegmentedControl() -> NSSegmentedControl { + let segmentedControl = NSSegmentedControl(images: [NSImage(named: NSImage.goBackTemplateName)!, + NSImage(named: NSImage.goForwardTemplateName)!], + trackingMode: .momentary, target: nil, action: nil) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + return segmentedControl + } + + static func makeAccountsPopUpButton() -> NSPopUpButton { + let popUpButton = NonVibrantPopUpButton() + popUpButton.bezelStyle = .texturedRounded + popUpButton.translatesAutoresizingMaskIntoConstraints = false + popUpButton.setAccessibilityLabel("Choose account") + return popUpButton + } + + static func makeNewColumnSegmentedControl() -> NSSegmentedControl { + let image = #imageLiteral(resourceName: "add_panel") + image.accessibilityDescription = "Add new column" + + let segmentedControl = NSSegmentedControl(images: [image], trackingMode: .momentary, + target: nil, action: #selector(addColumnMode(_:))) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + return segmentedControl + } + + static func makeStatusComposerSegmentedControl() -> NSSegmentedControl { + let image = #imageLiteral(resourceName: "compose") + image.accessibilityDescription = "Compose new status" + + let segmentedControl = NSSegmentedControl(images: [image], trackingMode: .momentary, + target: nil, action: #selector(composeStatus(_:))) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + return segmentedControl + } + + static func makeSearchSegmentedControl() -> NSSegmentedControl { + let segmentedControl = NSSegmentedControl(images: [NSImage(systemSymbolName: "magnifyingglass", accessibilityDescription: "Search")!], + trackingMode: .momentary, + target: nil, action: #selector(showSearch(_:))) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + return segmentedControl + } + + func makeToolbarContainerView() -> NSView? { + guard let toolbarView: NSView = (window as? ToolbarWindow)?.toolbarView else { + return nil + } + + guard let toolbarItemsContainer = toolbarView.findSubview(withClassName: "NSToolbarItemViewer") else { + installPersistentToolbarButtons(toolbarView: toolbarView) + return toolbarView + } + + installPersistentToolbarButtons(toolbarView: toolbarItemsContainer) + return toolbarItemsContainer.superview! + } } From a6018d8448d306c8d3a5affe543600706d6c5de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 23 Mar 2024 14:39:18 +0100 Subject: [PATCH 08/20] features/rearrange-columns: fix 'Done' button in sheet --- Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib | 2 +- .../ArrangeColumns/ArrangeColumnsWindowController.swift | 2 -- Mastonaut/Window Controllers/TimelinesWindowController.swift | 5 ++++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index 65a0910..59b30c2 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -33,7 +33,7 @@ DQ - + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index e2ae68c..480b5f1 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -111,6 +111,4 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega @IBAction func done(_ sender: Any) { close() } - - @IBAction func what(_ sender: Any) {} } diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 5701580..2c0da0d 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -76,6 +76,8 @@ class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, private var placeholderViewController: NSViewController? private var searchWindowController: NSWindowController? + private var arrangeColumnsWindowController: ArrangeColumnsWindowController? + // MARK: Lifecycle Support private var preservedWindowFrameStack: Stack = [] @@ -1121,7 +1123,8 @@ extension TimelinesWindowController // IBActions // // removeColumn(at: columnIndex, contract: true) - let wc = ArrangeColumnsWindowController() + arrangeColumnsWindowController = ArrangeColumnsWindowController() + let wc = arrangeColumnsWindowController! wc.columnViewControllers = timelinesViewController.columnViewControllers From 6bbec20f784dd5bb2933f953df730dbde0268d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 23 Mar 2024 17:23:45 +0100 Subject: [PATCH 09/20] features/rearrange-columns: wip set label and image for item --- .../ArrangeColumnsViewItem.swift | 6 +++--- .../ArrangeColumns/ArrangeColumnsViewItem.xib | 20 ++++++++++--------- .../ArrangeColumns/ArrangeColumnsWindow.xib | 6 +++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index 5e6f424..f98bf3e 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -21,12 +21,12 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { } func set(columnViewController: ColumnViewController) { - guard //let label, + guard let label, let columnMode = columnViewController.modelRepresentation as? ColumnMode else { return } -// label.stringValue = columnMode.getTitle() -// image.image = columnMode.getImage() + label.stringValue = columnMode.getTitle() + image.image = columnMode.getImage() // TODO: MAYBE aspect ratio // box.frame = NSRect(x: 0, y: 0, width: 100, height: 100) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib index b0497bb..f3ab6a1 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib @@ -1,13 +1,15 @@ - + - + + + @@ -16,7 +18,7 @@ - + @@ -28,17 +30,17 @@ + + + + + - + - - - - - diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index 59b30c2..2025e79 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -1,8 +1,8 @@ - + - + @@ -18,7 +18,7 @@ - + From 47c3424fa8028759d5dfea25491bba076ce3eee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sun, 24 Mar 2024 15:47:17 +0100 Subject: [PATCH 10/20] features/rearrange-columns: make previously selected menu item active again --- .../TimelinesWindowController.swift | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 2c0da0d..34ea5d5 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1113,15 +1113,35 @@ extension TimelinesWindowController // IBActions replaceColumn(at: columnIndex, with: newModel.makeViewController()) } + private func selectPreviouslySelectedColumnMode(_ sender: Any?) { + guard + let menuItem = sender as? NSMenuItem, + let columnIndex = menuItem.representedObject as? Int + else { + return + } + + let columnViewController = timelinesViewController.columnViewControllers[columnIndex] + + if + let previousColumnMode = columnViewController.modelRepresentation as? ColumnMode + { + let previouslySelectedItem = menuItem.menu?.items.first { + guard let columnMode = $0.representedObject as? ColumnMode + else { return false } + + return columnMode == previousColumnMode + } + + let popupButton = columnPopUpButtonMap.object(forKey: columnViewController) + + popupButton?.select(previouslySelectedItem) + } + } + @IBAction private func rearrangeColumns(_ sender: Any?) { -// guard -// let menuItem = sender as? NSMenuItem, -// let columnIndex = menuItem.representedObject as? Int -// else { -// return -// } -// -// removeColumn(at: columnIndex, contract: true) + // the Rearrange… menu item should only show as current temporarily + selectPreviouslySelectedColumnMode(sender) arrangeColumnsWindowController = ArrangeColumnsWindowController() let wc = arrangeColumnsWindowController! From 52c0f844d78c450059541470bd61b96c501a9a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Wed, 3 Apr 2024 23:45:53 +0200 Subject: [PATCH 11/20] features/rearrange-columns: wip better sheet sizing --- .../Window Controllers/TimelinesWindowController.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 34ea5d5..099c4ee 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1149,8 +1149,15 @@ extension TimelinesWindowController // IBActions wc.columnViewControllers = timelinesViewController.columnViewControllers if let childWindow = wc.window, - let parentWindow = window + let parentWindow = window, + let screen = parentWindow.screen { + let sheetWidth: CGFloat = 480 + let sheetHeight: CGFloat = 298 + + childWindow.minSize = NSSize(width: sheetWidth, height: sheetHeight) + childWindow.maxSize = NSSize(width: screen.frame.width, height: sheetHeight) + parentWindow.beginSheet(childWindow) } } From de90f162bd00b94a700d17444c909f1216c1b80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 17:38:48 +0200 Subject: [PATCH 12/20] features/rearrange-columns: drag&drop + recreate columns works --- .../ArrangeColumnsViewItem.swift | 13 --- .../ArrangeColumns/ArrangeColumnsWindow.xib | 9 ++- .../ArrangeColumnsWindowController.swift | 79 ++++++++----------- .../TimelinesWindowController.swift | 23 ++++-- 4 files changed, 56 insertions(+), 68 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index f98bf3e..85f1c95 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -14,11 +14,6 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { @IBOutlet var image: NSImageView! private var columnViewController: ColumnViewController? - - override func viewDidLoad() { - super.viewDidLoad() - // Do view setup here. - } func set(columnViewController: ColumnViewController) { guard let label, @@ -27,13 +22,5 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { label.stringValue = columnMode.getTitle() image.image = columnMode.getImage() - - // TODO: MAYBE aspect ratio -// box.frame = NSRect(x: 0, y: 0, width: 100, height: 100) - } - - func setHighlighted(_ highlighted: Bool) { - view.layer?.borderColor = highlighted ? NSColor.systemRed.cgColor : NSColor.systemBlue.cgColor - view.layer?.borderWidth = highlighted ? 3.0 : 0.0 } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index 2025e79..e0f599d 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -42,12 +42,13 @@ DQ - + - - - + + + + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index 480b5f1..ace5f76 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -29,83 +29,70 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega // collectionView.registerForDraggedTypes([NSPasteboard.PasteboardType(UTType.item.identifier)]) // collectionView.setDraggingSourceOperationMask(.move, forLocal: true) - + collectionView.registerForDraggedTypes([.string]) collectionView.setDraggingSourceOperationMask(.every, forLocal: true) collectionView.setDraggingSourceOperationMask(.every, forLocal: false) } - var columnViewControllers: [ColumnViewController]? + var getColumnViewControllers: (() -> [ColumnViewController])? + var moveColumnViewController: ((ColumnViewController, Int) -> Void)? func numberOfSections(in collectionView: NSCollectionView) -> Int { 1 } - - func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { - highlightItems(true, at: indexPaths) - } - func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { - highlightItems(false, at: indexPaths) - } - - func highlightItems(_ highlighted: Bool, at indexPaths: Set) { - for indexPath in indexPaths { - guard let item = collectionView.item(at: indexPath) as? ArrangeColumnsViewItem else { continue } - item.setHighlighted(highlighted) - } - } - func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { - columnViewControllers?.count ?? 0 - } - - func collectionView(_ collectionView: NSCollectionView, - pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? - { - print("pasteboardWriterForItemAt") - return nil + getColumnViewControllers?().count ?? 0 } - func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem - { + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let identifier = ReuseIdentifiers.item let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) let index = indexPath.item guard let viewItem = item as? ArrangeColumnsViewItem, - let columnViewControllers, - columnViewControllers.count >= index + let getColumnViewControllers, + getColumnViewControllers().count >= index else { return item } - viewItem.set(columnViewController: columnViewControllers[index]) - - viewItem.setHighlighted(false) + viewItem.set(columnViewController: getColumnViewControllers()[index]) return viewItem } - func collectionView(_ collectionView: NSCollectionView, writeItemsAt indexes: IndexSet, to pasteboard: NSPasteboard) -> Bool { - print("writeItemsAt") - return true + func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { + return String(indexPath.item) as NSString } - func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexes: IndexSet, with event: NSEvent) -> Bool { - print("canDragItemsAt") - return true + func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { + if proposedDropOperation.pointee == .on { + proposedDropOperation.pointee = .before + } + + return .move } - func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, index: Int, dropOperation: NSCollectionView.DropOperation) -> Bool { - print("acceptDrop") - // TODO: check type + func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool { + guard let stringResult = draggingInfo.draggingPasteboard.propertyList(forType: .string) as? String, + let stringUtf8Data = stringResult.data(using: .utf8) + else { return false } - return true - } + guard let item = try? JSONDecoder().decode(Int.self, from: stringUtf8Data), + let getColumnViewControllers, + let moveColumnViewController + else { return false } - func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndex proposedDropIndex: UnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { - print("validateDrop") - return NSDragOperation.move + let colController = getColumnViewControllers()[item] + + print("Moving \(colController) to \(indexPath.item)") + + moveColumnViewController(colController, indexPath.item) + + collectionView.reloadData() + + return true } @IBAction func done(_ sender: Any) { diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 099c4ee..23efb78 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1146,11 +1146,24 @@ extension TimelinesWindowController // IBActions arrangeColumnsWindowController = ArrangeColumnsWindowController() let wc = arrangeColumnsWindowController! - wc.columnViewControllers = timelinesViewController.columnViewControllers - + wc.getColumnViewControllers = { [self] in timelinesViewController.columnViewControllers } + wc.moveColumnViewController = { [self] + newControllerAtOldIndex, newIndex in + + guard let oldIndex = timelinesViewController.columnViewControllers.firstIndex(where: { $0 == newControllerAtOldIndex }), + let newModeAtOldIndex = newControllerAtOldIndex.modelRepresentation, + let oldModeAtNewIndex = timelinesViewController.columnViewControllers[newIndex].modelRepresentation + else { return } + + print("Found \(newModeAtOldIndex) at \(String(describing: oldIndex)); swapping with \(oldModeAtNewIndex) at \(newIndex)") + + replaceColumn(at: newIndex, with: newModeAtOldIndex.makeViewController()) + replaceColumn(at: oldIndex, with: oldModeAtNewIndex.makeViewController()) + } + if let childWindow = wc.window, - let parentWindow = window, - let screen = parentWindow.screen + let window, + let screen = window.screen { let sheetWidth: CGFloat = 480 let sheetHeight: CGFloat = 298 @@ -1158,7 +1171,7 @@ extension TimelinesWindowController // IBActions childWindow.minSize = NSSize(width: sheetWidth, height: sheetHeight) childWindow.maxSize = NSSize(width: screen.frame.width, height: sheetHeight) - parentWindow.beginSheet(childWindow) + window.beginSheet(childWindow) } } From 785fdba07c2c7671359dd53c6f83fb8cbae912cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:02:38 +0200 Subject: [PATCH 13/20] features/rearrange-columns: add X close button --- .../ArrangeColumns/ArrangeColumnsViewItem.swift | 15 ++++++++++++++- .../ArrangeColumnsWindowController.swift | 5 +++-- .../TimelinesWindowController.swift | 7 +++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index 85f1c95..c0bde65 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -15,12 +15,25 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { private var columnViewController: ColumnViewController? - func set(columnViewController: ColumnViewController) { + private var arrangeColumnsController: ArrangeColumnsWindowController? + + func set(columnViewController: ColumnViewController, arrangeColumnsController: ArrangeColumnsWindowController) { guard let label, let columnMode = columnViewController.modelRepresentation as? ColumnMode else { return } + self.columnViewController = columnViewController + self.arrangeColumnsController = arrangeColumnsController + label.stringValue = columnMode.getTitle() image.image = columnMode.getImage() } + + @IBAction func closeColumn(_ sender: Any) { + guard let columnViewController, + let closeColumn = arrangeColumnsController?.closeColumn + else { return } + + closeColumn(columnViewController) + } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index ace5f76..504f366 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -37,6 +37,7 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega var getColumnViewControllers: (() -> [ColumnViewController])? var moveColumnViewController: ((ColumnViewController, Int) -> Void)? + var closeColumn: ((ColumnViewController) -> Void)? func numberOfSections(in collectionView: NSCollectionView) -> Int { 1 @@ -57,7 +58,7 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega getColumnViewControllers().count >= index else { return item } - viewItem.set(columnViewController: getColumnViewControllers()[index]) + viewItem.set(columnViewController: getColumnViewControllers()[index], arrangeColumnsController: self) return viewItem } @@ -89,7 +90,7 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega print("Moving \(colController) to \(indexPath.item)") moveColumnViewController(colController, indexPath.item) - + collectionView.reloadData() return true diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 23efb78..9ce9167 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1161,6 +1161,13 @@ extension TimelinesWindowController // IBActions replaceColumn(at: oldIndex, with: oldModeAtNewIndex.makeViewController()) } + wc.closeColumn = { [self] columnViewController in + + guard let index = timelinesViewController.columnViewControllers.firstIndex(where: { $0 == columnViewController }) else { return } + + removeColumn(at: index, contract: true) + } + if let childWindow = wc.window, let window, let screen = window.screen From fc641d1944bcca7962290ccdbbad598d40ee7708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:02:49 +0200 Subject: [PATCH 14/20] features/rearrange-columns: vastly improve layout --- .../ArrangeColumns/ArrangeColumnsViewItem.xib | 61 ++++++++++++------- .../ArrangeColumns/ArrangeColumnsWindow.xib | 33 +++++++--- .../StatusComposerWindowController.xib | 8 +-- .../TimelinesWindowController.swift | 37 ++++++----- 4 files changed, 83 insertions(+), 56 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib index f3ab6a1..ebf1a95 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib @@ -16,31 +16,48 @@ - + - - - - - - - - - - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index e0f599d..d91332b 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -17,14 +17,14 @@ - + - - + + - + - + - + - - + + @@ -66,17 +69,27 @@ DQ + + + + + + + + + + - + diff --git a/Mastonaut/Window Controllers/StatusComposerWindowController.xib b/Mastonaut/Window Controllers/StatusComposerWindowController.xib index 4125e87..6db8a54 100644 --- a/Mastonaut/Window Controllers/StatusComposerWindowController.xib +++ b/Mastonaut/Window Controllers/StatusComposerWindowController.xib @@ -1,8 +1,8 @@ - + - + @@ -50,7 +50,7 @@ - + @@ -709,7 +709,7 @@ - + diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index 9ce9167..c79ad0a 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -23,8 +23,7 @@ import Logging import MastodonKit import PullRefreshableScrollView -class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, ToolbarWindowController -{ +class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, ToolbarWindowController { private var logger: Logger! // MARK: Outlets @@ -194,20 +193,18 @@ class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, return timelinesSplitViewController.children.first as! TimelinesViewController } - private lazy var accountMenuItems: [NSMenuItem] = { - [ - NSMenuItem(title: 🔠("View Profile"), - action: #selector(showUserProfile(_:)), - keyEquivalent: ""), - NSMenuItem(title: 🔠("Open Profile in Browser"), - action: #selector(openUserProfileInBrowser(_:)), - keyEquivalent: ""), - NSMenuItem(title: 🔠("View Favorites"), - action: #selector(showUserFavorites(_:)), - keyEquivalent: "F").with(modifierMask: [.command, .shift]), - .separator() - ] - }() + private lazy var accountMenuItems: [NSMenuItem] = [ + NSMenuItem(title: 🔠("View Profile"), + action: #selector(showUserProfile(_:)), + keyEquivalent: ""), + NSMenuItem(title: 🔠("Open Profile in Browser"), + action: #selector(openUserProfileInBrowser(_:)), + keyEquivalent: ""), + NSMenuItem(title: 🔠("View Favorites"), + action: #selector(showUserFavorites(_:)), + keyEquivalent: "F").with(modifierMask: [.command, .shift]), + .separator() + ] // MARK: Window Controller Lifecycle @@ -551,10 +548,10 @@ class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, var constraints: [NSLayoutConstraint] = [] let contentView = timelinesViewController.mainContentView - [currentUserPopUpButton, statusComposerSegmentedControl, searchSegmentedControl, newColumnSegmentedControl].forEach { - toolbarView.addSubview($0) + for item in [currentUserPopUpButton, statusComposerSegmentedControl, searchSegmentedControl, newColumnSegmentedControl] { + toolbarView.addSubview(item) let referenceView = toolbarView.superview ?? toolbarView - constraints.append(referenceView.centerYAnchor.constraint(equalTo: $0.centerYAnchor)) + constraints.append(referenceView.centerYAnchor.constraint(equalTo: item.centerYAnchor)) } constraints.append(TrackingLayoutConstraint.constraint(trackingMaxXOf: contentView, @@ -1173,7 +1170,7 @@ extension TimelinesWindowController // IBActions let screen = window.screen { let sheetWidth: CGFloat = 480 - let sheetHeight: CGFloat = 298 + let sheetHeight: CGFloat = 188 childWindow.minSize = NSSize(width: sheetWidth, height: sheetHeight) childWindow.maxSize = NSSize(width: screen.frame.width, height: sheetHeight) From 2b83894b22ea3de9add2d3983b1d98a0647791c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:08:18 +0200 Subject: [PATCH 15/20] features/rearrange-columns: minor layout improvement (align baseline of label/button) --- Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib index d91332b..cfd50a6 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib @@ -69,8 +69,8 @@ DQ - - + + @@ -85,6 +85,7 @@ DQ + From efa6feeaa17877f314ae4c872525985f5be34f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:09:05 +0200 Subject: [PATCH 16/20] features/rearrange-columns: close button needs to reload --- .../Features/ArrangeColumns/ArrangeColumnsViewItem.swift | 2 ++ .../ArrangeColumns/ArrangeColumnsWindowController.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift index c0bde65..01b20a2 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift @@ -35,5 +35,7 @@ class ArrangeColumnsViewItem: NSCollectionViewItem { else { return } closeColumn(columnViewController) + + arrangeColumnsController?.reloadData() } } diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index 504f366..8c855c7 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -95,6 +95,10 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega return true } + + func reloadData() { + collectionView.reloadData() + } @IBAction func done(_ sender: Any) { close() From a52a6c04dad6d05e92bb4e044f98270ba3897a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:13:11 +0200 Subject: [PATCH 17/20] features/rearrange-columns: if only one column remains, might as well close the sheet --- .../ArrangeColumns/ArrangeColumnsWindowController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index 8c855c7..b3c1b55 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -95,9 +95,14 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega return true } - + func reloadData() { collectionView.reloadData() + + // if only one column remains, there's nothing left for the user to do in the sheet + if let viewControllers = getColumnViewControllers?(), viewControllers.count == 1 { + close() + } } @IBAction func done(_ sender: Any) { From 35729b6df74e1648703a1cd0e1d03b2874339cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:45:02 +0200 Subject: [PATCH 18/20] features/rearrange-columns: fix drop edge cases --- .../ArrangeColumnsWindowController.swift | 6 +++--- .../TimelinesWindowController.swift | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index b3c1b55..ab387de 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -68,14 +68,14 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega } func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { - if proposedDropOperation.pointee == .on { - proposedDropOperation.pointee = .before - } + print("proposed index path: \(proposedDropIndexPath.pointee.item), drop operation: \(proposedDropOperation.pointee)") return .move } func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionView.DropOperation) -> Bool { + print("dropping at: \(indexPath.item)") + guard let stringResult = draggingInfo.draggingPasteboard.propertyList(forType: .string) as? String, let stringUtf8Data = stringResult.data(using: .utf8) else { return false } diff --git a/Mastonaut/Window Controllers/TimelinesWindowController.swift b/Mastonaut/Window Controllers/TimelinesWindowController.swift index c79ad0a..21fdef3 100644 --- a/Mastonaut/Window Controllers/TimelinesWindowController.swift +++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift @@ -1145,10 +1145,20 @@ extension TimelinesWindowController // IBActions wc.getColumnViewControllers = { [self] in timelinesViewController.columnViewControllers } wc.moveColumnViewController = { [self] - newControllerAtOldIndex, newIndex in + newControllerAtOldIndex, _newIndex in + + // if user drags past the end, treat it as the end + let newIndex = min(_newIndex, timelinesViewController.columnViewControllers.count - 1) guard let oldIndex = timelinesViewController.columnViewControllers.firstIndex(where: { $0 == newControllerAtOldIndex }), - let newModeAtOldIndex = newControllerAtOldIndex.modelRepresentation, + oldIndex != newIndex + else { + print("oldIndex wasn't found or is the same as newIndex") + + return + } + + guard let newModeAtOldIndex = newControllerAtOldIndex.modelRepresentation, let oldModeAtNewIndex = timelinesViewController.columnViewControllers[newIndex].modelRepresentation else { return } From a9bf62509c553578ee05f592ce4a6c05b90d1fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:47:29 +0200 Subject: [PATCH 19/20] features/rearrange-columns: remove test code --- .../ArrangeColumns/ArrangeColumnsView.swift | 24 ------------------ .../ArrangeColumns/ArrangeableColumn.swift | 25 ------------------- .../ArrangeColumns/UnclickableBox.swift | 14 ----------- 3 files changed, 63 deletions(-) delete mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift delete mode 100644 Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift delete mode 100644 Mastonaut/Features/ArrangeColumns/UnclickableBox.swift diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift deleted file mode 100644 index f8b160a..0000000 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ArrangeColumnsView.swift -// Mastonaut -// -// Created by Sören Kuklau on 30.10.23. -// - -import SwiftUI - -struct ArrangeColumnsView: View { - var body: some View { - HStack { - ArrangeableColumn(icon: "house", text: "Home") - - ArrangeableColumn(icon: "bell", text: "Notifications") - - ArrangeableColumn(icon: "star", text: "Favorites") - }.padding(10) - } -} - -#Preview { - ArrangeColumnsView() -} diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift b/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift deleted file mode 100644 index bca861a..0000000 --- a/Mastonaut/Features/ArrangeColumns/ArrangeableColumn.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ArrangeableColumn.swift -// Mastonaut -// -// Created by Sören Kuklau on 30.10.23. -// - -import SwiftUI - -struct ArrangeableColumn: View { - @State var icon: String - @State var text: String - - var body: some View { - HStack { - Image(systemName: icon) - Text(text) - }.frame(minWidth: 160, minHeight: 100) - .border(.secondary) - } -} - -#Preview { - ArrangeableColumn(icon: "house", text: "Home") -} diff --git a/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift b/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift deleted file mode 100644 index 585a9d5..0000000 --- a/Mastonaut/Features/ArrangeColumns/UnclickableBox.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UnclickableBoxView.swift -// Mastonaut -// -// Created by Sören Kuklau on 04.11.23. -// - -import Foundation - -class UnclickableBox: NSBox { - override func hitTest(_ point: NSPoint) -> NSView? { - return nil - } -} From 1f3123425b3bf1f709a3c9416e9d020f6c6ad06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Kuklau?= Date: Sat, 6 Apr 2024 19:49:27 +0200 Subject: [PATCH 20/20] features/rearrange-columns: remove test code --- .../ArrangeColumns/ArrangeColumnsWindowController.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift index ab387de..8cbebd2 100644 --- a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift +++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift @@ -27,9 +27,6 @@ class ArrangeColumnsWindowController: NSWindowController, NSCollectionViewDelega collectionView.register(ArrangeColumnsViewItem.self, forItemWithIdentifier: ReuseIdentifiers.item) -// collectionView.registerForDraggedTypes([NSPasteboard.PasteboardType(UTType.item.identifier)]) -// collectionView.setDraggingSourceOperationMask(.move, forLocal: true) - collectionView.registerForDraggedTypes([.string]) collectionView.setDraggingSourceOperationMask(.every, forLocal: true) collectionView.setDraggingSourceOperationMask(.every, forLocal: false)