diff --git a/CHANGELOG.md b/CHANGELOG.md index 5755fe8..c4c13d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2 - *Settings, Feeds:* Drag & Drop feed titles and urls as text - *Settings, Feeds:* OPML export with selected items only - *UI:* Accessibility hints for most UI elements +- *UI*: Show welcome message upon first usage (empty db) +- *DB*: Table for options. E.g., with what version was the db last used - Associate OPML files (double click and right click actions in Finder) - Quick Look preview for OPML files diff --git a/baRSS/AppHook.m b/baRSS/AppHook.m index a7afe7f..82aff9f 100644 --- a/baRSS/AppHook.m +++ b/baRSS/AppHook.m @@ -27,6 +27,7 @@ #import "DrawImage.h" #import "SettingsFeeds+DragDrop.h" #import "UserPrefs.h" +#import "StoreCoordinator.h" @interface AppHook() @property (strong) NSWindowController *prefWindow; @@ -41,17 +42,20 @@ } - (void)applicationWillFinishLaunching:(NSNotification *)notification { - [self migrateVersionUpdate]; + RegisterImageViewNames(); _statusItem = [BarStatusItem new]; NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; + [self migrateVersionUpdate]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - RegisterImageViewNames(); -// feed://https://feeds.feedburner.com/simpledesktops + if ([StoreCoordinator isEmpty]) { + [_statusItem showWelcomeMessage]; + } [FeedDownload registerNetworkChangeNotification]; // will call update scheduler + [_statusItem asyncReloadUnreadCount]; } - (void)applicationWillTerminate:(NSNotification *)aNotification { @@ -59,6 +63,7 @@ } - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { +// feed://https://feeds.feedburner.com/simpledesktops NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSString *scheme = [[[NSURL URLWithString:url] scheme] lowercaseString]; url = [url substringFromIndex:scheme.length + 1]; // + ':' diff --git a/baRSS/Core Data/StoreCoordinator.h b/baRSS/Core Data/StoreCoordinator.h index b173911..9897487 100644 --- a/baRSS/Core Data/StoreCoordinator.h +++ b/baRSS/Core Data/StoreCoordinator.h @@ -39,6 +39,7 @@ static const int dbFileVersion = 1; // update in case database structure changes + (NSArray*)getListOfFeedsThatNeedUpdate:(BOOL)forceAll inContext:(NSManagedObjectContext*)moc; // Count elements ++ (BOOL)isEmpty; + (NSUInteger)countTotalUnread; + (NSUInteger)countRootItemsInContext:(NSManagedObjectContext*)moc; + (NSArray*)countAggregatedUnread; diff --git a/baRSS/Core Data/StoreCoordinator.m b/baRSS/Core Data/StoreCoordinator.m index efa7ab6..135d1de 100644 --- a/baRSS/Core Data/StoreCoordinator.m +++ b/baRSS/Core Data/StoreCoordinator.m @@ -112,6 +112,11 @@ #pragma mark - Count Elements +/// @return @c YES if core data has no stored @c FeedGroup ++ (BOOL)isEmpty { + return [[FeedGroup fetchRequest] fetchFirst:[self getMainContext]] == nil; +} + /// @return Sum of all unread @c FeedArticle items. + (NSUInteger)countTotalUnread { return [[[FeedArticle fetchRequest] where:@"unread = YES"] fetchCount: [self getMainContext]]; diff --git a/baRSS/Helper/NSView+Ext.h b/baRSS/Helper/NSView+Ext.h index c150548..b32212b 100644 --- a/baRSS/Helper/NSView+Ext.h +++ b/baRSS/Helper/NSView+Ext.h @@ -42,6 +42,10 @@ static const CGFloat CENTER = -0.015625; /// Calculate @c origin.y going down from the top border of its @c superview NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; } +/// @c MAX() +NS_INLINE CGFloat Max(CGFloat a, CGFloat b) { return a < b ? b : a; } +/// @c Max(NSWidth(a.frame),NSWidth(b.frame)) +NS_INLINE CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.frame), NSWidth(b.frame)); } /* @@ -66,6 +70,7 @@ NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) + (NSView*)radioGroup:(NSArray*)entries target:(id)target action:(nonnull SEL)action; + (NSView*)radioGroup:(NSArray*)entries; // UI: Enclosing Container ++ (NSPopover*)popover:(NSSize)size; - (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect; + (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad; // Insert UI elements in parent view @@ -98,4 +103,5 @@ NS_INLINE CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) @interface NSTextField (Ext) - (instancetype)gray; - (instancetype)selectable; +- (instancetype)multiline:(NSSize)size; @end diff --git a/baRSS/Helper/NSView+Ext.m b/baRSS/Helper/NSView+Ext.m index 80f72b6..41acbca 100644 --- a/baRSS/Helper/NSView+Ext.m +++ b/baRSS/Helper/NSView+Ext.m @@ -56,8 +56,7 @@ NSView *parent = [[NSView alloc] init]; for (NSUInteger i = 0; i < labels.count; i++) { NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off]; - if (w < NSWidth(lbl.frame)) - w = NSWidth(lbl.frame); + w = Max(w, NSWidth(lbl.frame)); y += h + pad; } [parent setFrameSize: NSMakeSize(w, y - pad)]; @@ -151,8 +150,7 @@ btn.tag = (NSInteger)i-1; if (btn.tag == 0) btn.state = NSControlStateValueOn; - if (w < NSWidth(btn.frame)) // find max width (before alignmentRect:) - w = NSWidth(btn.frame); + w = Max(w, NSWidth(btn.frame)); // find max width (before alignmentRect:) [btn placeIn:parent x:0 y:h]; h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS; } @@ -172,6 +170,15 @@ #pragma mark - UI: Enclosing Container - +/// Create transient popover with initial view controller and view @c size ++ (NSPopover*)popover:(NSSize)size { + NSPopover *pop = [[NSPopover alloc] init]; + pop.behavior = NSPopoverBehaviorTransient; + pop.contentViewController = [[NSViewController alloc] init]; + pop.contentViewController.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, size.width, size.height)]; + return pop; +} + /// Insert @c scrollView, remove @c self from current view and set as @c documentView for the newly created scroll view. - (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect { NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight]; @@ -352,4 +359,14 @@ NS_INLINE void SetFontAndResize(NSControl *control, NSFont *font) { /// Set @c .selectable to @c YES - (instancetype)selectable { self.selectable = YES; return self; } +/// Set @c .maximumNumberOfLines @c = @c 7 and @c preferredMaxLayoutWidth. +- (instancetype)multiline:(NSSize)size { + [self setFrameSize:size]; + self.preferredMaxLayoutWidth = size.width; + self.lineBreakMode = NSLineBreakByWordWrapping; + self.usesSingleLineMode = NO; + self.maximumNumberOfLines = 7; // used in ModalFeedEditView + return self; +} + @end diff --git a/baRSS/Info.plist b/baRSS/Info.plist index 3bb63b4..d5b784e 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -60,7 +60,7 @@ CFBundleVersion - 9923 + 10141 LSApplicationCategoryType public.app-category.news LSMinimumSystemVersion diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m index 4b7e258..6cd295d 100644 --- a/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEdit.m @@ -338,7 +338,7 @@ // apply fitting size and display self.view.warningPopover.contentSize = newSize; - [self.view.warningPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSRectEdgeMinY]; + [self.view.warningPopover showRelativeToRect:NSZeroRect ofView:sender preferredEdge:NSRectEdgeMinY]; } /// Either hit by Cmd+R or reload button inside warning popover error description diff --git a/baRSS/Preferences/Feeds Tab/ModalFeedEditView.m b/baRSS/Preferences/Feeds Tab/ModalFeedEditView.m index 5bb1563..5fb8171 100644 --- a/baRSS/Preferences/Feeds Tab/ModalFeedEditView.m +++ b/baRSS/Preferences/Feeds Tab/ModalFeedEditView.m @@ -70,23 +70,11 @@ /// Prepare popover controller to display errors during download - (void)prepareWarningPopover { - NSPopover *pop = [[NSPopover alloc] init]; - pop.behavior = NSPopoverBehaviorTransient; - pop.contentViewController = [[NSViewController alloc] init]; - - NSView *content = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 300, 100)]; - pop.contentViewController.view = content; - + self.warningPopover = [NSView popover: NSMakeSize(300, 100)]; + NSView *content = self.warningPopover.contentViewController.view; // User visible error description text (after click on warning button) - NSTextField *txt = [[[NSView label:@""] selectable] sizableWidthAndHeight]; - txt.frame = NSInsetRect(content.frame, 4, 2); - txt.preferredMaxLayoutWidth = NSWidth(txt.frame); - txt.lineBreakMode = NSLineBreakByWordWrapping; - txt.maximumNumberOfLines = 7; - [content addSubview:txt]; - - self.warningPopover = pop; - self.warningText = txt; + self.warningText = [[[[[NSView label:@""] selectable] sizableWidthAndHeight] + multiline:NSMakeSize(292, 96)] placeIn:content x:4 y:2]; // Reload button is only visible on 5xx server error (right of ––––) self.warningReload = [[[[NSView buttonIcon:NSImageNameRefreshTemplate size:16] placeIn:content x:35 yTop:21] tooltip:NSLocalizedString(@"Retry download (⌘R)", nil)] diff --git a/baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m b/baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m index 18d244a..26ca608 100644 --- a/baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m +++ b/baRSS/Preferences/Feeds Tab/RefreshStatisticsView.m @@ -50,9 +50,7 @@ GrayLabel(NSLocalizedString(@"median:", nil)), [self createInlineButton:info[@"median"] callback:callback]]; NSView *buttonsView = [self placeViewsHorizontally:arr]; - CGFloat w = NSWidth(buttonsView.frame); - if (w < NSWidth(dateView.frame)) - w = NSWidth(dateView.frame); + CGFloat w = NSMaxWidth(dateView, buttonsView); [self setFrameSize:NSMakeSize(w, NSHeight(buttonsView.frame) + PAD_M + NSHeight(dateView.frame))]; [dateView placeIn:self x:CENTER yTop:0]; diff --git a/baRSS/Status Bar Menu/BarStatusItem.h b/baRSS/Status Bar Menu/BarStatusItem.h index 15d63ec..39b6973 100644 --- a/baRSS/Status Bar Menu/BarStatusItem.h +++ b/baRSS/Status Bar Menu/BarStatusItem.h @@ -29,5 +29,6 @@ - (void)setUnreadCountRelative:(NSInteger)count; - (void)asyncReloadUnreadCount; - (void)updateBarIcon; +- (void)showWelcomeMessage; @end diff --git a/baRSS/Status Bar Menu/BarStatusItem.m b/baRSS/Status Bar Menu/BarStatusItem.m index 565f18f..a9a03da 100644 --- a/baRSS/Status Bar Menu/BarStatusItem.m +++ b/baRSS/Status Bar Menu/BarStatusItem.m @@ -27,6 +27,7 @@ #import "UserPrefs.h" #import "BarMenu.h" #import "AppHook.h" +#import "NSView+Ext.h" @interface BarStatusItem() @property (strong) BarMenu *barMenu; @@ -45,8 +46,8 @@ self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength]; self.statusItem.highlightMode = YES; self.unreadCountTotal = 0; - [self updateBarIcon]; - [self asyncReloadUnreadCount]; + self.statusItem.image = [NSImage imageNamed:RSSImageMenuBarIconActive]; + self.statusItem.image.template = YES; // Add empty menu (will be populated once opened) self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainMenuWillOpen) name:NSMenuDidBeginTrackingNotification object:self.statusItem.menu]; @@ -126,6 +127,26 @@ }); } +/// Show popover with a brief notice that baRSS is running in the menu bar +- (void)showWelcomeMessage { + NSString *title = [NSString stringWithFormat:NSLocalizedString(@"Welcome to %@", nil), [UserPrefs appName]]; + NSString *message = NSLocalizedString(@"There's no application window.\nEverything is up there.", nil); + NSTextField *head = [[NSView label:title] bold]; + NSTextField *body = [[NSView label:message] small]; + + const CGFloat pad = 12; + CGFloat icon = NSHeight(head.frame) + PAD_S + NSHeight(body.frame); + CGFloat dx = pad + icon + PAD_L; // where text begins + + NSPopover *pop = [NSView popover:NSMakeSize(dx + NSMaxWidth(head, body) + pad, icon + 2 * pad)]; + NSView *content = pop.contentViewController.view; + + [[NSView imageView:NSImageNameApplicationIcon size:icon] placeIn:content x:pad y:pad]; + [head placeIn:content x:dx yTop:pad]; + [body placeIn:content x:dx y:pad]; + [pop showRelativeToRect:NSZeroRect ofView:self.statusItem.button preferredEdge:NSRectEdgeMaxY]; +} + #pragma mark - Main Menu Handling