Refactoring Part 3: Feed configuration and CoreData Model

This commit is contained in:
relikd
2018-12-09 01:49:26 +01:00
parent ae4700faca
commit 4c1ec7c474
28 changed files with 751 additions and 520 deletions

View File

@@ -21,22 +21,20 @@
// SOFTWARE.
#import <Cocoa/Cocoa.h>
#import "ModalSheet.h"
@class FeedConfig;
@class FeedGroup;
@protocol ModalEditDelegate <NSObject>
- (void)modalDidUpdateFeedConfig:(FeedConfig*)config;
@end
@protocol ModalFeedConfigEdit <NSObject>
@property (weak) id<ModalEditDelegate> delegate;
- (void)updateRepresentedObject; // must call [item.managedObjectContext refreshObject:item mergeChanges:YES];
@interface ModalEditDialog : NSViewController
+ (instancetype)modalWith:(FeedGroup*)group;
- (ModalSheet*)getModalSheet;
- (void)applyChangesToCoreDataObject;
@end
@interface ModalFeedEdit : NSViewController <ModalFeedConfigEdit, NSTextFieldDelegate>
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
@end
@interface ModalGroupEdit : NSViewController <ModalFeedConfigEdit>
@interface ModalGroupEdit : ModalEditDialog
@end

View File

@@ -23,6 +23,42 @@
#import "ModalFeedEdit.h"
#import "FeedDownload.h"
#import "StoreCoordinator.h"
#import "Feed+Ext.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#pragma mark - ModalEditDialog -
@interface ModalEditDialog()
@property (strong) FeedGroup *feedGroup;
@property (strong) ModalSheet *modalSheet;
@end
@implementation ModalEditDialog
/// Dedicated initializer for @c ModalEditDialog subclasses. Ensures @c .feedGroup property is set.
+ (instancetype)modalWith:(FeedGroup*)group {
ModalEditDialog *diag = [self new];
diag.feedGroup = group;
return diag;
}
/// @return New @c ModalSheet with its subclass @c .view property as dialog content.
- (ModalSheet *)getModalSheet {
if (!self.modalSheet)
self.modalSheet = [ModalSheet modalWithView:self.view];
return self.modalSheet;
}
/// This method should be overridden by subclasses. Used to save changes to persistent store.
- (void)applyChangesToCoreDataObject {
NSLog(@"[%@] is missing method: -(void)applyChangesToCoreDataObject", [self class]);
NSAssert(NO, @"Override required!");
}
@end
#pragma mark - ModalFeedEdit -
@interface ModalFeedEdit()
@property (weak) IBOutlet NSTextField *url;
@@ -34,143 +70,135 @@
@property (weak) IBOutlet NSButton *warningIndicator;
@property (weak) IBOutlet NSPopover *warningPopover;
@property (copy) NSString *previousURL;
@property (copy) NSString *previousURL; // check if changed and avoid multiple download
@property (copy) NSString *httpDate;
@property (copy) NSString *httpEtag;
@property (strong) NSError *feedError;
@property (strong) RSParsedFeed *feedResult;
@property (assign) BOOL shouldSaveObject;
@property (assign) BOOL shouldDeletePrevArticles;
@property (assign) BOOL objectNeedsSaving;
@property (assign) BOOL objectIsModified;
@property (strong) NSError *feedError; // download error or xml parser error
@property (strong) RSParsedFeed *feedResult; // parsed result
@property (assign) BOOL didDownloadFeed; // check if feed articles need update
@end
@implementation ModalFeedEdit
@synthesize delegate;
/// Init feed edit dialog with default values.
- (void)viewDidLoad {
[super viewDidLoad];
self.previousURL = @"";
self.refreshNum.intValue = 30;
self.shouldSaveObject = NO;
self.shouldDeletePrevArticles = NO;
self.objectNeedsSaving = NO;
self.objectIsModified = NO;
[self populateTextFields:self.feedGroup];
}
/**
Pre-fill UI control field values with @c FeedGroup properties.
*/
- (void)populateTextFields:(FeedGroup*)fg {
if (!fg || [fg hasChanges]) return; // hasChanges is true only if newly created
self.name.objectValue = fg.name;
self.url.objectValue = fg.feed.meta.url;
self.previousURL = self.url.stringValue;
self.refreshNum.intValue = fg.feed.meta.refreshNum;
NSInteger unit = (NSInteger)fg.feed.meta.refreshUnit;
if (unit < 0 || unit > self.refreshUnit.numberOfItems - 1)
unit = self.refreshUnit.numberOfItems - 1;
[self.refreshUnit selectItemAtIndex:unit];
}
#pragma mark - Edit Feed Data
/**
Use UI control field values to update the represented core data object. Also parse new articles if applicable.
Set @c scheduled to a new date if refresh interval was changed.
*/
- (void)applyChangesToCoreDataObject {
FeedMeta *meta = self.feedGroup.feed.meta;
BOOL intervalChanged = [meta setURL:self.previousURL refresh:self.refreshNum.intValue unit:(int16_t)self.refreshUnit.indexOfSelectedItem];
if (intervalChanged)
[meta calculateAndSetScheduled]; // updateTimer will be scheduled once preferences is closed
[self.feedGroup setName:self.name.stringValue andRefreshString:[meta readableRefreshString]];
if (self.didDownloadFeed) {
[meta setEtag:self.httpEtag modified:self.httpDate];
[self.feedGroup.feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
}
}
/**
Prepare UI (nullify @c result, @c error and start @c ProgressIndicator) and perform HTTP request.
Articles will be parsed and stored in class variables.
This should avoid unnecessary core data operations if user decides to cancel the edit.
The save operation will only be executed if user clicks on the 'OK' button.
*/
- (void)downloadRSS {
[self.modalSheet setDoneEnabled:NO];
// Assuming the user has not changed title since the last fetch.
// Reset to "" because after download it will be pre-filled with new feed title
if ([self.name.stringValue isEqualToString:self.feedResult.title]) {
self.name.stringValue = @"";
}
self.feedResult = nil;
self.feedError = nil;
self.httpEtag = nil;
self.httpDate = nil;
self.didDownloadFeed = NO;
[self.spinnerURL startAnimation:nil];
[self.spinnerName startAnimation:nil];
FeedConfig *fc = [self feedConfigOrNil];
if (fc) {
self.url.objectValue = fc.url;
self.name.objectValue = fc.name;
self.refreshNum.intValue = fc.refreshNum;
NSInteger unitIndex = fc.refreshUnit;
if (unitIndex < 0 || unitIndex > self.refreshUnit.numberOfItems - 1)
unitIndex = self.refreshUnit.numberOfItems - 1;
[self.refreshUnit selectItemAtIndex:unitIndex];
self.previousURL = self.url.stringValue;
[FeedDownload newFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.modalSheet.closeInitiated)
return;
self.didDownloadFeed = YES;
self.feedResult = result;
self.feedError = error; // MAIN THREAD!: warning indicator .hidden is bound to feedError
self.httpEtag = [response allHeaderFields][@"Etag"];
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
[self updateTextFieldURL:response.URL.absoluteString andTitle:result.title];
// TODO: add icon download
// TODO: play error sound?
[self.spinnerURL stopAnimation:nil];
[self.spinnerName stopAnimation:nil];
[self.modalSheet setDoneEnabled:YES];
});
}];
}
/// Set UI TextField values to downloaded values. Title will be updated if TextField is empty. URL on redirect.
- (void)updateTextFieldURL:(NSString*)responseURL andTitle:(NSString*)feedTitle {
// If URL was redirected (e.g., https redirect), replace original text field value with new one
if (responseURL.length > 0 && ![responseURL isEqualToString:self.previousURL]) {
self.previousURL = responseURL;
self.url.stringValue = responseURL;
}
// Copy feed title to text field. (only if user hasn't set anything else yet)
if ([self.name.stringValue isEqualToString:@""] && feedTitle.length > 0) {
self.name.stringValue = feedTitle; // no damage to replace an empty string
}
}
- (void)dealloc {
if (self.shouldSaveObject) {
if (self.objectNeedsSaving)
[self updateRepresentedObject];
FeedConfig *item = [self feedConfigOrNil];
NSUndoManager *um = item.managedObjectContext.undoManager;
[um endUndoGrouping];
if (!self.objectIsModified) {
[um disableUndoRegistration];
[um undoNestedGroup];
[um enableUndoRegistration];
} else {
[self.delegate modalDidUpdateFeedConfig:item];
}
}
}
- (void)updateRepresentedObject {
FeedConfig *item = [self feedConfigOrNil];
if (!item)
return;
if (!self.shouldSaveObject) // first call to this method
[item.managedObjectContext.undoManager beginUndoGrouping];
self.shouldSaveObject = YES;
self.objectNeedsSaving = NO; // after this method it is saved
// if's to prevent unnecessary undo groups if nothing has changed
if (![item.name isEqualToString: self.name.stringValue])
item.name = self.name.stringValue;
if (![item.url isEqualToString:self.url.stringValue])
item.url = self.url.stringValue;
if (item.refreshNum != self.refreshNum.intValue)
item.refreshNum = self.refreshNum.intValue;
if (item.refreshUnit != self.refreshUnit.indexOfSelectedItem)
item.refreshUnit = (int16_t)self.refreshUnit.indexOfSelectedItem;
if (self.shouldDeletePrevArticles) {
[item updateRSSFeed:self.feedResult];
[item setEtag:self.httpEtag modified:self.httpDate];
// TODO: add icon download
}
if ([item.managedObjectContext hasChanges]) {
self.objectIsModified = YES;
[item calculateAndSetScheduled];
[item.managedObjectContext refreshObject:item mergeChanges:YES];
}
}
- (FeedConfig*)feedConfigOrNil {
if ([self.representedObject isKindOfClass:[FeedConfig class]])
return self.representedObject;
return nil;
}
#pragma mark - NSTextField Delegate
/// Helper method to check whether url was modified since last download.
- (BOOL)urlHasChanged {
return ![self.previousURL isEqualToString:self.url.stringValue];
}
/// Hide warning button if an error was present but the user changed the url since.
- (void)controlTextDidChange:(NSNotification *)obj {
if (obj.object == self.url) {
self.warningIndicator.hidden = (!self.feedError || [self urlHasChanged]);
}
}
/// Whenever the user finished entering the url (return key or focus change) perform a download request.
- (void)controlTextDidEndEditing:(NSNotification *)obj {
if (obj.object == self.url && [self urlHasChanged]) {
self.shouldDeletePrevArticles = YES;
if (self.modalSheet.closeInitiated)
return;
self.previousURL = self.url.stringValue;
self.feedResult = nil;
self.feedError = nil;
[self.spinnerURL startAnimation:nil];
[self.spinnerName startAnimation:nil];
[FeedDownload newFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {
self.feedResult = result;
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
self.httpEtag = [response allHeaderFields][@"Etag"];
dispatch_async(dispatch_get_main_queue(), ^{
if (response && ![response.URL.absoluteString isEqualToString:self.url.stringValue]) {
// URL was redirected, so replace original text field value with new one
self.url.stringValue = response.URL.absoluteString;
self.previousURL = self.url.stringValue;
}
// TODO: play error sound?
self.feedError = error; // warning indicator .hidden is bound to feedError
self.objectNeedsSaving = YES; // stays YES if this block runs after updateRepresentedObject:
[self setTitleFromFeed];
[self.spinnerURL stopAnimation:nil];
[self.spinnerName stopAnimation:nil];
});
}];
}
}
- (void)setTitleFromFeed {
if ([self.name.stringValue isEqualToString:@""]) {
self.name.objectValue = self.feedResult.title;
[self downloadRSS];
}
}
/// Warning button next to url text field. Will be visible if an error occurs during download.
- (IBAction)didClickWarningButton:(NSButton*)sender {
if (!self.feedError)
return;
@@ -191,51 +219,49 @@
@end
#pragma mark - ModalGroupEdit
#pragma mark - ModalGroupEdit -
@implementation ModalGroupEdit
@synthesize delegate;
/// Init view and set group name if edeting an already existing object.
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.representedObject isKindOfClass:[FeedConfig class]]) {
FeedConfig *fc = self.representedObject;
((NSTextField*)self.view).objectValue = fc.name;
}
if (self.feedGroup && ![self.feedGroup hasChanges]) // hasChanges is true only if newly created
((NSTextField*)self.view).objectValue = self.feedGroup.name;
}
/// Set one single @c NSTextField as entire view. Populate with default value and placeholder.
- (void)loadView {
NSTextField *tf = [NSTextField textFieldWithString:NSLocalizedString(@"New Group", nil)];
tf.placeholderString = NSLocalizedString(@"New Group", nil);
tf.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
self.view = tf;
}
- (void)updateRepresentedObject {
if ([self.representedObject isKindOfClass:[FeedConfig class]]) {
FeedConfig *item = self.representedObject;
NSString *name = ((NSTextField*)self.view).stringValue;
if (![item.name isEqualToString: name]) {
item.name = name;
[item.managedObjectContext refreshObject:item mergeChanges:YES];
[self.delegate modalDidUpdateFeedConfig:item];
}
}
/// Edit of group finished. Save changes to core data object and perform save operation on delegate.
- (void)applyChangesToCoreDataObject {
NSString *name = ((NSTextField*)self.view).stringValue;
if (![self.feedGroup.name isEqualToString:name])
self.feedGroup.name = name;
}
@end
#pragma mark - StrictUIntFormatter
#pragma mark - StrictUIntFormatter -
@interface StrictUIntFormatter : NSFormatter
@end
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];

View File

@@ -21,18 +21,17 @@
// SOFTWARE.
#import "SettingsFeeds.h"
#import "BarMenu.h"
#import "ModalSheet.h"
#import "ModalFeedEdit.h"
#import "Constants.h"
#import "DrawImage.h"
#import "StoreCoordinator.h"
#import "Constants.h"
#import "ModalFeedEdit.h"
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
@interface SettingsFeeds () <ModalEditDelegate>
@interface SettingsFeeds ()
@property (weak) IBOutlet NSOutlineView *outlineView;
@property (weak) IBOutlet NSTreeController *dataStore;
@property (strong) NSViewController<ModalFeedConfigEdit> *modalController;
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong) NSUndoManager *undoManager;
@end
@@ -60,22 +59,21 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
}
- (IBAction)addFeed:(id)sender {
[self showModalForFeedConfig:nil isGroupEdit:NO];
[self showModalForFeedGroup:nil isGroupEdit:NO];
}
- (IBAction)addGroup:(id)sender {
[self showModalForFeedConfig:nil isGroupEdit:YES];
[self showModalForFeedGroup:nil isGroupEdit:YES];
}
- (IBAction)addSeparator:(id)sender {
[self.undoManager beginUndoGrouping];
FeedConfig *sp = [self insertSortedItemAtSelection];
sp.name = @"---";
sp.typ = SEPARATOR;
[self insertFeedGroupAtSelection:SEPARATOR].name = @"---";
[self.undoManager endUndoGrouping];
[self saveChanges];
}
/// Remove user selected item from persistent store.
- (IBAction)remove:(id)sender {
[self.undoManager beginUndoGrouping];
NSArray<NSTreeNode*> *parentNodes = [self.dataStore.selectedNodes valueForKeyPath:@"parentNode"];
@@ -88,99 +86,104 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
}
/// Open user selected item for editing.
- (IBAction)doubleClickOutlineView:(NSOutlineView*)sender {
if (sender.clickedRow == -1)
return; // ignore clicks on column headers and where no row was selected
FeedConfig *fc = [(NSTreeNode*)[sender itemAtRow:sender.clickedRow] representedObject];
[self showModalForFeedConfig:fc isGroupEdit:YES]; // yes will be overwritten anyway
FeedGroup *fg = [(NSTreeNode*)[sender itemAtRow:sender.clickedRow] representedObject];
[self showModalForFeedGroup:fg isGroupEdit:YES]; // yes will be overwritten anyway
}
#pragma mark - Insert & Edit Feed Items
- (void)openModalForSelection {
[self showModalForFeedConfig:self.dataStore.selectedObjects.firstObject isGroupEdit:YES]; // yes will be overwritten anyway
}
- (void)showModalForFeedConfig:(FeedConfig*)obj isGroupEdit:(BOOL)group {
BOOL existingItem = [obj isKindOfClass:[FeedConfig class]];
if (existingItem) {
if (obj.typ == SEPARATOR) return;
group = (obj.typ == GROUP);
/**
Open a new modal window to edit the selected @c FeedGroup.
@note isGroupEdit @c flag will be overwritten if @c FeedGroup parameter is not @c nil.
@param fg @c FeedGroup to be edited. If @c nil a new object will be created at the current selection.
@param flag If @c YES open group edit modal dialog. If @c NO open feed edit modal dialog.
*/
- (void)showModalForFeedGroup:(FeedGroup*)fg isGroupEdit:(BOOL)flag {
if (fg.typ == SEPARATOR) return;
[self.undoManager beginUndoGrouping];
if (!fg || ![fg isKindOfClass:[FeedGroup class]]) {
fg = [self insertFeedGroupAtSelection:(flag ? GROUP : FEED)];
}
self.modalController = (group ? [ModalGroupEdit new] : [ModalFeedEdit new]);
self.modalController.representedObject = obj;
self.modalController.delegate = self;
[self.view.window beginSheet:[ModalSheet modalWithView:self.modalController.view] completionHandler:^(NSModalResponse returnCode) {
ModalEditDialog *editDialog = (fg.typ == GROUP ? [ModalGroupEdit modalWith:fg] : [ModalFeedEdit modalWith:fg]);
[self.view.window beginSheet:[editDialog getModalSheet] completionHandler:^(NSModalResponse returnCode) {
if (returnCode == NSModalResponseOK) {
if (!existingItem) { // create new item
[self.undoManager beginUndoGrouping];
FeedConfig *item = [self insertSortedItemAtSelection];
item.typ = (group ? GROUP : FEED);
self.modalController.representedObject = item;
}
[self.modalController updateRepresentedObject];
if (!existingItem)
[self.undoManager endUndoGrouping];
[editDialog applyChangesToCoreDataObject];
[self.undoManager endUndoGrouping];
} else {
[self.undoManager endUndoGrouping];
[self.dataStore.managedObjectContext rollback];
}
BOOL hasChanges = [self.dataStore.managedObjectContext hasChanges];
if (hasChanges) {
[self saveChanges];
[self.dataStore rearrangeObjects];
} else {
[self.undoManager disableUndoRegistration];
[self.undoManager undoNestedGroup];
[self.undoManager enableUndoRegistration];
}
self.modalController = nil;
}];
}
/// Called after an item was modified. May be called twice if download was still in progress.
- (void)modalDidUpdateFeedConfig:(FeedConfig*)config {
[self saveChanges];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
}
#pragma mark - Helper -
/// Insert @c FeedConfig item either after current selection or inside selected folder (if expanded)
- (FeedConfig*)insertSortedItemAtSelection {
FeedConfig *newItem = [[FeedConfig alloc] initWithEntity:FeedConfig.entity insertIntoManagedObjectContext:self.dataStore.managedObjectContext];
NSTreeNode *selection = [[self.dataStore selectedNodes] firstObject];
NSIndexPath *pth = nil;
/// Insert @c FeedGroup item either after current selection or inside selected folder (if expanded)
- (FeedGroup*)insertFeedGroupAtSelection:(FeedGroupType)type {
FeedGroup *fg = [FeedGroup newGroup:type inContext:self.dataStore.managedObjectContext];
NSIndexPath *pth = [self indexPathForInsertAtNode:[[self.dataStore selectedNodes] firstObject]];
[self.dataStore insertObject:fg atArrangedObjectIndexPath:pth];
if (!selection) { // append to root
pth = [NSIndexPath indexPathWithIndex:[self.dataStore arrangedObjects].childNodes.count]; // or 0 to append at front
} else if ([self.outlineView isItemExpanded:selection]) { // append to group (if open)
pth = [selection.indexPath indexPathByAddingIndex:0]; // or 'selection.childNodes.count' to append at end
} else { // append before / after selected item
pth = selection.indexPath;
// remove the two lines below to insert infront of selection (instead of after selection)
NSUInteger lastIdx = [pth indexAtPosition:pth.length - 1];
pth = [[pth indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIdx + 1];
}
[self.dataStore insertObject:newItem atArrangedObjectIndexPath:pth];
if (pth.length > 2) { // some subfolder; not root folder (has parent!)
if (pth.length > 1) { // some subfolder and not root folder (has parent!)
NSTreeNode *parentNode = [[self.dataStore arrangedObjects] descendantNodeAtIndexPath:pth].parentNode;
newItem.parent = parentNode.representedObject;
fg.parent = parentNode.representedObject;
[self restoreOrderingAndIndexPathStr:parentNode];
} else {
[self restoreOrderingAndIndexPathStr:[self.dataStore arrangedObjects]]; // .parent = nil
}
return newItem;
return fg;
}
/// Loop over all descendants and update @c sortIndex @c (FeedConfig) as well as all @c indexPath @c (Feed)
/**
Index path will be selected as follow:
- @b root: append at end
- @b folder (expanded): append at front
- @b else: append after item.
@return indexPath where item will be inserted.
*/
- (NSIndexPath*)indexPathForInsertAtNode:(NSTreeNode*)node {
if (!node) { // append to root
return [NSIndexPath indexPathWithIndex:[self.dataStore arrangedObjects].childNodes.count]; // or 0 to append at front
} else if ([self.outlineView isItemExpanded:node]) { // append to group (if open)
return [node.indexPath indexPathByAddingIndex:0]; // or 'selection.childNodes.count' to append at end
} else { // append before / after selected item
NSIndexPath *pth = node.indexPath;
// remove the two lines below to insert infront of selection (instead of after selection)
NSUInteger lastIdx = [pth indexAtPosition:pth.length - 1];
return [[pth indexPathByRemovingLastIndex] indexPathByAddingIndex:lastIdx + 1];
}
}
/// Loop over all descendants and update @c sortIndex @c (FeedGroup) as well as all @c indexPath @c (Feed)
- (void)restoreOrderingAndIndexPathStr:(NSTreeNode*)parent {
NSArray<NSTreeNode*> *children = parent.childNodes;
for (NSUInteger i = 0; i < children.count; i++) {
NSTreeNode *n = [children objectAtIndex:i];
FeedConfig *fc = n.representedObject;
// Re-calculate sort index for all affected parents
if (fc.sortIndex != (int32_t)i)
fc.sortIndex = (int32_t)i;
// Re-calculate index path for all contained feed items
[fc iterateSorted:NO overDescendantFeeds:^(Feed *feed, BOOL *cancel) {
NSString *pthStr = [feed.config indexPathString];
if (![feed.indexPath isEqualToString:pthStr])
feed.indexPath = pthStr;
FeedGroup *fg = [children objectAtIndex:i].representedObject;
if (fg.sortIndex != (int32_t)i)
fg.sortIndex = (int32_t)i;
NSLog(@"%@ - %d", fg.name, fg.sortIndex);
[fg iterateSorted:NO overDescendantFeeds:^(Feed *feed, BOOL *cancel) {
[feed calculateAndSetIndexPathString];
}];
}
}
@@ -189,6 +192,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
#pragma mark - Dragging Support, Data Source Delegate
/// Begin drag-n-drop operation by copying selected nodes to memory
- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard {
[self.undoManager beginUndoGrouping];
[pboard declareTypes:[NSArray arrayWithObject:dragNodeType] owner:self];
@@ -197,6 +201,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
return YES;
}
/// Finish drag-n-drop operation by saving changes to persistent store
- (void)outlineView:(NSOutlineView *)outlineView draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation {
[self.undoManager endUndoGrouping];
if (self.dataStore.managedObjectContext.hasChanges) {
@@ -209,6 +214,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
self.currentlyDraggedNodes = nil;
}
/// Perform drag-n-drop operation, move nodes to new destination and update all indices
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index {
NSTreeNode *destParent = (item != nil ? item : [self.dataStore arrangedObjects]);
NSUInteger idx = (NSUInteger)index;
@@ -223,17 +229,16 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self restoreOrderingAndIndexPathStr:node];
}
[self restoreOrderingAndIndexPathStr:destParent];
return YES;
}
/// Validate method whether items can be dropped at destination
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index {
FeedConfig *fc = [(NSTreeNode*)item representedObject];
if (index == -1 && fc.typ != GROUP) { // if drag is on specific item and that item isnt a group
NSTreeNode *parent = item;
if (index == -1 && [parent isLeaf]) { // if drag is on specific item and that item isnt a group
return NSDragOperationNone;
}
NSTreeNode *parent = item;
while (parent != nil) {
for (NSTreeNode *node in self.currentlyDraggedNodes) {
if (parent == node)
@@ -248,10 +253,11 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
#pragma mark - Data Source Delegate
/// Populate @c NSOutlineView data cells with core data object values.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
FeedConfig *f = [(NSTreeNode*)item representedObject];
BOOL isFeed = (f.typ == FEED);
BOOL isSeperator = (f.typ == SEPARATOR);
FeedGroup *fg = [(NSTreeNode*)item representedObject];
BOOL isFeed = (fg.typ == FEED);
BOOL isSeperator = (fg.typ == SEPARATOR);
BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"];
NSString *cellIdent = (isRefreshColumn ? @"cellRefresh" : (isSeperator ? @"cellSeparator" : @"cellFeed"));
@@ -259,12 +265,12 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil];
if (isRefreshColumn) {
cellView.textField.stringValue = (!isFeed ? @"" : [f readableRefreshString]);
cellView.textField.stringValue = (isFeed && fg.refreshStr.length > 0 ? fg.refreshStr : @"");
} else if (isSeperator) {
return cellView; // the refresh cell is already skipped with the above if condition
} else {
cellView.textField.objectValue = f.name;
if (f.typ == GROUP) {
cellView.textField.objectValue = fg.name;
if (fg.typ == GROUP) {
cellView.imageView.image = [NSImage imageNamed:NSImageNameFolder];
} else {
// TODO: load icon
@@ -275,8 +281,10 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
cellView.imageView.image = defaultRSSIcon;
}
}
if (isFeed) // also for refresh column
cellView.textField.textColor = (f.refreshNum == 0 ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);
if (isFeed) {// also for refresh column
BOOL feedDisbaled = (fg.refreshStr.length == 0 || [fg.refreshStr characterAtIndex:0] == '0');
cellView.textField.textColor = (feedDisbaled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);
}
return cellView;
}
@@ -284,6 +292,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
#pragma mark - Keyboard Commands: undo, redo, copy, enter
/// Returning @c NO will result in a Action-Not-Available-Buzzer sound
- (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(undo:)) return [self.undoManager canUndo];
if (aSelector == @selector(redo:)) return [self.undoManager canRedo];
@@ -295,11 +304,12 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
if (aSelector == @selector(copy:))
return YES;
// can edit only if selection is not a separator
return (((FeedConfig*)self.dataStore.selectedNodes.firstObject.representedObject).typ != SEPARATOR);
return (((FeedGroup*)self.dataStore.selectedNodes.firstObject.representedObject).typ != SEPARATOR);
}
return [super respondsToSelector:aSelector];
}
/// Perform undo operation and redraw UI & menu bar unread count
- (void)undo:(id)sender {
[self.undoManager undo];
[self saveChanges];
@@ -307,6 +317,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self.dataStore rearrangeObjects]; // update ordering
}
/// Perform redo operation and redraw UI & menu bar unread count
- (void)redo:(id)sender {
[self.undoManager redo];
[self saveChanges];
@@ -314,10 +325,12 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
[self.dataStore rearrangeObjects]; // update ordering
}
/// User pressed enter; open edit dialog for selected item.
- (void)enterPressed:(id)sender {
[self openModalForSelection];
[self showModalForFeedGroup:self.dataStore.selectedObjects.firstObject isGroupEdit:YES]; // yes will be overwritten anyway
}
/// Copy human readable description of selected nodes to clipboard.
- (void)copy:(id)sender {
NSMutableString *str = [[NSMutableString alloc] init];
NSUInteger count = self.dataStore.selectedNodes.count;

View File

@@ -15,7 +15,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<treeController mode="entity" entityName="FeedConfig" fetchPredicateFormat="parent == nil" automaticallyPreparesContent="YES" childrenKeyPath="children" leafKeyPath="type" id="JPf-gH-wxm"/>
<treeController mode="entity" entityName="FeedGroup" fetchPredicateFormat="parent == nil" automaticallyPreparesContent="YES" childrenKeyPath="children" leafKeyPath="type" id="JPf-gH-wxm"/>
<customView id="zfc-Ie-Sdx" userLabel="View">
<rect key="frame" x="0.0" y="0.0" width="320" height="327"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

View File

@@ -25,8 +25,8 @@
#import "BarMenu.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import <ServiceManagement/ServiceManagement.h>
#import <ServiceManagement/ServiceManagement.h>
@interface SettingsGeneral()
@property (weak) IBOutlet NSPopUpButton *popupHttpApplication;
@@ -48,6 +48,7 @@
#pragma mark - UI interaction with IBAction
/// Run helper application to add thyself to startup items.
- (IBAction)changeStartOnLogin:(NSButton *)sender {
// launchctl list | grep de.relikd
CFStringRef helperIdentifier = CFBridgingRetain(@"de.relikd.baRSS-Helper");

View File

@@ -24,6 +24,7 @@
@implementation UserPrefs
/// @return @c YES if key is not set. Otherwise, return user defaults property from plist.
+ (BOOL)defaultYES:(NSString*)key {
if ([[NSUserDefaults standardUserDefaults] objectForKey:key] == NULL) {
return YES;
@@ -31,14 +32,17 @@
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
}
/// @return @c NO if key is not set. Otherwise, return user defaults property from plist.
+ (BOOL)defaultNO:(NSString*)key {
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
}
/// @return User configured custom browser. Or @c nil if not set yet. (which will fallback to default browser)
+ (NSString*)getHttpApplication {
return [[NSUserDefaults standardUserDefaults] stringForKey:@"defaultHttpApplication"];
}
/// Store custom browser bundle id to user defaults.
+ (void)setHttpApplication:(NSString*)bundleID {
[[NSUserDefaults standardUserDefaults] setObject:bundleID forKey:@"defaultHttpApplication"];
}

View File

@@ -23,6 +23,8 @@
#import <Cocoa/Cocoa.h>
@interface ModalSheet : NSPanel
@property (readonly) BOOL closeInitiated;
+ (instancetype)modalWithView:(NSView*)content;
- (void)setDoneEnabled:(BOOL)accept;
@end

View File

@@ -27,12 +27,22 @@
@end
@implementation ModalSheet
@synthesize closeInitiated = _closeInitiated;
/// User did click the 'Done' button.
- (void)didTapDoneButton:(id)sender { [self closeWithResponse:NSModalResponseOK]; }
/// User did click the 'Cancel' button.
- (void)didTapCancelButton:(id)sender { [self closeWithResponse:NSModalResponseAbort]; }
/// Manually disable 'Done' button if a task is still running.
- (void)setDoneEnabled:(BOOL)accept { self.btnDone.enabled = accept; }
/**
Called after user has clicked the 'Done' (Return) or 'Cancel' (Esc) button.
Flags controller as being closed @c .closeInitiated @c = @c YES.
And removes all subviews (clean up).
*/
- (void)closeWithResponse:(NSModalResponse)response {
_closeInitiated = YES;
// store modal view width and remove subviews to avoid _NSKeyboardFocusClipView issues
// first object is always the view of the modal dialog
CGFloat w = self.contentView.subviews.firstObject.frame.size.width;
@@ -41,6 +51,12 @@
[self.sheetParent endSheet:self returnCode:response];
}
/**
Designated initializer for @c ModalSheet.
@param content @c NSView will be displayed in dialog box. 'Done' and 'Cancel' button will be added automatically.
*/
+ (instancetype)modalWithView:(NSView*)content {
static const int padWindow = 20;
static const int padButtons = 12;

View File

@@ -32,6 +32,7 @@
@implementation Preferences
/// Restore tab selection from previous session
- (void)windowDidLoad {
[super windowDidLoad];
NSUInteger idx = (NSUInteger)[[NSUserDefaults standardUserDefaults] integerForKey:@"preferencesTab"];
@@ -40,6 +41,7 @@
[self tabClicked:self.window.toolbar.items[idx]];
}
/// Replace content view according to selected tab
- (IBAction)tabClicked:(NSToolbarItem *)sender {
self.window.contentView = nil;
if ([sender.itemIdentifier isEqualToString:@"tabGeneral"]) {
@@ -59,7 +61,7 @@
@end
/// A window that does not respond to Cmd-C, Cmd-Z, Cmd-Shift-Z and Enter-pressed events.
@interface NonRespondingWindow : NSWindow
@end