From d1afaef1de96aa32639f025ede63068dc37d7197 Mon Sep 17 00:00:00 2001 From: relikd Date: Fri, 10 Aug 2018 02:59:24 +0200 Subject: [PATCH] - Moved modal sheet to its own ViewController - Warning Indicator for non parsable URLs - Popover for error description - Modal Controller handles CoreData update - Bugfixes --- baRSS.xcodeproj/project.pbxproj | 32 ++- baRSS/DrawImage.m | 3 +- baRSS/NewsController.h | 1 + baRSS/NewsController.m | 11 + baRSS/Preferences/FeedConfig+Print.h | 40 ++++ baRSS/Preferences/Feeds Tab/ModalFeedEdit.h | 35 +++ baRSS/Preferences/Feeds Tab/ModalFeedEdit.m | 218 ++++++++++++++++++ baRSS/Preferences/Feeds Tab/ModalFeedEdit.xib | 175 ++++++++++++++ .../SettingsFeeds.h | 0 .../SettingsFeeds.m | 121 +++++----- .../SettingsFeeds.xib | 117 +--------- .../SettingsGeneral.h | 0 .../SettingsGeneral.m | 0 .../SettingsGeneral.xib | 2 +- baRSS/Preferences/ModalSheet.h | 24 +- baRSS/Preferences/ModalSheet.m | 82 ++----- 16 files changed, 584 insertions(+), 277 deletions(-) create mode 100644 baRSS/Preferences/FeedConfig+Print.h create mode 100644 baRSS/Preferences/Feeds Tab/ModalFeedEdit.h create mode 100644 baRSS/Preferences/Feeds Tab/ModalFeedEdit.m create mode 100644 baRSS/Preferences/Feeds Tab/ModalFeedEdit.xib rename baRSS/Preferences/{Settings Tabs => Feeds Tab}/SettingsFeeds.h (100%) rename baRSS/Preferences/{Settings Tabs => Feeds Tab}/SettingsFeeds.m (75%) rename baRSS/Preferences/{Settings Tabs => Feeds Tab}/SettingsFeeds.xib (67%) rename baRSS/Preferences/{Settings Tabs => General Tab}/SettingsGeneral.h (100%) rename baRSS/Preferences/{Settings Tabs => General Tab}/SettingsGeneral.m (100%) rename baRSS/Preferences/{Settings Tabs => General Tab}/SettingsGeneral.xib (99%) diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index ba756f5..21ae6af 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; }; 54ACC29521061E270020715F /* NewsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* NewsController.m */; }; 54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; }; + 54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E8831E211B509D00064188 /* ModalFeedEdit.m */; }; + 54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54E8831F211B509D00064188 /* ModalFeedEdit.xib */; }; 54F39C2E210BE1F700AEE730 /* DBv1.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28221061B3B0020715F /* DBv1.xcdatamodeld */; }; /* End PBXBuildFile section */ @@ -59,6 +61,10 @@ 54ACC29421061E270020715F /* NewsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NewsController.m; sourceTree = ""; }; 54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = ""; }; 54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = ""; }; + 54E8831D211B509D00064188 /* ModalFeedEdit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModalFeedEdit.h; sourceTree = ""; }; + 54E8831E211B509D00064188 /* ModalFeedEdit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEdit.m; sourceTree = ""; }; + 54E8831F211B509D00064188 /* ModalFeedEdit.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ModalFeedEdit.xib; sourceTree = ""; }; + 54EC3E1D211D03C100E314F4 /* FeedConfig+Print.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedConfig+Print.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -81,28 +87,27 @@ name = Frameworks; sourceTree = ""; }; - 546FC44521189ADC007CC3A3 /* Settings Tabs */ = { + 546FC44521189ADC007CC3A3 /* General Tab */ = { isa = PBXGroup; children = ( 546FC44021189975007CC3A3 /* SettingsGeneral.h */, 546FC44121189975007CC3A3 /* SettingsGeneral.m */, 546FC44221189975007CC3A3 /* SettingsGeneral.xib */, - 546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */, - 546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */, - 546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */, ); - path = "Settings Tabs"; + path = "General Tab"; sourceTree = ""; }; 546FC44D2118B357007CC3A3 /* Preferences */ = { isa = PBXGroup; children = ( + 54EC3E1D211D03C100E314F4 /* FeedConfig+Print.h */, 54ACC29621061FBA0020715F /* Preferences.h */, 54ACC29721061FBA0020715F /* Preferences.m */, 546FC4462118A8E6007CC3A3 /* Preferences.xib */, - 546FC44521189ADC007CC3A3 /* Settings Tabs */, 544B01182114B41200386E5C /* ModalSheet.h */, 544B01192114B41200386E5C /* ModalSheet.m */, + 546FC44521189ADC007CC3A3 /* General Tab */, + 54E88323211B542E00064188 /* Feeds Tab */, ); path = Preferences; sourceTree = ""; @@ -157,6 +162,19 @@ path = baRSS; sourceTree = ""; }; + 54E88323211B542E00064188 /* Feeds Tab */ = { + isa = PBXGroup; + children = ( + 546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */, + 546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */, + 546FC43E21188C78007CC3A3 /* SettingsFeeds.xib */, + 54E8831D211B509D00064188 /* ModalFeedEdit.h */, + 54E8831E211B509D00064188 /* ModalFeedEdit.m */, + 54E8831F211B509D00064188 /* ModalFeedEdit.xib */, + ); + path = "Feeds Tab"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -224,6 +242,7 @@ buildActionMask = 2147483647; files = ( 546FC4472118A8E6007CC3A3 /* Preferences.xib in Resources */, + 54E88321211B509D00064188 /* ModalFeedEdit.xib in Resources */, 54ACC28621061B3C0020715F /* Assets.xcassets in Resources */, 546FC44421189975007CC3A3 /* SettingsGeneral.xib in Resources */, 546FC43F21188C78007CC3A3 /* SettingsFeeds.xib in Resources */, @@ -249,6 +268,7 @@ 544B011A2114B41200386E5C /* ModalSheet.m in Sources */, 54ACC29821061FBA0020715F /* Preferences.m in Sources */, 546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */, + 54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */, 1968E0AE14B8E8A90E194980 /* PyHandler.m in Sources */, 54209E942117325100F3B5EF /* DrawImage.m in Sources */, ); diff --git a/baRSS/DrawImage.m b/baRSS/DrawImage.m index 0f04680..a1b1c78 100644 --- a/baRSS/DrawImage.m +++ b/baRSS/DrawImage.m @@ -117,7 +117,8 @@ @implementation DrawSeparator - (void)drawRect:(NSRect)dirtyRect { NSGradient *grdnt = [[NSGradient alloc] initWithStartingColor:[NSColor darkGrayColor] endingColor:[[NSColor darkGrayColor] colorWithAlphaComponent:0.0]]; - NSBezierPath *rounded = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(1, self.bounds.size.height/2.0-1, self.bounds.size.width-2, 2) xRadius:1 yRadius:1]; + NSRect separatorRect = NSMakeRect(1, self.frame.size.height / 2.0 - 1, self.frame.size.width - 2, 2); + NSBezierPath *rounded = [NSBezierPath bezierPathWithRoundedRect:separatorRect xRadius:1 yRadius:1]; [grdnt drawInBezierPath:rounded angle:0]; } @end diff --git a/baRSS/NewsController.h b/baRSS/NewsController.h index f08d98f..0a84c0f 100644 --- a/baRSS/NewsController.h +++ b/baRSS/NewsController.h @@ -24,4 +24,5 @@ @interface NewsController : NSObject ++ (void)downloadFeed:(NSString*)url withBlock:(nullable void (^)(NSDictionary* result, NSError* error))block; @end diff --git a/baRSS/NewsController.m b/baRSS/NewsController.m index 7a68527..44fcd97 100644 --- a/baRSS/NewsController.m +++ b/baRSS/NewsController.m @@ -70,4 +70,15 @@ NSLog(@"all unread"); } ++ (void)downloadFeed:(NSString*)url withBlock:(nullable void (^)(NSDictionary* result, NSError* error))block { + [NSThread detachNewThreadWithBlock:^{ + NSDictionary *dict = [PyHandler getFeed:url withEtag:nil andModified:nil]; + NSError *err = nil; + if (!dict || [dict[@"entries"] count] == 0 ) { + err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotParseResponse userInfo:nil]; + } + if (block) block(dict, err); + }]; +} + @end diff --git a/baRSS/Preferences/FeedConfig+Print.h b/baRSS/Preferences/FeedConfig+Print.h new file mode 100644 index 0000000..98ca3dd --- /dev/null +++ b/baRSS/Preferences/FeedConfig+Print.h @@ -0,0 +1,40 @@ +// +// The MIT License (MIT) +// Copyright (c) 2018 Oleg Geier +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef FeedConfig_Print_h +#define FeedConfig_Print_h + +@implementation FeedConfig (Print) +- (NSString*)readableRefreshString { + return [NSString stringWithFormat:@"%d%c", self.refreshNum, [@"smhdw" characterAtIndex:self.refreshUnit % 5]]; +} +- (NSString*)readableDescription { + switch (self.type) { + case 0: return [NSString stringWithFormat:@"%@", self.name]; // Group + case 2: return @"-------------"; // Separator + default: + return [NSString stringWithFormat:@"%@ (%@) - %@", self.name, self.url, [self readableRefreshString]]; + } +} +@end + +#endif /* FeedConfig_Print_h */ diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.h b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.h new file mode 100644 index 0000000..74076df --- /dev/null +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.h @@ -0,0 +1,35 @@ +// +// The MIT License (MIT) +// Copyright (c) 2018 Oleg Geier +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +@protocol ModalFeedConfigEdit +- (void)updateRepresentedObject; // must call [item.managedObjectContext refreshAllObjects] +@end + + +@interface ModalFeedEdit : NSViewController +@end + +@interface ModalGroupEdit : NSViewController +@end + diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m new file mode 100644 index 0000000..9d698db --- /dev/null +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m @@ -0,0 +1,218 @@ +// +// The MIT License (MIT) +// Copyright (c) 2018 Oleg Geier +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "ModalFeedEdit.h" +#import "NewsController.h" +#import "FeedConfig+CoreDataProperties.h" + +@interface ModalFeedEdit() +@property (weak) IBOutlet NSTextField *url; +@property (weak) IBOutlet NSTextField *name; +@property (weak) IBOutlet NSTextField *refreshNum; +@property (weak) IBOutlet NSPopUpButton *refreshUnit; +@property (weak) IBOutlet NSProgressIndicator *spinnerURL; +@property (weak) IBOutlet NSProgressIndicator *spinnerName; +@property (weak) IBOutlet NSButton *warningIndicator; +@property (weak) IBOutlet NSPopover *warningPopover; + +@property (copy) NSString *previousURL; +@property (strong) NSError *feedError; +@property (strong) NSDictionary *feedResult; + +@property (assign) BOOL shouldEvaluate; +@property (assign) BOOL lateEvaluation; +@end + +@implementation ModalFeedEdit + +- (void)viewDidLoad { + [super viewDidLoad]; + self.previousURL = @""; + self.refreshNum.intValue = 30; + self.shouldEvaluate = NO; + + 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; + } +} + +- (void)dealloc { + FeedConfig *item = [self feedConfigOrNil]; + if (self.shouldEvaluate && self.lateEvaluation && item) { + if (!item.name || [item.name isEqualToString:@""]) { + [self setTitleFromFeed]; + if (![item.name isEqualToString: self.name.stringValue]) // only if result isnt empty as well + item.name = self.name.stringValue; + [item.managedObjectContext refreshAllObjects]; + } + + } +} + +- (void)updateRepresentedObject { + FeedConfig *item = [self feedConfigOrNil]; + if (item) { + // 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; + + self.shouldEvaluate = YES; + self.lateEvaluation = NO; + // TODO: append feed result + NSLog(@"here i want to set it"); + [item.managedObjectContext refreshAllObjects]; + } +} + +- (FeedConfig*)feedConfigOrNil { + if ([self.representedObject isKindOfClass:[FeedConfig class]]) + return self.representedObject; + return nil; +} + +- (BOOL)urlHasChanged { + return ![self.previousURL isEqualToString:self.url.stringValue]; +} + +- (void)controlTextDidChange:(NSNotification *)obj { + if (obj.object == self.url) { + self.warningIndicator.hidden = (!self.feedError || [self urlHasChanged]); + } +} + +- (void)controlTextDidEndEditing:(NSNotification *)obj { + if (obj.object == self.url && [self urlHasChanged]) { + self.previousURL = self.url.stringValue; + self.feedResult = nil; + NSLog(@"setting result to nil"); + self.feedError = nil; + [self.spinnerURL startAnimation:nil]; + [self.spinnerName startAnimation:nil]; + [NewsController downloadFeed:self.previousURL withBlock:^(NSDictionary *result, NSError *error) { + self.feedResult = result; + NSLog(@"got results back"); + self.feedError = error; // warning indicator .hidden is bound to feedError + // TODO: play error sound? + dispatch_async(dispatch_get_main_queue(), ^{ + self.lateEvaluation = YES; // stays YES if this block runs after updateRepresentedObject: + [self setTitleFromFeed]; + [self.spinnerURL stopAnimation:nil]; + [self.spinnerName stopAnimation:nil]; + }); + }]; + } + // http://feeds.feedburner.com/simpledesktops +} + +- (void)setTitleFromFeed { + if ([self.name.stringValue isEqualToString:@""]) { + self.name.objectValue = self.feedResult[@"feed"][@"title"]; + } +} + +- (IBAction)didClickWarningButton:(NSButton*)sender { + if (!self.feedError) + return; + + NSString *str = self.feedError.localizedDescription; + NSTextField *tf = self.warningPopover.contentViewController.view.subviews.firstObject; + tf.maximumNumberOfLines = 7; + tf.objectValue = str; + + NSSize newSize = tf.fittingSize; // width is limited by the textfield's preferred width + newSize.width += 2 * tf.frame.origin.x; // the padding + newSize.height += 2 * tf.frame.origin.y; + + [self.warningPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSRectEdgeMinY]; + [self.warningPopover setContentSize:newSize]; +} + +@end + + +#pragma mark - ModalGroupEdit + +@implementation ModalGroupEdit +- (void)viewDidLoad { + [super viewDidLoad]; + if ([self.representedObject isKindOfClass:[FeedConfig class]]) { + FeedConfig *fc = self.representedObject; + ((NSTextField*)self.view).objectValue = fc.name; + } +} +- (void)loadView { + NSTextField *tf = [NSTextField textFieldWithString:@"New Group"]; + tf.placeholderString = @"New Group"; + 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 refreshAllObjects]; + } + } +} +@end + + +#pragma mark - StrictUIntFormatter + + +@interface StrictUIntFormatter : NSFormatter +@end + +@implementation StrictUIntFormatter +- (NSString *)stringForObjectValue:(id)obj { + return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]]; +} +- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error { + *obj = [[NSNumber numberWithInt:[string intValue]] stringValue]; + return YES; +} +- (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]; + if (c < '0' || c > '9') + return NO; + } + return YES; +} +@end diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.xib b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.xib new file mode 100644 index 0000000..5ecce5a --- /dev/null +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.xib @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Couldn't load Feed +An additional line +and a third + + + + + + + + + + + + diff --git a/baRSS/Preferences/Settings Tabs/SettingsFeeds.h b/baRSS/Preferences/Feeds Tab/SettingsFeeds.h similarity index 100% rename from baRSS/Preferences/Settings Tabs/SettingsFeeds.h rename to baRSS/Preferences/Feeds Tab/SettingsFeeds.h diff --git a/baRSS/Preferences/Settings Tabs/SettingsFeeds.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m similarity index 75% rename from baRSS/Preferences/Settings Tabs/SettingsFeeds.m rename to baRSS/Preferences/Feeds Tab/SettingsFeeds.m index bf22f3c..7a3ccd1 100644 --- a/baRSS/Preferences/Settings Tabs/SettingsFeeds.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m @@ -21,17 +21,18 @@ // SOFTWARE. #import "SettingsFeeds.h" -#import "DBv1+CoreDataModel.h" -#import "ModalSheet.h" -#import "DrawImage.h" #import "AppDelegate.h" +#import "DBv1+CoreDataModel.h" +#import "FeedConfig+Print.h" +#import "ModalSheet.h" +#import "ModalFeedEdit.h" +#import "DrawImage.h" @interface SettingsFeeds () -@property (weak) IBOutlet ModalFeedEdit *viewModalEditFeed; -@property (weak) IBOutlet ModalGroupEdit *viewModalEditGroup; @property (weak) IBOutlet NSOutlineView *outlineView; @property (weak) IBOutlet NSTreeController *dataStore; +@property (strong) NSViewController *modalController; @property (strong) NSArray *currentlyDraggedNodes; @property (strong) NSUndoManager *undoManager; @end @@ -89,40 +90,25 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; [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]]; +- (void)showModalForFeedConfig:(FeedConfig*)obj isGroupEdit:(BOOL)group { + BOOL existingItem = [obj isKindOfClass:[FeedConfig class]]; if (existingItem) { if (obj.type == 2) return; // Separator group = (obj.type == 0); - if (group) [self.viewModalEditGroup setGroupName:obj.name]; - else [self.viewModalEditFeed setURL:obj.url name:obj.name refreshNum:obj.refreshNum unit:obj.refreshUnit]; - } else { - if (group) [self.viewModalEditGroup setDefaultValues]; - else [self.viewModalEditFeed setDefaultValues]; } - NSView *content = (group ? self.viewModalEditGroup : self.viewModalEditFeed); - [self.view.window beginSheet:[ModalSheet modalWithView:content] completionHandler:^(NSModalResponse returnCode) { + self.modalController = (group ? [ModalGroupEdit new] : [ModalFeedEdit new]); + self.modalController.representedObject = obj; + + [self.view.window beginSheet:[ModalSheet modalWithView:self.modalController.view] completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSModalResponseOK) { - FeedConfig *item = obj; if (!existingItem) { // create new item - item = [self insertSortedItemAtSelection]; + FeedConfig *item = [self insertSortedItemAtSelection]; item.type = (group ? 0 : 1); + self.modalController.representedObject = item; } - if (group) { - if (![item.name isEqualToString: self.viewModalEditGroup.title.stringValue]) - item.name = self.viewModalEditGroup.title.stringValue; - } else { - if (![item.name isEqualToString: self.viewModalEditFeed.title.stringValue]) - item.name = self.viewModalEditFeed.title.stringValue; - if (![item.url isEqualToString:self.viewModalEditFeed.url.stringValue]) - item.url = self.viewModalEditFeed.url.stringValue; - if (item.refreshNum != self.viewModalEditFeed.refreshNum.intValue) - item.refreshNum = self.viewModalEditFeed.refreshNum.intValue; - if (item.refreshUnit != self.viewModalEditFeed.refreshUnit.indexOfSelectedItem) - item.refreshUnit = (int16_t)self.viewModalEditFeed.refreshUnit.indexOfSelectedItem; - } - [self.dataStore rearrangeObjects]; + [self.modalController updateRepresentedObject]; } + self.modalController = nil; }]; } @@ -132,7 +118,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; FeedConfig *selected = [[[self.dataStore arrangedObjects] descendantNodeAtIndexPath:selectedIndex] representedObject]; NSUInteger lastIndex = selected.children.count; - bool groupSelected = (selected.type == 0); + BOOL groupSelected = (selected.type == 0); if (!groupSelected) { lastIndex = (NSUInteger)selected.sortIndex + 1; // insert after selection @@ -191,7 +177,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; if (!item || !dstChildren) dstChildren = [self.dataStore arrangedObjects].childNodes; - bool isFolderDrag = (index == -1); + BOOL isFolderDrag = (index == -1); NSUInteger insertIndex = (isFolderDrag ? dstChildren.count : (NSUInteger)index); // index where the items will be moved to, but not final since items above can vanish NSIndexPath *dest = [item indexPath]; @@ -250,16 +236,16 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { FeedConfig *f = [(NSTreeNode*)item representedObject]; - bool isFeed = (f.type == 1); - bool isSeperator = (f.type == 2); - bool isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"]; + BOOL isFeed = (f.type == 1); + BOOL isSeperator = (f.type == 2); + BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"]; NSString *cellIdent = (isRefreshColumn ? @"cellRefresh" : (isSeperator ? @"cellSeparator" : @"cellFeed")); // owner is nil to prohibit repeated awakeFromNib calls NSTableCellView *cellView = [self.outlineView makeViewWithIdentifier:cellIdent owner:nil]; if (isRefreshColumn) { - cellView.textField.stringValue = (!isFeed ? @"" : [ModalFeedEdit stringForRefreshNum:f.refreshNum unit:f.refreshUnit]); + cellView.textField.stringValue = (!isFeed ? @"" : [f readableRefreshString]); } else if (isSeperator) { return cellView; // the refresh cell is already skipped with the above if condition } else { @@ -285,13 +271,17 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; - (BOOL)respondsToSelector:(SEL)aSelector { - if (aSelector == @selector(enterPressed:) || aSelector == @selector(copy:)) { - bool outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]]; - return outlineHasFocus && (self.dataStore.selectedNodes.count > 0); - } else if (aSelector == @selector(undo:)) { - return [self.undoManager canUndo]; - } else if (aSelector == @selector(redo:)) { - return [self.undoManager canRedo]; + if (aSelector == @selector(undo:)) return [self.undoManager canUndo]; + if (aSelector == @selector(redo:)) return [self.undoManager canRedo]; + if (aSelector == @selector(copy:) || aSelector == @selector(enterPressed:)) { + BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]]; + BOOL hasSelection = (self.dataStore.selectedNodes.count > 0); + if (!outlineHasFocus || !hasSelection) + return NO; + if (aSelector == @selector(copy:)) + return YES; + // can edit only if selection is not a separator + return (((FeedConfig*)self.dataStore.selectedNodes.firstObject.representedObject).type != 2); } return [super respondsToSelector:aSelector]; } @@ -312,32 +302,37 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; - (void)copy:(id)sender { NSMutableString *str = [[NSMutableString alloc] init]; - NSMutableArray *items = [NSMutableArray arrayWithArray:self.dataStore.selectedObjects]; - while (items.count > 0) { - [self traverseChildren:items[0] appendString:str indentation:0 onSelection:items]; + NSUInteger count = self.dataStore.selectedNodes.count; + NSMutableArray *groups = [NSMutableArray arrayWithCapacity:count]; + + // filter out nodes that are already present in some selected parent node + for (NSTreeNode *node in self.dataStore.selectedNodes) { + BOOL skipItem = NO; + for (NSTreeNode *stored in groups) { + NSIndexPath *p = node.indexPath; + while (p.length > stored.indexPath.length) + p = [p indexPathByRemovingLastIndex]; + if ([p isEqualTo:stored.indexPath]) { + skipItem = YES; + break; + } + } + if (!skipItem) { + [self traverseChildren:node appendString:str prefix:@""]; + if (node.childNodes.count > 0) + [groups addObject:node]; + } } [[NSPasteboard generalPasteboard] clearContents]; [[NSPasteboard generalPasteboard] setString:str forType:NSPasteboardTypeString]; NSLog(@"%@", str); } -- (void)traverseChildren:(FeedConfig*)obj appendString:(NSMutableString*)str indentation:(int)indent onSelection:(NSMutableArray*)arr { - for (NSUInteger i = 0; i < arr.count; i++) { - if (obj == arr[i]) { - [arr removeObjectAtIndex:i]; - break; - } - } - for (int i = indent; i > 0; i--) { - [str appendString:@" "]; - } - switch (obj.type) { - case 0: [str appendFormat:@"%@:\n", obj.name]; break; // Group - case 2: [str appendString:@"-------------\n"]; break; // Separator - default: [str appendFormat:@"%@ (%@) - %@\n", obj.name, obj.url, [ModalFeedEdit stringForRefreshNum:obj.refreshNum unit:obj.refreshUnit]]; - } - for (FeedConfig *child in obj.children) { - [self traverseChildren:child appendString:str indentation:indent + 1 onSelection:arr]; +- (void)traverseChildren:(NSTreeNode*)obj appendString:(NSMutableString*)str prefix:(NSString*)prefix { + [str appendFormat:@"%@%@\n", prefix, [obj.representedObject readableDescription]]; + prefix = [prefix stringByAppendingString:@" "]; + for (NSTreeNode *child in obj.childNodes) { + [self traverseChildren:child appendString:str prefix:prefix]; } } diff --git a/baRSS/Preferences/Settings Tabs/SettingsFeeds.xib b/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib similarity index 67% rename from baRSS/Preferences/Settings Tabs/SettingsFeeds.xib rename to baRSS/Preferences/Feeds Tab/SettingsFeeds.xib index d24ceea..5b354a4 100644 --- a/baRSS/Preferences/Settings Tabs/SettingsFeeds.xib +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib @@ -11,8 +11,6 @@ - - @@ -221,120 +219,7 @@ CA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/baRSS/Preferences/Settings Tabs/SettingsGeneral.h b/baRSS/Preferences/General Tab/SettingsGeneral.h similarity index 100% rename from baRSS/Preferences/Settings Tabs/SettingsGeneral.h rename to baRSS/Preferences/General Tab/SettingsGeneral.h diff --git a/baRSS/Preferences/Settings Tabs/SettingsGeneral.m b/baRSS/Preferences/General Tab/SettingsGeneral.m similarity index 100% rename from baRSS/Preferences/Settings Tabs/SettingsGeneral.m rename to baRSS/Preferences/General Tab/SettingsGeneral.m diff --git a/baRSS/Preferences/Settings Tabs/SettingsGeneral.xib b/baRSS/Preferences/General Tab/SettingsGeneral.xib similarity index 99% rename from baRSS/Preferences/Settings Tabs/SettingsGeneral.xib rename to baRSS/Preferences/General Tab/SettingsGeneral.xib index 86d0a63..01c1307 100644 --- a/baRSS/Preferences/Settings Tabs/SettingsGeneral.xib +++ b/baRSS/Preferences/General Tab/SettingsGeneral.xib @@ -219,7 +219,7 @@ - + diff --git a/baRSS/Preferences/ModalSheet.h b/baRSS/Preferences/ModalSheet.h index e91403c..2c9aac1 100644 --- a/baRSS/Preferences/ModalSheet.h +++ b/baRSS/Preferences/ModalSheet.h @@ -24,27 +24,5 @@ @interface ModalSheet : NSPanel + (instancetype)modalWithView:(NSView*)content; +- (void)setDoneEnabled:(BOOL)accept; @end - - -@interface ModalFeedEdit : NSView -@property (weak) IBOutlet NSTextField *url; -@property (weak) IBOutlet NSTextField *title; -@property (weak) IBOutlet NSTextField *refreshNum; -@property (weak) IBOutlet NSPopUpButton *refreshUnit; -- (void)setDefaultValues; -- (void)setURL:(NSString*)url name:(NSString*)name refreshNum:(int32_t)num unit:(int16_t)unit; -+ (NSString*)stringForRefreshNum:(int32_t)num unit:(int16_t)unit; -@end - - -@interface ModalGroupEdit : NSView -@property (weak) IBOutlet NSTextField *title; -- (void)setDefaultValues; -- (void)setGroupName:(NSString*)name; -@end - - -@interface StrictUIntFormatter : NSFormatter -@end - diff --git a/baRSS/Preferences/ModalSheet.m b/baRSS/Preferences/ModalSheet.m index aa30050..0622077 100644 --- a/baRSS/Preferences/ModalSheet.m +++ b/baRSS/Preferences/ModalSheet.m @@ -22,15 +22,15 @@ #import "ModalSheet.h" -#define BETWEEN(x,min,max) (x < min ? min : x > max ? max : x) - - -#pragma mark - ModalSheet +@interface ModalSheet() +@property (strong) NSButton *btnDone; +@end @implementation ModalSheet - (void)didTapDoneButton:(id)sender { [self closeWithResponse:NSModalResponseOK]; } - (void)didTapCancelButton:(id)sender { [self closeWithResponse:NSModalResponseAbort]; } +- (void)setDoneEnabled:(BOOL)accept { self.btnDone.enabled = accept; } - (void)closeWithResponse:(NSModalResponse)response { // store modal view width and remove subviews to avoid _NSKeyboardFocusClipView issues @@ -46,27 +46,30 @@ static const int padButtons = 12; static const int minWidth = 320; static const int maxWidth = 1200; - NSInteger prevWidth = [[NSUserDefaults standardUserDefaults] integerForKey:@"modalSheetWidth"]; - NSRect cFrame = NSMakeRect(padWindow, padWindow, BETWEEN(prevWidth, minWidth, maxWidth), content.frame.size.height); + NSInteger prevWidth = [[NSUserDefaults standardUserDefaults] integerForKey:@"modalSheetWidth"]; + if (prevWidth < minWidth) prevWidth = minWidth; + else if (prevWidth > maxWidth) prevWidth = maxWidth; + + NSRect cFrame = NSMakeRect(padWindow, padWindow, prevWidth, content.frame.size.height); NSRect wFrame = CGRectInset(cFrame, -padWindow, -padWindow); NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView; ModalSheet *sheet = [[super alloc] initWithContentRect:wFrame styleMask:style backing:NSBackingStoreBuffered defer:NO]; // Respond buttons - NSButton *btnDone = [NSButton buttonWithTitle:NSLocalizedString(@"Done", nil) target:sheet action:@selector(didTapDoneButton:)]; - btnDone.keyEquivalent = @"\r"; // Enter / Return - btnDone.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; + sheet.btnDone = [NSButton buttonWithTitle:NSLocalizedString(@"Done", nil) target:sheet action:@selector(didTapDoneButton:)]; + sheet.btnDone.keyEquivalent = @"\r"; // Enter / Return + sheet.btnDone.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; NSButton *btnCancel = [NSButton buttonWithTitle:NSLocalizedString(@"Cancel", nil) target:sheet action:@selector(didTapCancelButton:)]; btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC btnCancel.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin; - NSRect align = [btnDone alignmentRectForFrame:btnDone.frame]; + NSRect align = [sheet.btnDone alignmentRectForFrame:sheet.btnDone.frame]; align.origin.x = wFrame.size.width - align.size.width - padWindow; align.origin.y = padWindow; - [btnDone setFrameOrigin:[btnDone frameForAlignmentRect:align].origin]; + [sheet.btnDone setFrameOrigin:[sheet.btnDone frameForAlignmentRect:align].origin]; align.origin.x -= [btnCancel alignmentRectForFrame:btnCancel.frame].size.width + padButtons; [btnCancel setFrameOrigin:[btnCancel frameForAlignmentRect:align].origin]; @@ -78,7 +81,7 @@ // add all UI elements to the window view content.frame = cFrame; [sheet.contentView addSubview:content]; - [sheet.contentView addSubview:btnDone]; + [sheet.contentView addSubview:sheet.btnDone]; [sheet.contentView addSubview:btnCancel]; // add respond buttons to the window height @@ -93,58 +96,3 @@ @end -#pragma mark - ModalFeedEdit - - -@implementation ModalFeedEdit -- (void)setDefaultValues { - self.url.stringValue = @""; - self.title.stringValue = @""; - self.refreshNum.intValue = 30; - [self.refreshUnit selectItemAtIndex:1]; -} -- (void)setURL:(NSString*)url name:(NSString*)name refreshNum:(int32_t)num unit:(int16_t)unit { - self.url.objectValue = url; - self.title.objectValue = name; - self.refreshNum.intValue = num; - [self.refreshUnit selectItemAtIndex:BETWEEN(unit, 0, self.refreshUnit.numberOfItems - 1)]; -} -+ (NSString*)stringForRefreshNum:(int32_t)num unit:(int16_t)unit { - return [NSString stringWithFormat:@"%d%c", num, [@"smhdw" characterAtIndex:(NSUInteger)BETWEEN(unit, 0, 4)]]; -} -@end - - -#pragma mark - ModalGroupEdit - - -@implementation ModalGroupEdit -- (void)setDefaultValues { - self.title.stringValue = @"New Group"; -} -- (void)setGroupName:(NSString*)name { - self.title.objectValue = name; -} -@end - - -#pragma mark - StrictUIntFormatter - - -@implementation StrictUIntFormatter -- (NSString *)stringForObjectValue:(id)obj { - return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]]; -} -- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error { - *obj = [[NSNumber numberWithInt:[string intValue]] stringValue]; - return YES; -} -- (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]; - if (c < '0' || c > '9') - return NO; - } - return YES; -} -@end