From 1ced4448eae84f14da804bdaeb91cb9add0a5986 Mon Sep 17 00:00:00 2001 From: Daniel Jalkut Date: Sun, 1 Nov 2020 17:33:48 -0500 Subject: [PATCH] Support a new secret user default JalkutRespectFolderExpansionOnNextUnread, and revise the "next unread" strategy so that whether the search for a next unread wraps around to the top or not is parameterized. --- Mac/MainWindow/MainWindowController.swift | 25 ++++---- .../Sidebar/SidebarViewController.swift | 58 ++++++++++++------- .../Timeline/TimelineViewController.swift | 10 ++-- Shared/Timeline/ArticleArray.swift | 19 +++--- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 1bb206d19..0b1be8851 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -323,13 +323,16 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { NSCursor.setHiddenUntilMouseMoves(true) // TODO: handle search mode - if timelineViewController.canGoToNextUnread() { - goToNextUnreadInTimeline() + if timelineViewController.canGoToNextUnread(wrappingToTop: false) { + goToNextUnreadInTimeline(wrappingToTop: false) } - else if sidebarViewController.canGoToNextUnread() { - sidebarViewController.goToNextUnread() - if timelineViewController.canGoToNextUnread() { - goToNextUnreadInTimeline() + else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) { + sidebarViewController.goToNextUnread(wrappingToTop: true) + + // If we ended up on the same timelineViewController, we may need to wrap + // around to the top of its contents. + if timelineViewController.canGoToNextUnread(wrappingToTop: true) { + goToNextUnreadInTimeline(wrappingToTop: true) } } } @@ -995,13 +998,13 @@ private extension MainWindowController { // MARK: - Command Validation - func canGoToNextUnread() -> Bool { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else { return false } // TODO: handle search mode - return timelineViewController.canGoToNextUnread() || sidebarViewController.canGoToNextUnread() + return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping) } func canMarkAllAsRead() -> Bool { @@ -1188,14 +1191,14 @@ private extension MainWindowController { // MARK: - Misc. - func goToNextUnreadInTimeline() { + func goToNextUnreadInTimeline(wrappingToTop wrapping: Bool) { guard let timelineViewController = currentTimelineViewController else { return } - if timelineViewController.canGoToNextUnread() { - timelineViewController.goToNextUnread() + if timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) { + timelineViewController.goToNextUnread(wrappingToTop: wrapping) makeTimelineViewFirstResponder() } } diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 22f097268..e7dcf8cd3 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -294,15 +294,15 @@ protocol SidebarDelegate: class { // MARK: - Navigation - func canGoToNextUnread() -> Bool { - if let _ = nextSelectableRowWithUnreadArticle() { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { + if let _ = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) { return true } return false } - func goToNextUnread() { - guard let row = nextSelectableRowWithUnreadArticle() else { + func goToNextUnread(wrappingToTop wrapping: Bool = false) { + guard let row = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) else { assertionFailure("goToNextUnread called before checking if there is a next unread.") return } @@ -685,26 +685,42 @@ private extension SidebarViewController { return false } - func nextSelectableRowWithUnreadArticle() -> Int? { - // Skip group items, because they should never be selected. - - let selectedRow = outlineView.selectedRow - let numberOfRows = outlineView.numberOfRows - var row = selectedRow + 1 - - while (row < numberOfRows) { - if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { - return row - } - row += 1 + func rowIsExpandedFolder(_ row: Int) -> Bool { + if let node = nodeForRow(row), outlineView.isItemExpanded(node) { + return true } - - row = 0 - while (row <= selectedRow) { - if rowHasAtLeastOneUnreadArticle(row) && !rowIsGroupItem(row) { + return false + } + + func shouldSkipRow(_ row: Int) -> Bool { + let skipExpandedFolders = UserDefaults.standard.bool(forKey: "JalkutRespectFolderExpansionOnNextUnread") + + // Skip group items, because they should never be selected. + // Skip expanded folders only if Jalkut's pref is enabled. + if rowIsGroupItem(row) || (skipExpandedFolders && rowIsExpandedFolder(row)) { + return true + } + return false + } + + func nextSelectableRowWithUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? { + let numberOfRows = outlineView.numberOfRows + let startRow = outlineView.selectedRow + 1 + + let orderedRows: [Int] + if startRow == numberOfRows { + // Last item is selected, so start at the beginning if we allow wrapping + orderedRows = wrapping ? Array(0.. Bool { - guard let _ = indexOfNextUnreadArticle() else { + func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { + guard let _ = indexOfNextUnreadArticle(wrappingToTop: wrapping) else { return false } return true } - func indexOfNextUnreadArticle() -> Int? { - return articles.rowOfNextUnreadArticle(tableView.selectedRow) + func indexOfNextUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? { + return articles.rowOfNextUnreadArticle(tableView.selectedRow, wrappingToTop: wrapping) } func focus() { diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 391963424..104f06f8c 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -20,18 +20,21 @@ extension Array where Element == Article { return self[row] } - func rowOfNextUnreadArticle(_ selectedRow: Int) -> Int? { + func orderedRowIndexes(fromIndex startIndex: Int, wrappingToTop wrapping: Bool) -> [Int] { + if startIndex >= self.count { + // Wrap around to the top if specified + return wrapping ? Array(0..(startIndex..(0.. Int? { if isEmpty { return nil } - var rowIndex = selectedRow - while(true) { - - rowIndex = rowIndex + 1 - if rowIndex >= count { - break - } + for rowIndex in orderedRowIndexes(fromIndex: selectedRow + 1, wrappingToTop: wrapping) { let article = articleAtRow(rowIndex)! if !article.status.read { return rowIndex