Files
vienna-rss/Vienna/Sources/Main window/ArticleListView.m
T
Eitot d4f3d2a977 Move instance variable declarations from interface to implementation
Clang allows instance variable declaration in the class implementation. Apple has recommended this since at least 2013: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocDefiningClasses.html.
2023-05-29 07:21:55 +02:00

1726 lines
60 KiB
Objective-C

//
// ArticleListView.m
// Vienna
//
// Created by Steve on 8/27/05.
// Copyright (c) 2004-2017 Steve Palmer and Vienna contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Handle the Horizontal (also known as Report) and Vertical (also known as Condensed) layouts
#import "ArticleListView.h"
#import "Preferences.h"
#import "Constants.h"
#import "DateFormatterExtension.h"
#import "AppController.h"
#import "ArticleController.h"
#import "MessageListView.h"
#import "StringExtensions.h"
#import "HelperFunctions.h"
#import "Field.h"
#import "ProgressTextCell.h"
#import "Article.h"
#import "Folder.h"
#import "EnclosureView.h"
#import "Database.h"
#import "Vienna-Swift.h"
#define PROGRESS_INDICATOR_DIMENSION 8
// Shared defaults key
NSString * const MAPref_ShowEnclosureBar = @"ShowEnclosureBar";
static void *VNAArticleListViewObserverContext = &VNAArticleListViewObserverContext;
@interface ArticleListView ()
@property (weak, nonatomic) IBOutlet NSStackView *contentStackView;
@property (weak, nonatomic) IBOutlet EnclosureView *enclosureView;
-(void)initTableView;
-(BOOL)copyTableSelection:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard;
-(void)setTableViewFont;
-(void)showSortDirection;
-(void)handleReadingPaneChange:(NSNotificationCenter *)nc;
-(BOOL)viewNextUnreadInCurrentFolder:(NSInteger)currentRow;
-(void)markCurrentRead:(NSTimer *)aTimer;
-(void)refreshImmediatelyArticleAtCurrentRow;
-(void)refreshArticleAtCurrentRow;
-(void)makeRowSelectedAndVisible:(NSInteger)rowIndex;
-(void)updateArticleListRowHeight;
-(void)setOrientation:(NSInteger)newLayout;
@property NSView *articleTextView;
@property (strong) NSLayoutConstraint *textViewWidthConstraint;
@property (nonatomic) BOOL imbricatedSplitViewResizes; // used to avoid warnings about missing invalidation for a view's changing state
// MARK: ArticleView delegate
@property (readwrite, getter=isCurrentPageFullHTML, nonatomic) BOOL currentPageFullHTML;
@end
@implementation ArticleListView {
IBOutlet MessageListView *articleList;
NSObject<ArticleContentView, Tab> *articleText;
IBOutlet NSSplitView *splitView2;
NSInteger tableLayout;
BOOL isAppInitialising;
BOOL isChangingOrientation;
BOOL isInTableInit;
BOOL blockSelectionHandler;
NSTimer *markReadTimer;
NSFont *articleListFont;
NSFont *articleListUnreadFont;
NSMutableDictionary *reportCellDict;
NSMutableDictionary *unreadReportCellDict;
NSMutableDictionary *selectionDict;
NSMutableDictionary *topLineDict;
NSMutableDictionary *linkLineDict;
NSMutableDictionary *middleLineDict;
NSMutableDictionary *bottomLineDict;
NSMutableDictionary *unreadTopLineDict;
NSMutableDictionary *unreadTopLineSelectionDict;
NSURL *currentURL;
BOOL isLoadingHTMLArticle;
NSProgressIndicator *progressIndicator;
}
/* initWithFrame
* Initialise our view.
*/
-(instancetype)initWithFrame:(NSRect)frame
{
self= [super initWithFrame:frame];
if (self) {
isChangingOrientation = NO;
isInTableInit = NO;
blockSelectionHandler = NO;
markReadTimer = nil;
_currentPageFullHTML = NO;
isLoadingHTMLArticle = NO;
currentURL = nil;
self.imbricatedSplitViewResizes = NO;
}
return self;
}
/* awakeFromNib
* Do things that only make sense once the NIB is loaded.
*/
-(void)awakeFromNib
{
// Register for notification
NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(handleArticleListFontChange:) name:MA_Notify_ArticleListFontChange object:nil];
[nc addObserver:self selector:@selector(handleReadingPaneChange:) name:MA_Notify_ReadingPaneChange object:nil];
[nc addObserver:self selector:@selector(handleLoadFullHTMLChange:) name:MA_Notify_LoadFullHTMLChange object:nil];
[nc addObserver:self selector:@selector(handleRefreshArticle:) name:MA_Notify_ArticleViewChange object:nil];
[nc addObserver:self selector:@selector(handleArticleViewEnded:) name:MA_Notify_ArticleViewEnded object:nil];
[self initialiseArticleView];
}
/* initialiseArticleView
* Do the things to initialise the article view from the database. This is the
* only point during initialisation where the database is guaranteed to be
* ready for use.
*/
-(void)initialiseArticleView
{
WebKitArticleTab *articleTextController = [[WebKitArticleTab alloc] init];
articleText = articleTextController;
self.articleTextView = articleTextController.view;
[self.contentStackView addView:self.articleTextView inGravity:NSStackViewGravityTop];
Preferences * prefs = [Preferences standardPreferences];
// Mark the start of the init phase
isAppInitialising = YES;
articleText.listView = self;
// Create report and condensed view attribute dictionaries
NSMutableParagraphStyle * style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
style.lineBreakMode = NSLineBreakByTruncatingTail;
style.tighteningFactorForTruncation = 0.0;
reportCellDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor textColor], NSForegroundColorAttributeName, nil];
unreadReportCellDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor textColor], NSForegroundColorAttributeName, nil];
selectionDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor whiteColor], NSForegroundColorAttributeName, nil];
unreadTopLineDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor textColor], NSForegroundColorAttributeName, nil];
topLineDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor textColor], NSForegroundColorAttributeName, nil];
unreadTopLineSelectionDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor whiteColor], NSForegroundColorAttributeName, nil];
middleLineDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor systemBlueColor], NSForegroundColorAttributeName, nil];
linkLineDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor systemBlueColor], NSForegroundColorAttributeName, nil];
bottomLineDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:style, NSParagraphStyleAttributeName, [NSColor systemGrayColor], NSForegroundColorAttributeName, nil];
// Set the reading pane orientation
[self setOrientation:prefs.layout];
// With vertical layout and "Use Web Page for Articles" set, we need to
// manage article view's width so that it does not grow or shrink randomly
// on certain sites.
// The best solution I found is programmatically setting a constraint with a
// "constant" value.
// This did not work for me: self.textViewWidthConstraint =
// [NSLayoutConstraint constraintWithItem:articleTextView attribute:NSLayoutAttributeWidth
// relatedBy:NSLayoutRelationEqual toItem:self.contentStackView attribute:NSLayoutAttributeWidth
// multiplier:1.f constant:0.f];
self.textViewWidthConstraint =
[NSLayoutConstraint constraintWithItem:self.articleTextView attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute
multiplier:0.f constant:self.contentStackView.frame.size.width];
self.textViewWidthConstraint.priority = NSLayoutPriorityRequired;
self.articleTextView.translatesAutoresizingMaskIntoConstraints = NO;
// for some reason, this constraint is necessary for new browser with vertical layout, but it is counterproductive with other configurations
self.textViewWidthConstraint.active = splitView2.vertical;
// Initialise the article list view
[self initTableView];
// Make sure we skip the column filter button in the Tab order
articleList.nextKeyView = self.articleTextView;
// Allow us to control the behavior of the NSSplitView
splitView2.delegate = self;
// Done initialising
isAppInitialising = NO;
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
[userDefaults addObserver:self
forKeyPath:MAPref_ShowEnclosureBar
options:NSKeyValueObservingOptionNew
context:VNAArticleListViewObserverContext];
[userDefaults addObserver:self
forKeyPath:MAPref_ShowUnreadArticlesInBold
options:0
context:VNAArticleListViewObserverContext];
}
/* initTableView
* Do all the initialization for the article list table view control
*/
-(void)initTableView
{
Preferences * prefs = [Preferences standardPreferences];
// Variable initialization here
articleListFont = nil;
articleListUnreadFont = nil;
// Initialize the article columns from saved data
NSArray * dataArray = [prefs arrayForKey:MAPref_ArticleListColumns];
Database * db = [Database sharedManager];
Field * field;
NSUInteger index;
for (index = 0; index < dataArray.count;) {
NSString * name;
NSInteger width = 100;
BOOL visible = NO;
name = dataArray[index++];
if (index < dataArray.count) {
visible = [dataArray[index++] integerValue] == YES;
}
if (index < dataArray.count) {
width = [dataArray[index++] integerValue];
}
field = [db fieldByName:name];
field.visible = visible;
field.width = width;
}
// Set the default fonts
[self setTableViewFont];
// Get the default list of visible columns
[self updateVisibleColumns];
// In condensed mode, the summary field takes up the whole space.
articleList.columnAutoresizingStyle = NSTableViewUniformColumnAutoresizingStyle;
NSMenu *articleListMenu = [[NSMenu alloc] init];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Mark Read", @"Title of a menu item")
action:@selector(markRead:)
keyEquivalent:@""];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Mark Unread", @"Title of a menu item")
action:@selector(markUnread:)
keyEquivalent:@""];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Mark Flagged", @"Title of a menu item")
action:@selector(markFlagged:)
keyEquivalent:@""];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Delete Article", @"Title of a menu item")
action:@selector(deleteMessage:)
keyEquivalent:@""];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Restore Article", @"Title of a menu item")
action:@selector(restoreMessage:)
keyEquivalent:@""];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Download Enclosure", @"Title of a menu item")
action:@selector(downloadEnclosure:)
keyEquivalent:@""];
[articleListMenu addItem:[NSMenuItem separatorItem]];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Open Subscription Home Page", @"Title of a menu item")
action:@selector(viewSourceHomePage:)
keyEquivalent:@""];
NSMenuItem *openFeedInBrowser = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Open Subscription Home Page in External Browser", @"Title of a menu item")
action:@selector(viewSourceHomePageInAlternateBrowser:)
keyEquivalent:@""];
openFeedInBrowser.keyEquivalentModifierMask = NSEventModifierFlagOption;
openFeedInBrowser.alternate = YES;
[articleListMenu addItem:openFeedInBrowser];
[articleListMenu addItemWithTitle:NSLocalizedString(@"Open Article Page", @"Title of a menu item")
action:@selector(viewArticlePages:)
keyEquivalent:@""];
NSMenuItem *openItemInBrowser = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Open Article Page in External Browser", @"Title of a menu item")
action:@selector(viewArticlePagesInAlternateBrowser:)
keyEquivalent:@""];
openItemInBrowser.keyEquivalentModifierMask = NSEventModifierFlagOption;
openItemInBrowser.alternate = YES;
[articleListMenu addItem:openItemInBrowser];
articleList.menu = articleListMenu;
// Set the target for double-click actions
articleList.doubleAction = @selector(doubleClickRow:);
articleList.action = @selector(singleClickRow:);
articleList.delegate = self;
articleList.dataSource = self;
articleList.target = self;
articleList.accessibilityValueDescription = NSLocalizedString(@"Articles", nil);
}
/* singleClickRow
* Handle a single click action. If the click was in the read or flagged column then
* treat it as an action to mark the article read/unread or flagged/unflagged. Later
* trap the comments column and expand/collapse. If the click lands on the enclosure
* colum, download the associated enclosure.
*/
-(IBAction)singleClickRow:(id)sender
{
NSInteger row = articleList.clickedRow;
NSInteger column = articleList.clickedColumn;
NSArray * allArticles = self.controller.articleController.allArticles;
if (row >= 0 && row < (NSInteger)allArticles.count) {
NSArray * columns = articleList.tableColumns;
if (column >= 0 && column < (NSInteger)columns.count) {
Article * theArticle = allArticles[row];
NSString * columnName = ((NSTableColumn *)columns[column]).identifier;
if ([columnName isEqualToString:MA_Field_Read]) {
[self.controller.articleController markReadByArray:@[theArticle] readFlag:!theArticle.read];
return;
}
if ([columnName isEqualToString:MA_Field_Flagged]) {
[self.controller.articleController markFlaggedByArray:@[theArticle] flagged:!theArticle.flagged];
return;
}
if ([columnName isEqualToString:MA_Field_HasEnclosure]) {
// TODO: Do interesting stuff with the enclosure here.
}
}
}
}
/* doubleClickRow
* Handle double-click on the selected article. Open the original feed item in
* the default browser.
*/
-(IBAction)doubleClickRow:(id)sender
{
NSInteger clickedRow = articleList.clickedRow;
if (clickedRow != -1) {
Article * theArticle = self.controller.articleController.allArticles[clickedRow];
[self.controller openURLFromString:theArticle.link inPreferredBrowser:YES];
}
}
/* ensureSelectedArticle
* Ensure that there is a selected article and that it is visible.
*/
-(void)ensureSelectedArticle
{
if (articleList.selectedRow == -1) {
[self makeRowSelectedAndVisible:0];
} else {
[articleList scrollRowToVisible:articleList.selectedRow];
}
}
/* updateVisibleColumns
* Iterates through the array of visible columns and makes them
* visible or invisible as needed.
*/
-(void)updateVisibleColumns
{
NSArray * fields = [[Database sharedManager] arrayOfFields];
NSInteger count = fields.count;
NSInteger index;
// Save current selection
NSIndexSet * selArray = articleList.selectedRowIndexes;
// Mark we're doing an update of the tableview
isInTableInit = YES;
[articleList setAutosaveName:nil];
[self updateArticleListRowHeight];
// Create the new columns
for (index = 0; index < count; ++index) {
Field * field = fields[index];
NSString * identifier = field.name;
NSInteger tag = field.tag;
BOOL showField;
// Handle which fields can be visible in the condensed (vertical) layout
// versus the report (horizontal) layout
if (tableLayout == VNALayoutReport) {
showField = field.visible && tag != ArticleFieldIDHeadlines && tag != ArticleFieldIDComments;
} else {
showField = NO;
if (tag == ArticleFieldIDRead || tag == ArticleFieldIDFlagged || tag == ArticleFieldIDHasEnclosure) {
showField = field.visible;
}
if (tag == ArticleFieldIDHeadlines) {
showField = YES;
}
}
// Set column hidden or shown
NSTableColumn *col = [articleList tableColumnWithIdentifier:identifier];
col.hidden = !showField;
// Add to the end only those columns which should be visible
// and aren't created yet
if (showField && [articleList columnWithIdentifier:identifier]==-1) {
NSTableColumn * column = [[NSTableColumn alloc] initWithIdentifier:identifier];
// Replace the normal text field cell with a progress text cell so we can
// display a progress indicator when loading HTML pages. NOTE: This is handled
// in willDisplayCell:forTableColumn:row: where it sets the inProgress flag.
// We need to use a different column for condensed layout vs. table layout.
BOOL isProgressColumn = NO;
if (tableLayout == VNALayoutReport && [column.identifier isEqualToString:MA_Field_Subject]) {
isProgressColumn = YES;
}
if (tableLayout == VNALayoutCondensed && [column.identifier isEqualToString:MA_Field_Headlines]) {
isProgressColumn = YES;
}
if (isProgressColumn) {
ProgressTextCell * progressCell;
progressCell = [[ProgressTextCell alloc] init];
column.dataCell = progressCell;
} else {
VNAVerticallyCenteredTextFieldCell * cell;
cell = [[VNAVerticallyCenteredTextFieldCell alloc] init];
column.dataCell = cell;
}
BOOL isResizable = (tag != ArticleFieldIDRead && tag != ArticleFieldIDFlagged && tag != ArticleFieldIDComments && tag != ArticleFieldIDHasEnclosure);
column.resizingMask = (isResizable ? NSTableColumnUserResizingMask : NSTableColumnNoResizing);
// the headline column is auto-resizable
column.resizingMask = column.resizingMask | ([column.identifier isEqualToString:MA_Field_Headlines] ? NSTableColumnAutoresizingMask : 0);
// Set the header attributes.
NSTableHeaderCell * headerCell = column.headerCell;
headerCell.title = field.displayName;
// Set the other column atributes.
[column setEditable:NO];
column.minWidth = 10;
[articleList addTableColumn:column];
}
// Set column size for visible columns
if (showField) {
NSTableColumn *column = [articleList tableColumnWithIdentifier:identifier];
column.width = field.width;
}
}
// Set the images for specific header columns
if (@available(macOS 11, *)) {
NSImageSymbolScale scale = NSImageSymbolScaleSmall;
NSImageSymbolConfiguration *config = nil;
config = [NSImageSymbolConfiguration configurationWithScale:scale];
NSImage *readImage = [NSImage imageWithSystemSymbolName:@"circlebadge"
accessibilityDescription:nil];
readImage = [readImage imageWithSymbolConfiguration:config];
NSImage *flagImage = [NSImage imageWithSystemSymbolName:@"flag"
accessibilityDescription:nil];
flagImage = [flagImage imageWithSymbolConfiguration:config];
NSImage *enclImage = [NSImage imageWithSystemSymbolName:@"paperclip"
accessibilityDescription:nil];
enclImage = [enclImage imageWithSymbolConfiguration:config];
[articleList setHeaderImage:MA_Field_Read
image:readImage];
[articleList setHeaderImage:MA_Field_Flagged
image:flagImage];
[articleList setHeaderImage:MA_Field_HasEnclosure
image:enclImage];
} else {
[articleList setHeaderImage:MA_Field_Read
image:[NSImage imageNamed:@"unread_header"]];
[articleList setHeaderImage:MA_Field_Flagged
image:[NSImage imageNamed:@"flagged_header"]];
[articleList setHeaderImage:MA_Field_HasEnclosure
image:[NSImage imageNamed:@"enclosure_header"]];
}
// Initialise the sort direction
[self showSortDirection];
// Put the selection back
[articleList selectRowIndexes:selArray byExtendingSelection:NO];
if (tableLayout == VNALayoutReport) {
articleList.autosaveName = @"Vienna3ReportLayoutColumns";
} else {
articleList.autosaveName = @"Vienna3CondensedLayoutColumns";
}
[articleList setAutosaveTableColumns:YES];
// Done
isInTableInit = NO;
}
/* saveTableSettings
* Save the table column settings, specifically the visibility and width.
*/
-(void)saveTableSettings
{
Preferences * prefs = [Preferences standardPreferences];
// Remember the current folder and article
NSString * guid = self.selectedArticle.guid;
[prefs setInteger:self.controller.articleController.currentFolderId forKey:MAPref_CachedFolderID];
[prefs setString:(guid != nil ? guid : @"") forKey:MAPref_CachedArticleGUID];
// An array we need for the settings
NSMutableArray * dataArray = [[NSMutableArray alloc] init];
// Create the new columns
for (Field * field in [[Database sharedManager] arrayOfFields]) {
[dataArray addObject:field.name];
[dataArray addObject:@(field.visible)];
[dataArray addObject:@(field.width)];
}
// Save these to the preferences
[prefs setObject:dataArray forKey:MAPref_ArticleListColumns];
// We're done
}
/* setTableViewFont
* Gets the font for the article list and adjusts the table view
* row height to properly display that font.
*/
-(void)setTableViewFont
{
Preferences * prefs = [Preferences standardPreferences];
articleListFont = [NSFont fontWithName:prefs.articleListFont size:prefs.articleListFontSize];
articleListUnreadFont = [prefs boolForKey:MAPref_ShowUnreadArticlesInBold] ? [[NSFontManager sharedFontManager] convertWeight:YES ofFont:articleListFont] : articleListFont;
reportCellDict[NSFontAttributeName] = articleListFont;
unreadReportCellDict[NSFontAttributeName] = articleListUnreadFont;
topLineDict[NSFontAttributeName] = articleListFont;
unreadTopLineDict[NSFontAttributeName] = articleListUnreadFont;
middleLineDict[NSFontAttributeName] = articleListFont;
linkLineDict[NSFontAttributeName] = articleListFont;
bottomLineDict[NSFontAttributeName] = articleListFont;
selectionDict[NSFontAttributeName] = articleListFont;
unreadTopLineSelectionDict[NSFontAttributeName] = articleListUnreadFont;
[self updateArticleListRowHeight];
}
/* updateArticleListRowHeight
* Compute the number of rows that the current view requires. For table layout, there's just
* one line. For condensed layout, the number of lines depends on which fields are visible but
* there's always a minimum of one line anyway.
*/
-(void)updateArticleListRowHeight
{
Database * db = [Database sharedManager];
CGFloat height = [APPCONTROLLER.layoutManager defaultLineHeightForFont:articleListFont];
NSInteger numberOfRowsInCell;
if (tableLayout == VNALayoutReport) {
numberOfRowsInCell = 1;
} else {
numberOfRowsInCell = 0;
if ([db fieldByName:MA_Field_Subject].visible) {
++numberOfRowsInCell;
}
if ([db fieldByName:MA_Field_Folder].visible || [db fieldByName:MA_Field_Date].visible || [db fieldByName:MA_Field_Author].visible) {
++numberOfRowsInCell;
}
if ([db fieldByName:MA_Field_Link].visible) {
++numberOfRowsInCell;
}
if ([db fieldByName:MA_Field_Summary].visible) {
++numberOfRowsInCell;
}
if (numberOfRowsInCell == 0) {
++numberOfRowsInCell;
}
}
articleList.rowHeight = (height + 2.0f) * (CGFloat)numberOfRowsInCell;
}
/* showSortDirection
* Shows the current sort column and direction in the table.
*/
-(void)showSortDirection
{
NSString * sortColumnIdentifier = self.controller.articleController.sortColumnIdentifier;
for (NSTableColumn * column in articleList.tableColumns) {
if ([column.identifier isEqualToString:sortColumnIdentifier]) {
NSString * imageName = ([[Preferences standardPreferences].articleSortDescriptors[0] ascending]) ? @"NSAscendingSortIndicator" : @"NSDescendingSortIndicator";
articleList.highlightedTableColumn = column;
[articleList setIndicatorImage:[NSImage imageNamed:imageName] inTableColumn:column];
} else {
// Remove any existing image in the column header.
[articleList setIndicatorImage:nil inTableColumn:column];
}
}
}
/* scrollToArticle
* Moves the selection to the specified article.
*/
-(void)scrollToArticle:(NSString *)guid
{
if (guid != nil) {
NSInteger rowIndex = 0;
for (Article * thisArticle in self.controller.articleController.allArticles) {
if ([thisArticle.guid isEqualToString:guid]) {
[self makeRowSelectedAndVisible:rowIndex];
return;
}
++rowIndex;
}
}
[articleList deselectAll:self];
[self refreshArticleAtCurrentRow];
}
/* mainView
* Return the primary view of this view.
*/
-(NSView *)mainView
{
return articleList;
}
/* canDeleteMessageAtRow
* Returns YES if the message at the specified row can be deleted, otherwise NO.
*/
-(BOOL)canDeleteMessageAtRow:(NSInteger)row
{
return articleList.window.visible && self.selectedArticle != nil && ![Database sharedManager].readOnly;
}
/* canGoForward
* Return TRUE if we can go forward in the backtrack queue.
*/
-(BOOL)canGoForward
{
return self.controller.articleController.canGoForward;
}
/* canGoBack
* Return TRUE if we can go backward in the backtrack queue.
*/
-(BOOL)canGoBack
{
return self.controller.articleController.canGoBack;
}
/* handleGoForward
* Move forward through the backtrack queue.
*/
-(IBAction)handleGoForward:(id)sender
{
[self.controller.articleController goForward];
}
/* handleGoBack
* Move backward through the backtrack queue.
*/
-(IBAction)handleGoBack:(id)sender
{
[self.controller.articleController goBack];
}
/* makeTextStandardSize
* Reset webview text size to default
*/
-(IBAction)makeTextStandardSize:(id)sender
{
[articleText resetTextSize];
}
/* makeTextSmaller
* Make webview text size smaller
*/
-(IBAction)makeTextSmaller:(id)sender
{
[articleText decreaseTextSize];
}
/* makeTextLarger
* Make webview text size larger
*/
-(IBAction)makeTextLarger:(id)sender
{
[articleText increaseTextSize];
}
/* updateAlternateMenuTitle
* Sets the approprate title for the alternate item in the contextual menu
* when user changes preference for opening pages in external browser
*/
- (void)updateAlternateMenuTitle
{
NSMenuItem *mainMenuItem;
NSMenuItem *contextualMenuItem;
NSInteger index;
NSMenu *articleListMenu = articleList.menu;
if (articleListMenu == nil) {
return;
}
mainMenuItem = menuItemWithAction(@selector(viewSourceHomePageInAlternateBrowser:));
if (mainMenuItem != nil) {
index = [articleListMenu indexOfItemWithTarget:nil andAction:@selector(viewSourceHomePageInAlternateBrowser:)];
if (index >= 0) {
contextualMenuItem = [articleListMenu itemAtIndex:index];
contextualMenuItem.title = mainMenuItem.title;
}
}
mainMenuItem = menuItemWithAction(@selector(viewArticlePagesInAlternateBrowser:));
if (mainMenuItem != nil) {
index = [articleListMenu indexOfItemWithTarget:nil andAction:@selector(viewArticlePagesInAlternateBrowser:)];
if (index >= 0) {
contextualMenuItem = [articleListMenu itemAtIndex:index];
contextualMenuItem.title = mainMenuItem.title;
}
}
} // updateAlternateMenuTitle
- (BOOL)acceptsFirstResponder
{
return YES;
}
/* handleKeyDown [delegate]
* Support special key codes. If we handle the key, return YES otherwise
* return NO to allow the framework to pass it on for default processing.
*/
-(BOOL)handleKeyDown:(unichar)keyChar withFlags:(NSUInteger)flags
{
return [self.controller handleKeyDown:keyChar withFlags:flags];
}
/* selectedArticle
* Returns the selected article, or nil if no article is selected.
*/
-(Article *)selectedArticle
{
NSInteger currentSelectedRow = articleList.selectedRow;
return (currentSelectedRow >= 0 && currentSelectedRow < self.controller.articleController.allArticles.count) ? self.controller.articleController.allArticles[currentSelectedRow] : nil;
}
/* printDocument
* Print the active article.
*/
-(void)printDocument:(id)sender
{
[articleText printDocument:sender];
}
/* handleArticleListFontChange
* Called when the user changes the article list font and/or size in the Preferences
*/
-(void)handleArticleListFontChange:(NSNotification *)note
{
[self setTableViewFont];
if (self == self.controller.articleController.mainArticleView) {
[articleList reloadData];
}
}
/* handleLoadFullHTMLChange
* Called when the user changes the folder setting to load the article in full HTML.
*/
-(void)handleLoadFullHTMLChange:(NSNotification *)note
{
if (self == self.controller.articleController.mainArticleView) {
[self refreshArticlePane];
}
}
/* handleReadingPaneChange
* Respond to the change to the reading pane orientation.
*/
-(void)handleReadingPaneChange:(NSNotificationCenter *)nc
{
if (self == self.controller.articleController.mainArticleView) {
[self setOrientation:[Preferences standardPreferences].layout];
[self updateVisibleColumns];
[articleList reloadData];
}
}
/* setOrientation
* Adjusts the article view orientation and updates the article list row
* height to accommodate the summary view
*/
-(void)setOrientation:(NSInteger)newLayout
{
isChangingOrientation = YES;
tableLayout = newLayout;
splitView2.autosaveName = nil;
splitView2.vertical = (newLayout == VNALayoutCondensed);
splitView2.dividerStyle = (splitView2.vertical ? NSSplitViewDividerStyleThin : NSSplitViewDividerStylePaneSplitter);
splitView2.autosaveName = (newLayout == VNALayoutCondensed ? @"Vienna3SplitView2CondensedLayout" : @"Vienna3SplitView2ReportLayout");
self.textViewWidthConstraint.active = splitView2.vertical;
[splitView2 display];
isChangingOrientation = NO;
}
/* makeRowSelectedAndVisible
* Selects the specified row in the table and makes it visible by
* scrolling it to the center of the table.
*/
-(void)makeRowSelectedAndVisible:(NSInteger)rowIndex
{
if (self.controller.articleController.allArticles.count == 0u) {
[articleList deselectAll:self];
} else if (rowIndex != articleList.selectedRow) {
[articleList selectRowIndexes:[NSIndexSet indexSetWithIndex:rowIndex] byExtendingSelection:NO];
// make sure our current selection is visible
[articleList scrollRowToVisible:rowIndex];
// then try to center it in the list
NSInteger pageSize = [articleList rowsInRect:articleList.visibleRect].length;
NSInteger lastRow = articleList.numberOfRows - 1;
NSInteger visibleRow = rowIndex + (pageSize / 2);
if (visibleRow > lastRow) {
visibleRow = lastRow;
}
[articleList scrollRowToVisible:visibleRow];
}
}
/*
* viewNextUnreadInFolder
* Search the following unread article in the current folder
* and select it if found
*/
-(BOOL)viewNextUnreadInFolder
{
return [self viewNextUnreadInCurrentFolder:(articleList.selectedRow + 1)];
}
/* viewNextUnreadInCurrentFolder
* Select the next unread article in the current folder after currentRow.
*/
-(BOOL)viewNextUnreadInCurrentFolder:(NSInteger)currentRow
{
if (currentRow < 0) {
currentRow = 0;
}
NSArray * allArticles = self.controller.articleController.allArticles;
NSInteger totalRows = allArticles.count;
Article * theArticle;
while (currentRow < totalRows) {
theArticle = allArticles[currentRow];
if (!theArticle.read) {
[self makeRowSelectedAndVisible:currentRow];
return YES;
}
++currentRow;
}
return NO;
}
// Display the enclosure view below the article list view.
- (void)showEnclosureView {
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
if (![userDefaults boolForKey:MAPref_ShowEnclosureBar]) {
return;
}
if (![self.contentStackView.views containsObject:self.enclosureView]) {
[self.contentStackView addView:self.enclosureView
inGravity:NSStackViewGravityTop];
}
}
// Hide the enclosure view if it is present.
- (void)hideEnclosureView {
if ([self.contentStackView.views containsObject:self.enclosureView]) {
[self.contentStackView removeView:self.enclosureView];
}
}
/* selectFirstUnreadInFolder
* Moves the selection to the first unread article in the current article list or the
* first article if the folder has no unread articles.
*/
-(BOOL)selectFirstUnreadInFolder
{
BOOL result = [self viewNextUnreadInCurrentFolder:-1];
if (!result) {
NSInteger count = self.controller.articleController.allArticles.count;
if (count > 0) {
[self makeRowSelectedAndVisible:0];
}
}
return result;
}
-(void)scrollDownDetailsOrNextUnread
{
if (articleText.canScrollDown) {
[(NSView *)articleText scrollPageDown:nil];
} else {
ArticleController * articleController = self.controller.articleController;
[articleController markReadByArray:articleController.markedArticleRange readFlag:YES];
[articleController displayNextUnread];
}
}
-(void)scrollUpDetailsOrGoBack
{
if (articleText.canScrollUp) {
[(NSView *)articleText scrollPageUp:nil];
} else {
[self.controller.articleController goBack];
}
}
/* viewLink
* There's no view link address for article views. If we eventually implement a local
* scheme such as vienna:<feedurl>/<guid> then we could use that as a link address.
*/
-(NSString *)viewLink
{
return nil;
}
/* performFindPanelAction
* Implement the search action.
*/
-(void)performFindPanelAction:(NSInteger)actionTag
{
[self.controller.articleController reloadArrayOfArticles];
// This action is send continuously by the filter field, so make sure not the mark read while searching
if (articleList.selectedRow < 0 && self.controller.articleController.allArticles.count > 0 ) {
BOOL shouldSelectArticle = YES;
if ([Preferences standardPreferences].markReadInterval > 0.0f) {
Article * article = self.controller.articleController.allArticles[0u];
if (!article.read) {
shouldSelectArticle = NO;
}
}
if (shouldSelectArticle) {
[self makeRowSelectedAndVisible:0];
}
}
}
/* refreshFolder
* Refreshes the current folder by applying the current sort or thread
* logic and redrawing the article list. The selected article is preserved
* and restored on completion of the refresh.
*/
-(void)refreshFolder:(NSInteger)refreshFlag
{
blockSelectionHandler = YES;
Article * currentSelectedArticle = self.selectedArticle;
switch (refreshFlag) {
case VNARefreshRedrawList:
break;
case VNARefreshReapplyFilter:
[self.controller.articleController refilterArrayOfArticles];
[self.controller.articleController sortArticles];
break;
case VNARefreshSortAndRedraw:
[self.controller.articleController sortArticles];
break;
}
[articleList reloadData];
[self scrollToArticle:currentSelectedArticle.guid];
blockSelectionHandler = NO;
}
/* startLoadIndicator
* add the indicator of articles' data being loaded
*/
-(void)startLoadIndicator
{
if (progressIndicator == nil) {
NSRect progressIndicatorFrame;
progressIndicatorFrame.size = NSMakeSize(articleList.visibleRect.size.width, PROGRESS_INDICATOR_DIMENSION);
progressIndicatorFrame.origin = articleList.visibleRect.origin;
progressIndicator = [[NSProgressIndicator alloc] initWithFrame:progressIndicatorFrame];
progressIndicator.displayedWhenStopped = NO;
[articleList addSubview:progressIndicator];
}
[progressIndicator startAnimation:self];
}
/* stopLoadIndicator
* remove the indicator of articles loading
*/
-(void)stopLoadIndicator
{
[progressIndicator stopAnimation:self];
[progressIndicator removeFromSuperviewWithoutNeedingDisplay];
progressIndicator = nil;
}
/* refreshImmediatelyArticleAtCurrentRow
* Refreshes the article at the current selected row.
*/
-(void)refreshImmediatelyArticleAtCurrentRow
{
[self refreshArticlePane];
Article * theArticle = self.selectedArticle;
if (theArticle != nil && !theArticle.read) {
CGFloat interval = [Preferences standardPreferences].markReadInterval;
if (interval > 0 && !isAppInitialising) {
markReadTimer = [NSTimer scheduledTimerWithTimeInterval:(double)interval
target:self
selector:@selector(markCurrentRead:)
userInfo:nil
repeats:NO];
}
}
}
/* refreshArticleAtCurrentRow
* Refreshes the article at the current selected row.
*/
-(void)refreshArticleAtCurrentRow
{
Article * article = self.selectedArticle;
if (article == nil) {
[articleText setArticles:@[]];
[self hideEnclosureView];
} else {
[self refreshImmediatelyArticleAtCurrentRow];
// Add this to the backtrack list
NSString * guid = article.guid;
[self.controller.articleController addBacktrack:guid];
}
}
/* handleRefreshArticle
* Respond to the notification to refresh the current article pane.
*/
-(void)handleRefreshArticle:(NSNotification *)nc
{
if (self == self.controller.articleController.mainArticleView && !isAppInitialising) {
[self refreshArticlePane];
}
}
/* handleArticleViewEnded
* Handle the end of a load whether or not it completed and whether or not an
* error occurred.
*/
- (void)handleArticleViewEnded:(NSNotification *)nc
{
if (nc.object == articleText) {
[self endMainFrameLoad];
}
}
/* clearCurrentURL
* Clears the current URL.
*/
-(void)clearCurrentURL
{
// If we already have an URL release it.
if (currentURL) {
currentURL = nil;
}
}
/* loadArticleLink
* Loads the specified link into the article text view. NOTE: This is done
* via this selector method so that this is called via the event queue in
* order to give the WebView drawing a chance to clear out the WebView
* before this link is loaded.
*/
-(void)loadArticleLink:(NSString *) articleLink
{
// Remember we're loading from HTML so the status message is set
// appropriately.
[self startMainFrameLoad];
// Load the actual link.
articleText.tabUrl = cleanedUpUrlFromString(articleLink);
[articleText loadTab];
// Clear the current URL.
[self clearCurrentURL];
// Remember the new URL.
currentURL = [[NSURL alloc] initWithString:articleLink];
// We need to redraw the article list so the progress indicator is shown.
articleList.needsDisplay = YES;
}
/* url
* Return the URL of current article.
*/
-(NSURL *)url
{
if (self.isCurrentPageFullHTML) {
return currentURL;
} else {
return nil;
}
}
/* refreshArticlePane
* Updates the article pane for the current selected articles.
*/
-(void)refreshArticlePane
{
NSArray * msgArray = self.markedArticleRange;
if (msgArray.count == 0) {
// Clear the current URL.
[self clearCurrentURL];
// We are not a FULL HTML page.
self.currentPageFullHTML = NO;
// Clear out the page.
[articleText setArticles:@[]];
} else {
Article * firstArticle = msgArray[0];
Folder * folder = [[Database sharedManager] folderFromID:firstArticle.folderId];
if (folder.loadsFullHTML && msgArray.count == 1) {
if (!self.currentPageFullHTML) {
// Clear out the text so the user knows something happened in response to the
// click on the article.
[articleText setArticles:@[]];
}
// Remember we have a full HTML page so we can setup the context menus
// appropriately.
self.currentPageFullHTML = YES;
// Now set the article to the URL in the RSS feed's article. NOTE: We use
// performSelector:withObject:afterDelay: here so that this link load gets
// queued up into the event loop, otherwise the WebView class won't draw the
// clearing of the HTML before this new link gets loaded.
[self performSelector: @selector(loadArticleLink:) withObject:firstArticle.link afterDelay:0.0];
} else {
// Clear the current URL.
[self clearCurrentURL];
// Remember we do NOT have a full HTML page so we can setup the context menus
// appropriately.
self.currentPageFullHTML = NO;
// Remember we're NOT loading from HTML so the status message is set
// appropriately.
isLoadingHTMLArticle = NO;
// Set the article to the HTML from the RSS feed.
[articleText setArticles:msgArray];
}
}
// Show the enclosure view if just one article is selected and it has an
// enclosure.
if (msgArray.count != 1) {
[self hideEnclosureView];
} else {
Article * oneArticle = msgArray[0];
if (!oneArticle.hasEnclosure) {
[self hideEnclosureView];
} else {
[self showEnclosureView];
[self.enclosureView setEnclosureFile:oneArticle.enclosure];
}
}
}
/* markCurrentRead
* Mark the current article as read.
*/
-(void)markCurrentRead:(NSTimer *)aTimer
{
Article * theArticle = self.selectedArticle;
if (theArticle != nil && !theArticle.read && ![Database sharedManager].readOnly) {
[self.controller.articleController markReadByArray:@[theArticle] readFlag:YES];
}
}
/* numberOfRowsInTableView [datasource]
* Datasource for the table view. Return the total number of rows we'll display which
* is equivalent to the number of articles in the current folder.
*/
-(NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
return self.controller.articleController.allArticles.count;
}
/* objectValueForTableColumn [datasource]
* Called by the table view to obtain the object at the specified column and row. This is
* called often so it needs to be fast.
*/
-(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
Database * db = [Database sharedManager];
NSArray * allArticles = self.controller.articleController.allArticles;
Article * theArticle;
if(rowIndex < 0 || rowIndex >= allArticles.count) {
return nil;
}
theArticle = allArticles[rowIndex];
NSString * identifier = aTableColumn.identifier;
if ([identifier isEqualToString:MA_Field_Read]) {
if (!theArticle.read) {
if (@available(macOS 11, *)) {
NSImage *image = nil;
if (theArticle.revised) {
image = [NSImage imageWithSystemSymbolName:@"sparkles"
accessibilityDescription:nil];
// Setting the template property to NO enables the tint color.
image.template = NO;
} else {
image = [NSImage imageWithSystemSymbolName:@"circlebadge.fill"
accessibilityDescription:nil];
// Setting the template property to NO enables the tint color.
image.template = NO;
}
return image;
} else {
if (theArticle.revised) {
return [NSImage imageNamed:@"revised"];
} else {
return [NSImage imageNamed:@"unread"];
}
}
}
return nil;
}
if ([identifier isEqualToString:MA_Field_Flagged]) {
if (theArticle.flagged) {
if (@available(macOS 11, *)) {
NSImage *image = [NSImage imageWithSystemSymbolName:@"flag.fill"
accessibilityDescription:nil];
// Setting the template property to NO enables the tint color.
image.template = NO;
return image;
} else {
return [NSImage imageNamed:@"flagged"];
}
}
return nil;
}
if ([identifier isEqualToString:MA_Field_Comments]) {
if (theArticle.hasComments) {
if (@available(macOS 11, *)) {
NSImage *image = [NSImage imageWithSystemSymbolName:@"ellipsis.bubble.fill"
accessibilityDescription:nil];
return image;
} else {
return [NSImage imageNamed:@"comments"];
}
}
return nil;
}
if ([identifier isEqualToString:MA_Field_HasEnclosure]) {
if (theArticle.hasEnclosure) {
if (@available(macOS 11, *)) {
NSImage *image = [NSImage imageWithSystemSymbolName:@"paperclip"
accessibilityDescription:nil];
return image;
} else {
return [NSImage imageNamed:@"enclosure"];
}
}
return nil;
}
NSMutableAttributedString * theAttributedString;
if ([identifier isEqualToString:MA_Field_Headlines]) {
theAttributedString = [[NSMutableAttributedString alloc] initWithString:@""];
BOOL isSelectedRow = [aTableView isRowSelected:rowIndex] && (NSApp.mainWindow.firstResponder == aTableView);
if ([db fieldByName:MA_Field_Subject].visible) {
NSDictionary * topLineDictPtr;
if (theArticle.read) {
topLineDictPtr = (isSelectedRow ? selectionDict : topLineDict);
} else {
topLineDictPtr = (isSelectedRow ? unreadTopLineSelectionDict : unreadTopLineDict);
}
NSString * topString = [NSString stringWithFormat:@"%@", theArticle.title];
NSMutableAttributedString * topAttributedString = [[NSMutableAttributedString alloc] initWithString:topString attributes:topLineDictPtr];
[topAttributedString fixFontAttributeInRange:NSMakeRange(0u, topAttributedString.length)];
[theAttributedString appendAttributedString:topAttributedString];
}
// Add the summary line that appears below the title.
if ([db fieldByName:MA_Field_Summary].visible) {
NSString * summaryString = theArticle.summary;
NSInteger maxSummaryLength = MIN([summaryString length], 150);
NSString * middleString = [NSString stringWithFormat:@"\n%@", [summaryString substringToIndex:maxSummaryLength]];
NSDictionary * middleLineDictPtr = (isSelectedRow ? selectionDict : middleLineDict);
NSMutableAttributedString * middleAttributedString = [[NSMutableAttributedString alloc] initWithString:middleString attributes:middleLineDictPtr];
[middleAttributedString fixFontAttributeInRange:NSMakeRange(0u, middleAttributedString.length)];
[theAttributedString appendAttributedString:middleAttributedString];
}
// Add the link line that appears below the summary and title.
if ([db fieldByName:MA_Field_Link].visible) {
NSString * articleLink = theArticle.link;
if (articleLink != nil) {
NSString * linkString = [NSString stringWithFormat:@"\n%@", articleLink];
NSMutableDictionary * linkLineDictPtr = (isSelectedRow ? selectionDict : linkLineDict);
NSURL * articleURL = [NSURL URLWithString:articleLink];
if (articleURL != nil) {
linkLineDictPtr = [linkLineDictPtr mutableCopy];
linkLineDictPtr[NSLinkAttributeName] = articleURL;
}
NSMutableAttributedString * linkAttributedString = [[NSMutableAttributedString alloc] initWithString:linkString attributes:linkLineDictPtr];
[linkAttributedString fixFontAttributeInRange:NSMakeRange(0u, linkAttributedString.length)];
[theAttributedString appendAttributedString:linkAttributedString];
}
}
// Create the detail line that appears at the bottom.
NSDictionary * bottomLineDictPtr = (isSelectedRow ? selectionDict : bottomLineDict);
NSMutableString * summaryString = [NSMutableString stringWithString:@""];
NSString * delimiter = @"";
if ([db fieldByName:MA_Field_Folder].visible) {
Folder * folder = [db folderFromID:theArticle.folderId];
[summaryString appendFormat:@"%@", folder.name];
delimiter = @" - ";
}
if ([db fieldByName:MA_Field_Date].visible) {
[summaryString appendFormat:@"%@%@", delimiter, [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date]];
delimiter = @" - ";
}
if ([db fieldByName:MA_Field_Author].visible) {
if (!theArticle.author.vna_isBlank) {
[summaryString appendFormat:@"%@%@", delimiter, theArticle.author];
}
}
if (![summaryString isEqualToString:@""]) {
summaryString = [NSMutableString stringWithFormat:@"\n%@", summaryString];
}
NSMutableAttributedString * summaryAttributedString = [[NSMutableAttributedString alloc] initWithString:summaryString attributes:bottomLineDictPtr];
[summaryAttributedString fixFontAttributeInRange:NSMakeRange(0u, summaryAttributedString.length)];
[theAttributedString appendAttributedString:summaryAttributedString];
return theAttributedString;
}
NSString * cellString;
if ([identifier isEqualToString:MA_Field_Date]) {
cellString = [NSDateFormatter vna_relativeDateStringFromDate:theArticle.date];
} else if ([identifier isEqualToString:MA_Field_Folder]) {
Folder * folder = [db folderFromID:theArticle.folderId];
cellString = folder.name;
} else if ([identifier isEqualToString:MA_Field_Author]) {
cellString = theArticle.author;
} else if ([identifier isEqualToString:MA_Field_Link]) {
cellString = theArticle.link;
} else if ([identifier isEqualToString:MA_Field_Subject]) {
cellString = theArticle.title;
} else if ([identifier isEqualToString:MA_Field_Summary]) {
cellString = theArticle.summary;
} else if ([identifier isEqualToString:MA_Field_Enclosure]) {
cellString = theArticle.enclosure;
} else {
cellString = @"";
[NSException raise:@"ArticleListView unknown table column identifier exception" format:@"Unknown table column identifier: %@", identifier];
}
theAttributedString = [[NSMutableAttributedString alloc] initWithString:SafeString(cellString) attributes:(theArticle.read ? reportCellDict : unreadReportCellDict)];
[theAttributedString fixFontAttributeInRange:NSMakeRange(0u, theAttributedString.length)];
return theAttributedString;
}
/* menuWillAppear [ExtendedTableView delegate]
* Called when the popup menu is opened on the table. We ensure that the item under the
* cursor is selected.
*/
-(void)tableView:(ExtendedTableView *)tableView menuWillAppear:(NSEvent *)theEvent
{
NSInteger row = [articleList rowAtPoint:[articleList convertPoint:theEvent.locationInWindow fromView:nil]];
if (row >= 0) {
// Select the row under the cursor if it isn't already selected
if (articleList.numberOfSelectedRows <= 1) {
blockSelectionHandler = YES; // to prevent expansion tooltip from overlapping the menu
if (row != articleList.selectedRow) {
[articleList selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
// will perform a refresh once the menu is deselected
[self performSelector: @selector(refreshArticleAtCurrentRow) withObject:nil afterDelay:0.0];
}
blockSelectionHandler = NO;
}
}
}
/* tableViewSelectionDidChange [delegate]
* Handle the selection changing in the table view unless blockSelectionHandler is set.
*/
-(void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
[markReadTimer invalidate];
markReadTimer = nil;
if (!blockSelectionHandler) {
[self refreshArticleAtCurrentRow];
}
}
/* shouldShowCellExpansionForTableColumn [delegate]
* Handle expansion tooltip for truncated texts
*/
- (BOOL)tableView:(NSTableView *)tableView shouldShowCellExpansionForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
// prevent overlapping of contextual menu and expansion tooltip
return !blockSelectionHandler;
}
/* didClickTableColumns
* Handle the user click in the column header to sort by that column.
*/
-(void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn
{
NSString * columnName = tableColumn.identifier;
[self.controller.articleController sortByIdentifier:columnName];
[self showSortDirection];
}
/* tableViewColumnDidResize
* This notification is called when the user completes resizing a column. We obtain the
* new column size and save the settings.
*/
-(void)tableViewColumnDidResize:(NSNotification *)notification
{
if (!isInTableInit && !isAppInitialising && !isChangingOrientation) {
NSTableColumn * tableColumn = notification.userInfo[@"NSTableColumn"];
Field * field = [[Database sharedManager] fieldByName:tableColumn.identifier];
NSInteger oldWidth = [notification.userInfo[@"NSOldWidth"] integerValue];
if (oldWidth != tableColumn.width) {
field.width = tableColumn.width;
[self saveTableSettings];
}
}
}
/* writeRowsWithIndexes
* Called to initiate a drag from MessageListView. Use the common copy selection code to copy to
* the pasteboard.
*/
-(BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(nonnull NSPasteboard *)pboard
{
return [self copyTableSelection:rowIndexes toPasteboard:pboard];
}
/* willDisplayCell
* Hook before a cell is displayed to set the cell's loading HTML flag for
* the progress indicator.
*/
-(void)tableView:(NSTableView *)tv willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)rowIndex
{
NSString * columnIdentifer = tableColumn.identifier;
BOOL isProgressColumn = NO;
// We need to use a different column for condensed layout vs. table layout.
if (tableLayout == VNALayoutReport && [columnIdentifer isEqualToString:MA_Field_Subject]) {
isProgressColumn = YES;
} else if (tableLayout == VNALayoutCondensed && [columnIdentifer isEqualToString:MA_Field_Headlines]) {
isProgressColumn = YES;
}
if (isProgressColumn) {
ProgressTextCell * realCell = (ProgressTextCell *)cell;
// Set the in-progress flag as appropriate so the progress indicator gets
// displayed and removed as needed.
if ([realCell respondsToSelector:@selector(setInProgress:forRow:)]) {
if (rowIndex == tv.selectedRow && isLoadingHTMLArticle) {
[realCell setInProgress:YES forRow:rowIndex];
} else {
[realCell setInProgress:NO forRow:rowIndex];
}
}
}
}
/* copyTableSelection
* This is the common copy selection code. We build an array of dictionary entries each of
* which include details of each selected article in the standard RSS item format defined by
* Ranchero NetNewsWire. See http://ranchero.com/netnewswire/rssclipboard.php for more details.
*/
-(BOOL)copyTableSelection:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard
{
NSMutableArray * arrayOfArticles = [[NSMutableArray alloc] init];
NSMutableArray * arrayOfURLs = [[NSMutableArray alloc] init];
NSMutableArray * arrayOfTitles = [[NSMutableArray alloc] init];
NSMutableString * fullHTMLText = [[NSMutableString alloc] init];
NSMutableString * fullPlainText = [[NSMutableString alloc] init];
Database * db = [Database sharedManager];
NSInteger count = rowIndexes.count;
// Set up the pasteboard
[pboard declareTypes:@[VNAPasteboardTypeRSSItem, VNAPasteboardTypeWebURLsWithTitles, NSPasteboardTypeString, NSPasteboardTypeHTML]
owner:self];
if (count == 1) {
[pboard addTypes:@[VNAPasteboardTypeURL, VNAPasteboardTypeURLName, NSPasteboardTypeURL]
owner:self];
}
// Open the HTML string
[fullHTMLText appendString:@"<html><body>"];
// Get all the articles that are being dragged
NSUInteger msgIndex = rowIndexes.firstIndex;
while (msgIndex != NSNotFound) {
Article * thisArticle = self.controller.articleController.allArticles[msgIndex];
Folder * folder = [db folderFromID:thisArticle.folderId];
NSString * msgText = thisArticle.body;
NSString * msgTitle = thisArticle.title;
NSString * msgLink = thisArticle.link;
[arrayOfURLs addObject:msgLink];
[arrayOfTitles addObject:msgTitle];
NSMutableDictionary * articleDict = [NSMutableDictionary dictionary];
[articleDict setValue:msgTitle forKey:@"rssItemTitle"];
[articleDict setValue:msgLink forKey:@"rssItemLink"];
[articleDict setValue:msgText forKey:@"rssItemDescription"];
[articleDict setValue:folder.name forKey:@"sourceName"];
[articleDict setValue:folder.homePage forKey:@"sourceHomeURL"];
[articleDict setValue:folder.feedURL forKey:@"sourceRSSURL"];
[arrayOfArticles addObject:articleDict];
// Plain text
[fullPlainText appendFormat:@"%@\n%@\n\n", msgTitle, msgText];
// Add HTML version too.
[fullHTMLText appendFormat:@"<a href=\"%@\">%@</a><br />%@<br /><br />", msgLink, msgTitle, msgText];
if (count == 1) {
[pboard setString:msgLink forType:VNAPasteboardTypeURL];
[pboard setString:msgTitle forType:VNAPasteboardTypeURLName];
// Write the link to the pastboard.
[[NSURL URLWithString:msgLink] writeToPasteboard:pboard];
}
msgIndex = [rowIndexes indexGreaterThanIndex:msgIndex];
}
// Close the HTML string
[fullHTMLText appendString:@"</body></html>"];
// Put string on the pasteboard for external drops.
[pboard setPropertyList:arrayOfArticles forType:VNAPasteboardTypeRSSItem];
[pboard setPropertyList:@[arrayOfURLs, arrayOfTitles] forType:VNAPasteboardTypeWebURLsWithTitles];
[pboard setString:fullPlainText forType:NSPasteboardTypeString];
[pboard setString:fullHTMLText.vna_stringByEscapingExtendedCharacters forType:NSPasteboardTypeHTML];
return YES;
}
/* markedArticleRange
* Retrieve an array of selected articles.
*/
-(NSArray *)markedArticleRange
{
NSMutableArray * articleArray = nil;
if (articleList.numberOfSelectedRows > 0) {
NSIndexSet * rowIndexes = articleList.selectedRowIndexes;
NSUInteger rowIndex = rowIndexes.firstIndex;
articleArray = [NSMutableArray arrayWithCapacity:rowIndexes.count];
while (rowIndex != NSNotFound) {
[articleArray addObject:self.controller.articleController.allArticles[rowIndex]];
rowIndex = [rowIndexes indexGreaterThanIndex:rowIndex];
}
}
return [articleArray copy];
}
/* dealloc
* Clean up behind ourself.
*/
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
[userDefaults removeObserver:self
forKeyPath:MAPref_ShowEnclosureBar
context:VNAArticleListViewObserverContext];
[userDefaults removeObserver:self
forKeyPath:MAPref_ShowUnreadArticlesInBold
context:VNAArticleListViewObserverContext];
[splitView2 setDelegate:nil];
[articleList setDelegate:nil];
}
// MARK: Key-value observation
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
if (context != VNAArticleListViewObserverContext) {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
return;
}
if ([keyPath isEqualToString:MAPref_ShowEnclosureBar]) {
NSNumber *showEnclosureBar = change[NSKeyValueChangeNewKey];
if (showEnclosureBar.boolValue) {
[self refreshArticlePane];
} else {
[self hideEnclosureView];
}
return;
}
if ([keyPath isEqualToString:MAPref_ShowUnreadArticlesInBold]) {
[self setTableViewFont];
if (self == self.controller.articleController.mainArticleView) {
[articleList reloadData];
}
}
//TODO
}
// MARK: ArticleView delegate
@synthesize error;
@synthesize controller;
- (void)startMainFrameLoad
{
isLoadingHTMLArticle = YES;
}
/// Handle the end of a load whether or not it completed and whether or not an
/// error occurred.
- (void)endMainFrameLoad
{
if (isLoadingHTMLArticle) {
isLoadingHTMLArticle = NO;
articleList.needsDisplay = YES;
}
}
// MARK : splitView2 delegate
- (void)splitViewWillResizeSubviews:(NSNotification *)notification {
NSDictionary * info = notification.userInfo;
NSInteger userResizeKey = ((NSNumber *)info[@"NSSplitViewUserResizeKey"]).integerValue;
if (userResizeKey == 1) { // user initiated resize
self.textViewWidthConstraint.active = NO;
if (self.imbricatedSplitViewResizes) {
// remove any other constraint affecting articleTextView's horizontal axis,
// and let autoresizing do the job
for (NSLayoutConstraint *c in [self.articleTextView constraintsAffectingLayoutForOrientation:NSLayoutConstraintOrientationHorizontal]) {
if ((c.firstItem == self.articleTextView || c.secondItem == self.articleTextView) && (c != self.textViewWidthConstraint)) {
[self.articleTextView removeConstraint:c];
}
}
self.articleTextView.translatesAutoresizingMaskIntoConstraints = YES;
} else {
self.imbricatedSplitViewResizes = YES;
}
}
}
- (void)splitViewDidResizeSubviews:(NSNotification *)notification {
// update and reactivate constraint
self.textViewWidthConstraint.constant = self.contentStackView.frame.size.width;
NSDictionary * info = notification.userInfo;
NSInteger userResizeKey = ((NSNumber *)info[@"NSSplitViewUserResizeKey"]).integerValue;
if (userResizeKey == 1) {
if (self.imbricatedSplitViewResizes) {
// remove again any other constraint affecting articleTextView's horizontal axis,
// and let autoresizing do the job
for (NSLayoutConstraint *c in [self.articleTextView constraintsAffectingLayoutForOrientation:NSLayoutConstraintOrientationHorizontal]) {
if ((c.firstItem == self.articleTextView || c.secondItem == self.articleTextView) && (c != self.textViewWidthConstraint)) {
[self.articleTextView removeConstraint:c];
}
}
self.articleTextView.translatesAutoresizingMaskIntoConstraints = YES;
} else {
self.articleTextView.translatesAutoresizingMaskIntoConstraints = NO;
self.textViewWidthConstraint.active = YES;
}
self.imbricatedSplitViewResizes = NO;
}
}
@end