diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift
new file mode 100644
index 0000000..01b20a2
--- /dev/null
+++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.swift
@@ -0,0 +1,41 @@
+//
+// ArrangeColumnsViewItem.swift
+// Mastonaut
+//
+// Created by SΓΆren Kuklau on 30.10.23.
+//
+
+import Cocoa
+
+class ArrangeColumnsViewItem: NSCollectionViewItem {
+ @IBOutlet var box: NSBox!
+
+ @IBOutlet var label: NSTextField!
+ @IBOutlet var image: NSImageView!
+
+ private var 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)
+
+ arrangeColumnsController?.reloadData()
+ }
+}
diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib
new file mode 100644
index 0000000..ebf1a95
--- /dev/null
+++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsViewItem.xib
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib
new file mode 100644
index 0000000..cfd50a6
--- /dev/null
+++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindow.xib
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift
new file mode 100644
index 0000000..8cbebd2
--- /dev/null
+++ b/Mastonaut/Features/ArrangeColumns/ArrangeColumnsWindowController.swift
@@ -0,0 +1,108 @@
+//
+// ArrangeColumnsViewController.swift
+// Mastonaut
+//
+// Created by SΓΆren Kuklau on 30.10.23.
+//
+
+import Foundation
+import UniformTypeIdentifiers
+
+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)
+
+ collectionView.registerForDraggedTypes([.string])
+ collectionView.setDraggingSourceOperationMask(.every, forLocal: true)
+ collectionView.setDraggingSourceOperationMask(.every, forLocal: false)
+ }
+
+ var getColumnViewControllers: (() -> [ColumnViewController])?
+ var moveColumnViewController: ((ColumnViewController, Int) -> Void)?
+ var closeColumn: ((ColumnViewController) -> Void)?
+
+ func numberOfSections(in collectionView: NSCollectionView) -> Int {
+ 1
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
+ getColumnViewControllers?().count ?? 0
+ }
+
+ 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 getColumnViewControllers,
+ getColumnViewControllers().count >= index
+ else { return item }
+
+ viewItem.set(columnViewController: getColumnViewControllers()[index], arrangeColumnsController: self)
+
+ return viewItem
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
+ return String(indexPath.item) as NSString
+ }
+
+ func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation {
+ 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 }
+
+ guard let item = try? JSONDecoder().decode(Int.self, from: stringUtf8Data),
+ let getColumnViewControllers,
+ let moveColumnViewController
+ else { return false }
+
+ let colController = getColumnViewControllers()[item]
+
+ print("Moving \(colController) to \(indexPath.item)")
+
+ moveColumnViewController(colController, indexPath.item)
+
+ collectionView.reloadData()
+
+ 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) {
+ close()
+ }
+}
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
+ }
+ }
}
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 615fcfa..21fdef3 100644
--- a/Mastonaut/Window Controllers/TimelinesWindowController.swift
+++ b/Mastonaut/Window Controllers/TimelinesWindowController.swift
@@ -23,1232 +23,1318 @@ import Logging
import MastodonKit
import PullRefreshableScrollView
-class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, ToolbarWindowController
-{
- private var logger: Logger!
-
- // MARK: Outlets
-
- @IBOutlet private var newColumnMenu: NSMenu!
-
- // MARK: Services
-
- private unowned let accountsService = AppDelegate.shared.accountsService
- private unowned let instanceService = AppDelegate.shared.instanceService
-
- // MARK: KVO Observers
+class TimelinesWindowController: NSWindowController, UserPopUpButtonDisplaying, ToolbarWindowController {
+ private var logger: Logger!
- private var observations = [NSKeyValueObservation]()
- private var accountObservations = [NSKeyValueObservation]()
+ // MARK: Outlets
- // MARK: Toolbar Buttons
+ @IBOutlet private var newColumnMenu: NSMenu!
- 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)
+ // MARK: Services
- newColumnSegmentedControl.setEnabled(!newColumnMenu.items.isEmpty, forSegment: 0)
+ private unowned let accountsService = AppDelegate.shared.accountsService
+ private unowned let instanceService = AppDelegate.shared.instanceService
- NSLayoutConstraint.activate(popUpButtonConstraints)
- }
+ // MARK: KVO Observers
- func buildColumnsPopupButtonMenu(currentColumnMode: ColumnMode,
- takenModes: [ColumnMode],
- index: Int) -> NSMenu
- {
- let followedLists = currentAccount?.followedLists
+ private var observations = [NSKeyValueObservation]()
+ private var accountObservations = [NSKeyValueObservation]()
- logger.debug2("Building columns popup menu. Followed lists: \(followedLists?.count ?? 0)")
+ // MARK: Toolbar Buttons
- var menuItemSection = 0
+ 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)
+
+ // 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)
+
+ var sidebarViewController: SidebarViewController? {
+ get { return timelinesSplitViewController.sidebarViewController }
+ set { timelinesSplitViewController.sidebarViewController = newValue }
+ }
+
+ // MARK: Child View Controllers
+
+ private var placeholderViewController: NSViewController?
+ private var searchWindowController: NSWindowController?
+
+ private var arrangeColumnsWindowController: ArrangeColumnsWindowController?
+
+ // 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
+
+ for item in [currentUserPopUpButton, statusComposerSegmentedControl, searchSegmentedControl, newColumnSegmentedControl] {
+ toolbarView.addSubview(item)
+ let referenceView = toolbarView.superview ?? toolbarView
+ constraints.append(referenceView.centerYAnchor.constraint(equalTo: item.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)
+
+ NSLayoutConstraint.activate(popUpButtonConstraints)
+ }
+
+ func buildColumnsPopupButtonMenu(currentColumnMode: ColumnMode,
+ takenModes: [ColumnMode],
+ index: Int) -> NSMenu
+ {
+ let followedLists = currentAccount?.followedLists
- let staticColumnModes = ColumnMode.staticItems
+ logger.debug2("Building columns popup menu. Followed lists: \(followedLists?.count ?? 0)")
- let menu = NSMenu(title: "")
+ var menuItemSection = 0
- menu.autoenablesItems = false
+ let staticColumnModes = ColumnMode.staticItems
- var items: [NSMenuItem] = staticColumnModes.filter { !takenModes.contains($0) }
- .map { $0.makeMenuItemForChanging(with: self, columnId: index) }
+ let menu = NSMenu(title: "")
- if currentColumnMode.menuItemSection == menuItemSection {
- items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index))
- }
+ menu.autoenablesItems = false
- items.sort(by: { $0.columnModel! < $1.columnModel! })
+ var items: [NSMenuItem] = staticColumnModes.filter { !takenModes.contains($0) }
+ .map { $0.makeMenuItemForChanging(with: self, columnId: index) }
- menuItemSection = 1
+ if currentColumnMode.menuItemSection == menuItemSection {
+ items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index))
+ }
- items.append(.separator())
+ items.sort(by: { $0.columnModel! < $1.columnModel! })
- let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) }
- .map { $0.makeMenuItemForChanging(with: self, columnId: index) }
+ menuItemSection = 1
- for _item in personalItems {
- items.append(_item)
- }
+ items.append(.separator())
- if currentColumnMode.menuItemSection == menuItemSection {
- items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index))
- }
+ let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) }
+ .map { $0.makeMenuItemForChanging(with: self, columnId: index) }
- var listItems: [NSMenuItem] = []
- var haveAtLeastOneList = false
+ for _item in personalItems {
+ items.append(_item)
+ }
- if let followedLists = followedLists {
- if followedLists.count > 0 {
- listItems.append(.separator())
- listItems.append(.sectionHeader(π ("Lists")))
+ if currentColumnMode.menuItemSection == menuItemSection {
+ items.append(currentColumnMode.makeMenuItemForChanging(with: self, columnId: index))
+ }
- for _list in followedLists {
- if let list = _list as? FollowedList {
- let columnMode = ColumnMode.list(list: list)
+ var listItems: [NSMenuItem] = []
+ var haveAtLeastOneList = false
- listItems.append(columnMode.makeMenuItemForChanging(with: self, columnId: index))
- haveAtLeastOneList = true
- }
- }
- }
- }
+ if let followedLists = followedLists {
+ if followedLists.count > 0 {
+ listItems.append(.separator())
+ listItems.append(.sectionHeader(π ("Lists")))
- if haveAtLeastOneList {
- items.append(contentsOf: listItems)
- }
+ for _list in followedLists {
+ if let list = _list as? FollowedList {
+ let columnMode = ColumnMode.list(list: list)
- items.append(.separator())
+ listItems.append(columnMode.makeMenuItemForChanging(with: self, columnId: index))
+ haveAtLeastOneList = true
+ }
+ }
+ }
+ }
- let reloadColumnItem = NSMenuItem()
- reloadColumnItem.title = π ("Reload this Column")
- reloadColumnItem.target = self
- reloadColumnItem.representedObject = index
- reloadColumnItem.action = #selector(TimelinesWindowController.reloadColumn(_:))
- items.append(reloadColumnItem)
+ if haveAtLeastOneList {
+ items.append(contentsOf: listItems)
+ }
- if index > 0 {
- let removeColumnItem = NSMenuItem()
- removeColumnItem.title = π ("Remove this Column")
- removeColumnItem.target = self
- removeColumnItem.representedObject = index
- removeColumnItem.action = #selector(TimelinesWindowController.removeColumn(_:))
- items.append(removeColumnItem)
- }
+ items.append(.separator())
+ items.append(.sectionHeader(π ("This column")))
- menu.setItems(items)
+ let reloadColumnItem = NSMenuItem()
+ reloadColumnItem.title = π ("Reload")
+ reloadColumnItem.target = self
+ reloadColumnItem.representedObject = index
+ reloadColumnItem.action = #selector(TimelinesWindowController.reloadColumn(_:))
+ items.append(reloadColumnItem)
- return menu
- }
+ if index > 0 {
+ let rearrangeColumnsItem = NSMenuItem()
+ rearrangeColumnsItem.title = π ("Rearrangeβ¦")
+ rearrangeColumnsItem.target = self
+ rearrangeColumnsItem.representedObject = index
+ rearrangeColumnsItem.action = #selector(TimelinesWindowController.rearrangeColumns(_:))
+ items.append(rearrangeColumnsItem)
- func buildNewColumnMenuItems(takenModes: [ColumnMode]) -> [NSMenuItem] {
- var items: [NSMenuItem] = ColumnMode.staticItems.filter { !takenModes.contains($0) }
- .map { $0.makeMenuItemForAdding(with: self) }
+ let removeColumnItem = NSMenuItem()
+ removeColumnItem.title = π ("Remove")
+ removeColumnItem.target = self
+ removeColumnItem.representedObject = index
+ removeColumnItem.action = #selector(TimelinesWindowController.removeColumn(_:))
+ items.append(removeColumnItem)
+ }
- items.append(.separator())
+ menu.setItems(items)
- let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) }
- .map { $0.makeMenuItemForAdding(with: self) }
+ return menu
+ }
- for _item in personalItems {
- items.append(_item)
- }
+ func buildNewColumnMenuItems(takenModes: [ColumnMode]) -> [NSMenuItem] {
+ var items: [NSMenuItem] = ColumnMode.staticItems.filter { !takenModes.contains($0) }
+ .map { $0.makeMenuItemForAdding(with: self) }
- let followedLists = currentAccount?.followedLists
+ items.append(.separator())
- logger.debug2("Building new column menu. Followed lists: \(followedLists?.count ?? 0)")
+ let personalItems = ColumnMode.staticPersonalItems.filter { !takenModes.contains($0) }
+ .map { $0.makeMenuItemForAdding(with: self) }
- var listItems: [NSMenuItem] = []
- var haveAtLeastOneList = false
+ for _item in personalItems {
+ items.append(_item)
+ }
- if let followedLists = followedLists {
- if followedLists.count > 0 {
- listItems.append(.separator())
- listItems.append(.sectionHeader(π ("Lists")))
+ let followedLists = currentAccount?.followedLists
- for _list in followedLists {
- if let list = _list as? FollowedList {
- let columnMode = ColumnMode.list(list: list)
+ logger.debug2("Building new column menu. Followed lists: \(followedLists?.count ?? 0)")
- if !takenModes.contains(columnMode) {
- listItems.append(columnMode.makeMenuItemForAdding(with: self))
- haveAtLeastOneList = true
- }
- }
- }
- }
- }
+ var listItems: [NSMenuItem] = []
+ var haveAtLeastOneList = false
- if haveAtLeastOneList {
- items.append(contentsOf: listItems)
- }
+ if let followedLists = followedLists {
+ if followedLists.count > 0 {
+ listItems.append(.separator())
+ listItems.append(.sectionHeader(π ("Lists")))
- return items
- }
+ for _list in followedLists {
+ if let list = _list as? FollowedList {
+ let columnMode = ColumnMode.list(list: list)
- // MARK: - Keyboard Navigation
-
- override func moveRight(_ sender: Any?) {
- timelinesViewController.makeNextColumnFirstResponder()
- }
-
- override func moveDown(_ sender: Any?) {
- timelinesViewController.makeNextColumnFirstResponder()
- }
-
- override func moveUp(_ sender: Any?) {
- timelinesViewController.makeNextColumnFirstResponder()
- }
-
- override func moveLeft(_ sender: Any?) {
- timelinesViewController.makePreviousColumnFirstResponder()
- }
+ if !takenModes.contains(columnMode) {
+ listItems.append(columnMode.makeMenuItemForAdding(with: self))
+ haveAtLeastOneList = true
+ }
+ }
+ }
+ }
+ }
+
+ if haveAtLeastOneList {
+ items.append(contentsOf: listItems)
+ }
+
+ return items
+ }
+
+ // MARK: - Keyboard Navigation
+
+ override func moveRight(_ sender: Any?) {
+ timelinesViewController.makeNextColumnFirstResponder()
+ }
+
+ override func moveDown(_ sender: Any?) {
+ timelinesViewController.makeNextColumnFirstResponder()
+ }
+
+ override func moveUp(_ sender: Any?) {
+ timelinesViewController.makeNextColumnFirstResponder()
+ }
+
+ 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 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 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())
+ }
+
+ 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?) {
+ // the Rearrange⦠menu item should only show as current temporarily
+ selectPreviouslySelectedColumnMode(sender)
+
+ arrangeColumnsWindowController = ArrangeColumnsWindowController()
+ let wc = arrangeColumnsWindowController!
+
+ wc.getColumnViewControllers = { [self] in timelinesViewController.columnViewControllers }
+ wc.moveColumnViewController = { [self]
+ 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 }),
+ 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 }
+
+ print("Found \(newModeAtOldIndex) at \(String(describing: oldIndex)); swapping with \(oldModeAtNewIndex) at \(newIndex)")
+
+ replaceColumn(at: newIndex, with: newModeAtOldIndex.makeViewController())
+ 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
+ {
+ let sheetWidth: CGFloat = 480
+ let sheetHeight: CGFloat = 188
+
+ childWindow.minSize = NSSize(width: sheetWidth, height: sheetHeight)
+ childWindow.maxSize = NSSize(width: screen.frame.width, height: sheetHeight)
+
+ window.beginSheet(childWindow)
+ }
+ }
+
+ @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!
+ }
}
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: