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: