Feed Statistics View
- Bugfix: group unread count fetch & undo / redo operations - ModalSheet refactored
This commit is contained in:
@@ -26,6 +26,8 @@
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedMeta+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "Statistics.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
|
||||
#pragma mark - ModalEditDialog -
|
||||
@@ -46,7 +48,7 @@
|
||||
/// @return New @c ModalSheet with its subclass @c .view property as dialog content.
|
||||
- (ModalSheet *)getModalSheet {
|
||||
if (!self.modalSheet)
|
||||
self.modalSheet = [ModalSheet modalWithView:self.view];
|
||||
self.modalSheet = [[ModalSheet alloc] initWithView:self.view];
|
||||
return self.modalSheet;
|
||||
}
|
||||
/// This method should be overridden by subclasses. Used to save changes to persistent store.
|
||||
@@ -60,7 +62,7 @@
|
||||
#pragma mark - ModalFeedEdit -
|
||||
|
||||
|
||||
@interface ModalFeedEdit()
|
||||
@interface ModalFeedEdit() <RefreshIntervalButtonDelegate>
|
||||
@property (weak) IBOutlet NSTextField *url;
|
||||
@property (weak) IBOutlet NSTextField *name;
|
||||
@property (weak) IBOutlet NSTextField *refreshNum;
|
||||
@@ -69,6 +71,7 @@
|
||||
@property (weak) IBOutlet NSProgressIndicator *spinnerName;
|
||||
@property (weak) IBOutlet NSButton *warningIndicator;
|
||||
@property (weak) IBOutlet NSPopover *warningPopover;
|
||||
@property (strong) NSView *statisticsView;
|
||||
|
||||
@property (copy) NSString *previousURL; // check if changed and avoid multiple download
|
||||
@property (copy) NSString *httpDate;
|
||||
@@ -105,6 +108,7 @@
|
||||
unit = self.refreshUnit.numberOfItems - 1;
|
||||
[self.refreshUnit selectItemAtIndex:unit];
|
||||
self.warningIndicator.image = [fg.feed iconImage16];
|
||||
[self statsForCoreDataObject];
|
||||
}
|
||||
|
||||
#pragma mark - Edit Feed Data
|
||||
@@ -189,6 +193,7 @@
|
||||
NSPoint belowURL = NSMakePoint(0,self.url.frame.size.height);
|
||||
if ([menu popUpMenuPositioningItem:nil atLocation:belowURL inView:self.url]) {
|
||||
NSInteger idx = [menu indexOfItem:menu.highlightedItem];
|
||||
if (idx < 0) idx = 0; // User hit enter without selection. Assume first item, because PopUpMenu did return YES!
|
||||
return [list objectAtIndex:(NSUInteger)idx].link;
|
||||
}
|
||||
return nil; // user selection canceled
|
||||
@@ -214,6 +219,8 @@
|
||||
if (parsedTitle.length > 0 && [self.name.stringValue isEqualToString:@""]) {
|
||||
self.name.stringValue = parsedTitle; // no damage to replace an empty string
|
||||
}
|
||||
// TODO: user preference to automatically select refresh interval (selection: None,min,max,avg,median)
|
||||
[self statsForDownloadObject];
|
||||
// 4. Continue with favicon download (or finish with error)
|
||||
if (self.feedError) {
|
||||
[self finishDownloadWithFavicon:[NSImage imageNamed:NSImageNameCaution]];
|
||||
@@ -243,6 +250,70 @@
|
||||
[self.modalSheet setDoneEnabled:YES];
|
||||
}
|
||||
|
||||
#pragma mark - Feed Statistics
|
||||
|
||||
/// Perform statistics on newly downloaded feed item
|
||||
- (void)statsForDownloadObject {
|
||||
NSMutableArray<NSDate*> *arr = [NSMutableArray arrayWithCapacity:self.feedResult.articles.count];
|
||||
for (RSParsedArticle *a in self.feedResult.articles) {
|
||||
NSDate *d = a.datePublished;
|
||||
if (!d) d = a.dateModified;
|
||||
if (!d) continue;
|
||||
[arr addObject:d];
|
||||
}
|
||||
[self appendViewWithFeedStatistics:arr count:self.feedResult.articles.count];
|
||||
}
|
||||
|
||||
/// Perform statistics on stored core data object
|
||||
- (void)statsForCoreDataObject {
|
||||
NSArray<FeedArticle*> *articles = [self.feedGroup.feed sortedArticles];
|
||||
[self appendViewWithFeedStatistics:[articles valueForKeyPath:@"published"] count:articles.count];
|
||||
}
|
||||
|
||||
/// Generate statistics UI with buttons to quickly select refresh unit and duration.
|
||||
- (void)appendViewWithFeedStatistics:(NSArray*)dates count:(NSUInteger)count {
|
||||
static const CGFloat statsPadding = 15.f;
|
||||
CGFloat prevHeight = 0.f;
|
||||
if (self.statisticsView != nil) {
|
||||
prevHeight = self.statisticsView.frame.size.height + statsPadding;
|
||||
[self.statisticsView removeFromSuperview];
|
||||
self.statisticsView = nil;
|
||||
}
|
||||
NSDictionary *stats = [Statistics refreshInterval:dates];
|
||||
NSView *v = [Statistics viewForRefreshInterval:stats articleCount:count callback:self];
|
||||
[[self getModalSheet] extendContentViewBy:v.frame.size.height + statsPadding - prevHeight];
|
||||
[v setFrameOrigin:NSMakePoint(0.5f*(NSWidth(self.view.frame) - NSWidth(v.frame)), 0)];
|
||||
[self.view addSubview:v];
|
||||
self.statisticsView = v;
|
||||
}
|
||||
|
||||
/// Callback method for @c Statistics @c +viewForRefreshInterval:articleCount:callback:
|
||||
- (void)refreshIntervalButtonClicked:(NSButton *)sender {
|
||||
NSInteger num = (sender.tag >> 3);
|
||||
NSInteger unit = (sender.tag & 0x7);
|
||||
if (self.refreshNum.integerValue != num) {
|
||||
[self animateControlAttention:self.refreshNum];
|
||||
self.refreshNum.integerValue = num;
|
||||
}
|
||||
if (self.refreshUnit.indexOfSelectedItem != unit) {
|
||||
[self animateControlAttention:self.refreshUnit];
|
||||
[self.refreshUnit selectItemAtIndex:unit];
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to animate @c NSControl to draw user attention. View will be scalled up in a fraction of a second.
|
||||
- (void)animateControlAttention:(NSView*)control {
|
||||
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform"];
|
||||
CATransform3D tr = CATransform3DIdentity;
|
||||
tr = CATransform3DTranslate(tr, NSMidX(control.bounds), NSMidY(control.bounds), 0);
|
||||
tr = CATransform3DScale(tr, 1.1, 1.1, 1);
|
||||
tr = CATransform3DTranslate(tr, -NSMidX(control.bounds), -NSMidY(control.bounds), 0);
|
||||
scale.toValue = [NSValue valueWithCATransform3D:tr];
|
||||
scale.duration = 0.15f;
|
||||
scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
|
||||
[control.layer addAnimation:scale forKey:scale.keyPath];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSTextField Delegate
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="i0K-k8-GMU" userLabel="View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="79"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MOX-a1-Yda" userLabel="URL Label">
|
||||
<rect key="frame" x="-2" y="60" width="103" height="17"/>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
/// Display Save File Panel to select export destination. All feeds from core data will be exported.
|
||||
+ (void)showExportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc {
|
||||
NSSavePanel *sp = [NSSavePanel savePanel];
|
||||
sp.nameFieldStringValue = [NSString stringWithFormat:@"baRSS feeds %@", [self currentDayAsString]];
|
||||
sp.nameFieldStringValue = [NSString stringWithFormat:@"baRSS feeds %@", [self currentDayAsStringISO8601:NO]];
|
||||
sp.allowedFileTypes = @[@"opml"];
|
||||
sp.allowsOtherFileTypes = YES;
|
||||
NSView *radioView = [self radioGroupCreate:@[NSLocalizedString(@"Hierarchical", nil),
|
||||
@@ -68,6 +68,7 @@
|
||||
/// Handle import dialog and perform web requests (feed data & icon). Creates a single undo group.
|
||||
+ (void)showImportDialog:(NSWindow*)window withTreeController:(NSTreeController*)tree {
|
||||
NSManagedObjectContext *moc = tree.managedObjectContext;
|
||||
//[moc refreshAllObjects];
|
||||
[moc.undoManager beginUndoGrouping];
|
||||
[self showImportDialog:window withContext:moc success:^(NSArray<Feed *> *added) {
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
@@ -204,7 +205,9 @@
|
||||
@return Save this string to file.
|
||||
*/
|
||||
+ (NSString*)exportFeedsHierarchical:(BOOL)flag inContext:(NSManagedObjectContext*)moc {
|
||||
NSDictionary *info = @{@"dateCreated" : [NSDate date], @"ownerName" : @"baRSS", OPMLTitleKey : @"baRSS feeds"};
|
||||
NSDictionary *info = @{OPMLTitleKey : @"baRSS feeds",
|
||||
@"ownerName" : @"baRSS",
|
||||
@"dateCreated" : [self currentDayAsStringISO8601:YES]};
|
||||
RSOPMLItem *doc = [RSOPMLItem itemWithAttributes:info];
|
||||
@autoreleasepool {
|
||||
NSArray<FeedGroup*> *arr = [StoreCoordinator sortedListOfRootObjectsInContext:moc];
|
||||
@@ -246,10 +249,13 @@
|
||||
#pragma mark - Helper
|
||||
|
||||
|
||||
/// @return Date formatted as @c yyyy-MM-dd
|
||||
+ (NSString*)currentDayAsString {
|
||||
NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
|
||||
return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
|
||||
/// @param flag If @c YES use long internet format for opml file. If @c NO use short format as filename.
|
||||
+ (NSString*)currentDayAsStringISO8601:(BOOL)flag {
|
||||
if (flag)
|
||||
return [[[NSISO8601DateFormatter alloc] init] stringFromDate:[NSDate date]];
|
||||
// NSDateComponents *now = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear fromDate:[NSDate date]];
|
||||
// return [NSString stringWithFormat:@"%04ld-%02ld-%02ld", now.year, now.month, now.day];
|
||||
return [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterNoStyle];
|
||||
}
|
||||
|
||||
/// Count items where @c xmlURL key is set.
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
@implementation SettingsFeeds
|
||||
|
||||
// TODO: drag-n-drop feeds to opml file?
|
||||
// Declare a string constant for the drag type - to be used when writing and retrieving pasteboard data...
|
||||
static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
@@ -52,37 +53,80 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
|
||||
self.dataStore.managedObjectContext.undoManager = self.undoManager;
|
||||
self.dataStore.managedObjectContext.automaticallyMergesChangesFromParent = NO;
|
||||
}
|
||||
|
||||
/**
|
||||
Refresh current context from parent context and start new undo grouping.
|
||||
@note Should be balanced with @c endCoreDataChangeUndoChanges:
|
||||
*/
|
||||
- (void)beginCoreDataChange {
|
||||
// Does seem to create problems with undo stack if refreshing from parent context
|
||||
//[self.dataStore.managedObjectContext refreshAllObjects];
|
||||
[self.undoManager beginUndoGrouping];
|
||||
}
|
||||
|
||||
/**
|
||||
End undo grouping and save changes to persistent store. Or undo group if no changes occured.
|
||||
@note Should be balanced with @c beginCoreDataChange
|
||||
|
||||
@param flag If @c YES force @c NSUndoManager to undo the changes immediatelly.
|
||||
@return Returns @c YES if context was saved.
|
||||
*/
|
||||
- (BOOL)endCoreDataChangeShouldUndo:(BOOL)flag {
|
||||
[self.undoManager endUndoGrouping];
|
||||
if (!flag && self.dataStore.managedObjectContext.hasChanges) {
|
||||
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
||||
return YES;
|
||||
}
|
||||
[self.undoManager disableUndoRegistration];
|
||||
[self.undoManager undoNestedGroup];
|
||||
[self.undoManager enableUndoRegistration];
|
||||
return NO;
|
||||
}
|
||||
|
||||
/**
|
||||
After the user did undo or redo we can't ensure integrity without doing some additional work.
|
||||
*/
|
||||
- (void)saveWithUnpredictableChange {
|
||||
NSSet<Feed*> *arr = [self.dataStore.managedObjectContext.insertedObjects
|
||||
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@", [Feed class]]];
|
||||
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
||||
[StoreCoordinator restoreFeedCountsAndIndexPaths:[arr valueForKeyPath:@"objectID"]]; // main context will not create undo group
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||
[self.dataStore rearrangeObjects]; // update ordering
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UI Button Interaction
|
||||
|
||||
|
||||
/// Add feed button.
|
||||
- (IBAction)addFeed:(id)sender {
|
||||
[self showModalForFeedGroup:nil isGroupEdit:NO];
|
||||
}
|
||||
|
||||
/// Add group button.
|
||||
- (IBAction)addGroup:(id)sender {
|
||||
[self showModalForFeedGroup:nil isGroupEdit:YES];
|
||||
}
|
||||
|
||||
/// Add separator button.
|
||||
- (IBAction)addSeparator:(id)sender {
|
||||
[self.undoManager beginUndoGrouping];
|
||||
[self beginCoreDataChange];
|
||||
[self insertFeedGroupAtSelection:SEPARATOR].name = @"---";
|
||||
[self.undoManager endUndoGrouping];
|
||||
[self saveChanges];
|
||||
[self endCoreDataChangeShouldUndo:NO];
|
||||
}
|
||||
|
||||
/// Remove user selected item from persistent store.
|
||||
/// Remove feed button. User has selected one or more item in outline view.
|
||||
- (IBAction)remove:(id)sender {
|
||||
[self.undoManager beginUndoGrouping];
|
||||
[self beginCoreDataChange];
|
||||
NSArray<NSTreeNode*> *parentNodes = [self.dataStore.selectedNodes valueForKeyPath:@"parentNode"];
|
||||
[self.dataStore remove:sender];
|
||||
for (NSTreeNode *parent in parentNodes) {
|
||||
[self restoreOrderingAndIndexPathStr:parent];
|
||||
}
|
||||
[self.undoManager endUndoGrouping];
|
||||
[self saveChanges];
|
||||
[self endCoreDataChangeShouldUndo:NO];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||
}
|
||||
|
||||
@@ -94,6 +138,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
[self showModalForFeedGroup:fg isGroupEdit:YES]; // yes will be overwritten anyway
|
||||
}
|
||||
|
||||
/// Share menu button. Currently only import & export feeds as OPML.
|
||||
- (IBAction)shareMenu:(NSButton*)sender {
|
||||
if (!sender.menu) {
|
||||
sender.menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Import / Export menu", nil)];
|
||||
@@ -116,11 +161,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
#pragma mark - Insert & Edit Feed Items / Modal Dialog
|
||||
|
||||
|
||||
/// Save core data changes of current object context to persistent store
|
||||
- (void)saveChanges {
|
||||
[StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES];
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
@@ -130,7 +170,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
*/
|
||||
- (void)showModalForFeedGroup:(FeedGroup*)fg isGroupEdit:(BOOL)flag {
|
||||
if (fg.type == SEPARATOR) return;
|
||||
[self.undoManager beginUndoGrouping];
|
||||
[self beginCoreDataChange];
|
||||
if (!fg || ![fg isKindOfClass:[FeedGroup class]]) {
|
||||
fg = [self insertFeedGroupAtSelection:(flag ? GROUP : FEED)];
|
||||
}
|
||||
@@ -140,19 +180,9 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
[self.view.window beginSheet:[editDialog getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
||||
if (returnCode == NSModalResponseOK) {
|
||||
[editDialog applyChangesToCoreDataObject];
|
||||
[self.undoManager endUndoGrouping];
|
||||
} else {
|
||||
[self.undoManager endUndoGrouping];
|
||||
[self.dataStore.managedObjectContext rollback];
|
||||
}
|
||||
BOOL hasChanges = [self.dataStore.managedObjectContext hasChanges];
|
||||
if (hasChanges) {
|
||||
[self saveChanges];
|
||||
if ([self endCoreDataChangeShouldUndo:(returnCode != NSModalResponseOK)]) {
|
||||
[self.dataStore rearrangeObjects];
|
||||
} else {
|
||||
[self.undoManager disableUndoRegistration];
|
||||
[self.undoManager undoNestedGroup];
|
||||
[self.undoManager enableUndoRegistration];
|
||||
}
|
||||
}];
|
||||
}
|
||||
@@ -213,7 +243,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
/// Begin drag-n-drop operation by copying selected nodes to memory
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard {
|
||||
[self.undoManager beginUndoGrouping];
|
||||
[self beginCoreDataChange];
|
||||
[pboard declareTypes:[NSArray arrayWithObject:dragNodeType] owner:self];
|
||||
[pboard setString:@"dragging" forType:dragNodeType];
|
||||
self.currentlyDraggedNodes = items;
|
||||
@@ -222,14 +252,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
/// 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) {
|
||||
[self saveChanges];
|
||||
} else {
|
||||
[self.undoManager disableUndoRegistration];
|
||||
[self.undoManager undoNestedGroup];
|
||||
[self.undoManager enableUndoRegistration];
|
||||
}
|
||||
[self endCoreDataChangeShouldUndo:NO];
|
||||
self.currentlyDraggedNodes = nil;
|
||||
}
|
||||
|
||||
@@ -318,17 +341,13 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
/// Perform undo operation and redraw UI & menu bar unread count
|
||||
- (void)undo:(id)sender {
|
||||
[self.undoManager undo];
|
||||
[self saveChanges];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||
[self.dataStore rearrangeObjects]; // update ordering
|
||||
[self saveWithUnpredictableChange];
|
||||
}
|
||||
|
||||
/// Perform redo operation and redraw UI & menu bar unread count
|
||||
- (void)redo:(id)sender {
|
||||
[self.undoManager redo];
|
||||
[self saveChanges];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||
[self.dataStore rearrangeObjects]; // update ordering
|
||||
[self saveWithUnpredictableChange];
|
||||
}
|
||||
|
||||
/// User pressed enter; open edit dialog for selected item.
|
||||
@@ -362,7 +381,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
}
|
||||
[[NSPasteboard generalPasteboard] clearContents];
|
||||
[[NSPasteboard generalPasteboard] setString:str forType:NSPasteboardTypeString];
|
||||
NSLog(@"%@", str); // TODO: drag-n-drop feed to opml?
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
- (IBAction)fixCache:(NSButton *)sender {
|
||||
[StoreCoordinator deleteUnreferencedFeeds];
|
||||
[StoreCoordinator restoreFeedCountsAndIndexPaths];
|
||||
[StoreCoordinator restoreFeedCountsAndIndexPaths:nil];
|
||||
}
|
||||
|
||||
- (IBAction)changeMenuBarIconSetting:(NSButton*)sender {
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
@property (readonly) BOOL didCloseAndSave;
|
||||
@property (readonly) BOOL didCloseAndCancel;
|
||||
|
||||
+ (instancetype)modalWithView:(NSView*)content;
|
||||
- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_UNAVAILABLE;
|
||||
- (instancetype)initWithView:(NSView*)content NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)setDoneEnabled:(BOOL)accept;
|
||||
- (void)extendContentViewBy:(CGFloat)dy;
|
||||
@end
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#import "ModalSheet.h"
|
||||
|
||||
@interface ModalSheet()
|
||||
@property (strong) NSButton *btnDone;
|
||||
@property (weak) NSButton *btnDone;
|
||||
@end
|
||||
|
||||
@implementation ModalSheet
|
||||
@@ -52,15 +52,13 @@
|
||||
[self.sheetParent endSheet:self returnCode:response];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Designated initializer for @c ModalSheet.
|
||||
Designated initializer for @c ModalSheet. 'Done' and 'Cancel' button will be added automatically.
|
||||
|
||||
@param content @c NSView will be displayed in dialog box. 'Done' and 'Cancel' button will be added automatically.
|
||||
@param content @c NSView will be displayed in dialog box.
|
||||
*/
|
||||
+ (instancetype)modalWithView:(NSView*)content {
|
||||
- (instancetype)initWithView:(NSView*)content {
|
||||
static const int padWindow = 20;
|
||||
static const int padButtons = 12;
|
||||
static const int minWidth = 320;
|
||||
static const int maxWidth = 1200;
|
||||
|
||||
@@ -68,48 +66,67 @@
|
||||
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);
|
||||
NSSize contentSize = NSMakeSize(prevWidth, content.frame.size.height);
|
||||
[content setFrameSize:contentSize];
|
||||
|
||||
NSSize wSize = NSMakeSize(contentSize.width + 2 * padWindow, contentSize.height + 2 * padWindow);
|
||||
|
||||
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;
|
||||
ModalSheet *sheet = [[super alloc] initWithContentRect:wFrame styleMask:style backing:NSBackingStoreBuffered defer:NO];
|
||||
|
||||
// Respond buttons
|
||||
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 = [sheet.btnDone alignmentRectForFrame:sheet.btnDone.frame];
|
||||
align.origin.x = wFrame.size.width - align.size.width - padWindow;
|
||||
align.origin.y = padWindow;
|
||||
[sheet.btnDone setFrameOrigin:[sheet.btnDone frameForAlignmentRect:align].origin];
|
||||
|
||||
align.origin.x -= [btnCancel alignmentRectForFrame:btnCancel.frame].size.width + padButtons;
|
||||
[btnCancel setFrameOrigin:[btnCancel frameForAlignmentRect:align].origin];
|
||||
|
||||
// this is equivalent, however I'm not sure if these values will change in a future OS
|
||||
// [btnDone setFrameOrigin:NSMakePoint(wFrame.size.width - btnDone.frame.size.width - 12, 13)]; // =20 with alignment
|
||||
// [btnCancel setFrameOrigin:NSMakePoint(btnDone.frame.origin.x - btnCancel.frame.size.width, 13)];
|
||||
|
||||
// add all UI elements to the window view
|
||||
content.frame = cFrame;
|
||||
[sheet.contentView addSubview:content];
|
||||
[sheet.contentView addSubview:sheet.btnDone];
|
||||
[sheet.contentView addSubview:btnCancel];
|
||||
|
||||
// add respond buttons to the window height
|
||||
wFrame.size.height += align.size.height + padButtons;
|
||||
[sheet setContentSize:wFrame.size];
|
||||
|
||||
// constraints on resizing
|
||||
sheet.minSize = NSMakeSize(minWidth + 2 * padWindow, wFrame.size.height);
|
||||
sheet.maxSize = NSMakeSize(maxWidth, wFrame.size.height);
|
||||
return sheet;
|
||||
self = [super initWithContentRect:NSMakeRect(0, 0, wSize.width, wSize.height) styleMask:style backing:NSBackingStoreBuffered defer:NO];
|
||||
if (self) {
|
||||
NSButton *btnDone = [NSButton buttonWithTitle:NSLocalizedString(@"Done", nil) target:self action:@selector(didTapDoneButton:)];
|
||||
NSButton *btnCancel = [NSButton buttonWithTitle:NSLocalizedString(@"Cancel", nil) target:self action:@selector(didTapCancelButton:)];
|
||||
btnDone.keyEquivalent = @"\r"; // Enter / Return
|
||||
btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC
|
||||
|
||||
// Make room for buttons
|
||||
wSize.height += btnDone.frame.size.height;
|
||||
[self setContentSize:wSize];
|
||||
|
||||
// Restrict resizing to width only (after setContentSize:)
|
||||
self.minSize = NSMakeSize(minWidth + 2 * padWindow, wSize.height);
|
||||
self.maxSize = NSMakeSize(maxWidth + 2 * padWindow, wSize.height);
|
||||
|
||||
// Content view (set origin after setContentSize:)
|
||||
[content setFrameOrigin:NSMakePoint(padWindow, wSize.height - padWindow - contentSize.height)];
|
||||
[self.contentView addSubview:content];
|
||||
|
||||
// Respond buttons
|
||||
[self placeButtons:@[btnDone, btnCancel] inBottomRightCornerWithPadding:padWindow];
|
||||
[self.contentView addSubview:btnCancel];
|
||||
[self.contentView addSubview:btnDone];
|
||||
self.btnDone = btnDone;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
Buttons will stick to the right margin and bottom margin when resizing. Also sets autoresizingMask.
|
||||
|
||||
@param buttons First item is rightmost button. Next buttons will be appended left of that button and so on.
|
||||
@param padding Distance between button and right / bottom edge.
|
||||
*/
|
||||
- (void)placeButtons:(NSArray<NSButton*> *)buttons inBottomRightCornerWithPadding:(int)padding {
|
||||
NSEdgeInsets edge = buttons.firstObject.alignmentRectInsets;
|
||||
NSPoint p = NSMakePoint(self.contentView.frame.size.width - padding + edge.right, padding - edge.bottom);
|
||||
for (NSButton *btn in buttons) {
|
||||
p.x -= btn.frame.size.width;
|
||||
[btn setFrameOrigin:p];
|
||||
btn.autoresizingMask = NSViewMinXMargin | NSViewMaxYMargin;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Resize modal window by @c dy. Makes room for additional content. Use negative values to shrink window.
|
||||
*/
|
||||
- (void)extendContentViewBy:(CGFloat)dy {
|
||||
self.minSize = NSMakeSize(self.minSize.width, self.minSize.height + dy);
|
||||
self.maxSize = NSMakeSize(self.maxSize.width, self.maxSize.height + dy);
|
||||
NSRect r = self.frame;
|
||||
r.size.height += dy;
|
||||
[self setFrame:r display:YES animate:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user