OPML export / import + bug fixes + Refactoring (RSXML 2.0, StoreCoordinator, Feed type)
This commit is contained in:
@@ -73,6 +73,7 @@
|
||||
@property (copy) NSString *previousURL; // check if changed and avoid multiple download
|
||||
@property (copy) NSString *httpDate;
|
||||
@property (copy) NSString *httpEtag;
|
||||
@property (strong) NSImage *favicon;
|
||||
@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
|
||||
@@ -85,6 +86,8 @@
|
||||
[super viewDidLoad];
|
||||
self.previousURL = @"";
|
||||
self.refreshNum.intValue = 30;
|
||||
self.warningIndicator.image = nil;
|
||||
[self.warningIndicator.cell setHighlightsBy:NSNoCellMask];
|
||||
[self populateTextFields:self.feedGroup];
|
||||
}
|
||||
|
||||
@@ -101,6 +104,7 @@
|
||||
if (unit < 0 || unit > self.refreshUnit.numberOfItems - 1)
|
||||
unit = self.refreshUnit.numberOfItems - 1;
|
||||
[self.refreshUnit selectItemAtIndex:unit];
|
||||
self.warningIndicator.image = [fg.feed iconImage16];
|
||||
}
|
||||
|
||||
#pragma mark - Edit Feed Data
|
||||
@@ -111,31 +115,27 @@
|
||||
*/
|
||||
- (void)applyChangesToCoreDataObject {
|
||||
Feed *feed = self.feedGroup.feed;
|
||||
[self.feedGroup setNameIfChanged:self.name.stringValue];
|
||||
FeedMeta *meta = 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]];
|
||||
[meta setUrlIfChanged:self.previousURL];
|
||||
[meta setRefresh:self.refreshNum.intValue unit:(int16_t)self.refreshUnit.indexOfSelectedItem]; // updateTimer will be scheduled once preferences is closed
|
||||
if (self.didDownloadFeed) {
|
||||
[meta setEtag:self.httpEtag modified:self.httpDate];
|
||||
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
|
||||
}
|
||||
if (!feed.icon) {
|
||||
NSString *faviconURL = feed.link;
|
||||
if (faviconURL.length == 0)
|
||||
faviconURL = meta.url;
|
||||
[FeedDownload backgroundDownloadFavicon:faviconURL forFeed:feed];
|
||||
[feed setIcon:self.favicon replaceExisting: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.
|
||||
Prepare UI (nullify @c result, @c error and start @c ProgressIndicator).
|
||||
Also disable 'Done' button during download and re-enable after all downloads are finished.
|
||||
*/
|
||||
- (void)downloadRSS {
|
||||
[self.modalSheet setDoneEnabled:NO];
|
||||
- (void)preDownload {
|
||||
[self.modalSheet setDoneEnabled:NO]; // prevent user from closing the dialog during download
|
||||
[self.spinnerURL startAnimation:nil];
|
||||
[self.spinnerName startAnimation:nil];
|
||||
self.warningIndicator.image = nil;
|
||||
self.didDownloadFeed = 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]) {
|
||||
@@ -145,62 +145,91 @@
|
||||
self.feedError = nil;
|
||||
self.httpEtag = nil;
|
||||
self.httpDate = nil;
|
||||
self.didDownloadFeed = NO;
|
||||
[self.spinnerURL startAnimation:nil];
|
||||
[self.spinnerName startAnimation:nil];
|
||||
|
||||
self.favicon = nil;
|
||||
}
|
||||
|
||||
/**
|
||||
All properties 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 {
|
||||
if (self.modalSheet.didCloseAndCancel)
|
||||
return;
|
||||
[self preDownload];
|
||||
[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: play error sound?
|
||||
[self.spinnerURL stopAnimation:nil];
|
||||
[self.spinnerName stopAnimation:nil];
|
||||
[self.modalSheet setDoneEnabled:YES];
|
||||
});
|
||||
if (self.modalSheet.didCloseAndCancel)
|
||||
return;
|
||||
self.didDownloadFeed = YES;
|
||||
self.feedResult = result;
|
||||
self.feedError = error;
|
||||
self.httpEtag = [response allHeaderFields][@"Etag"];
|
||||
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
|
||||
[self postDownload:response.URL.absoluteString];
|
||||
}];
|
||||
}
|
||||
|
||||
/// 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
|
||||
/**
|
||||
Update UI TextFields with downloaded values.
|
||||
Title will be updated if TextField is empty. URL on redirect.
|
||||
Finally begin favicon download and return control to user (enable 'Done' button).
|
||||
*/
|
||||
- (void)postDownload:(NSString*)responseURL {
|
||||
if (self.modalSheet.didCloseAndCancel)
|
||||
return;
|
||||
// 1. Stop spinner animation for name field. (keep spinner for URL running until favicon downloaded)
|
||||
// TODO: play error sound?
|
||||
[self.spinnerName stopAnimation:nil];
|
||||
// 2. If URL was redirected, replace original text field value with new one. (e.g., https redirect)
|
||||
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
|
||||
// 3. Copy parsed feed title to text field. (only if user hasn't set anything else yet)
|
||||
NSString *parsedTitle = self.feedResult.title;
|
||||
if (parsedTitle.length > 0 && [self.name.stringValue isEqualToString:@""]) {
|
||||
self.name.stringValue = parsedTitle; // no damage to replace an empty string
|
||||
}
|
||||
// 4. Continue with favicon download (or finish with error)
|
||||
if (self.feedError) {
|
||||
[self finishDownloadWithFavicon:[NSImage imageNamed:NSImageNameCaution]];
|
||||
} else {
|
||||
NSString *faviconURL = self.feedResult.link; // TODO: add support for custom URLs ?
|
||||
if (faviconURL.length == 0)
|
||||
faviconURL = responseURL;
|
||||
[FeedDownload downloadFavicon:faviconURL finished:^(NSImage * _Nullable img) {
|
||||
if (self.modalSheet.didCloseAndCancel)
|
||||
return;
|
||||
self.favicon = img;
|
||||
[self finishDownloadWithFavicon:img];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The last step of the download process.
|
||||
Stop spinning animation set favivon image preview (right of url bar) and re-enable 'Done' button.
|
||||
*/
|
||||
- (void)finishDownloadWithFavicon:(NSImage*)img {
|
||||
if (self.modalSheet.didCloseAndCancel)
|
||||
return;
|
||||
[self.warningIndicator.cell setHighlightsBy: (self.feedError ? NSContentsCellMask : NSNoCellMask)];
|
||||
self.warningIndicator.image = img;
|
||||
[self.spinnerURL stopAnimation:nil];
|
||||
[self.modalSheet setDoneEnabled:YES];
|
||||
}
|
||||
|
||||
|
||||
#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]) {
|
||||
if (self.modalSheet.closeInitiated)
|
||||
return;
|
||||
self.previousURL = self.url.stringValue;
|
||||
[self downloadRSS];
|
||||
if (obj.object == self.url) {
|
||||
if (![self.previousURL isEqualToString:self.url.stringValue]) {
|
||||
self.previousURL = self.url.stringValue;
|
||||
[self downloadRSS];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,9 +273,7 @@
|
||||
}
|
||||
/// 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;
|
||||
[self.feedGroup setNameIfChanged:((NSTextField*)self.view).stringValue];
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -35,11 +35,11 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" fixedFrame="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Asm-D9-ZfT">
|
||||
<rect key="frame" x="107" y="58" width="193" height="21"/>
|
||||
<rect key="frame" x="107" y="58" width="191" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="https://example.org/feed.rss" drawsBackground="YES" usesSingleLineMode="YES" id="0Sk-H2-VAC">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
@@ -56,11 +56,11 @@
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ab8-rr-HbK">
|
||||
<rect key="frame" x="107" y="29" width="193" height="21"/>
|
||||
<rect key="frame" x="107" y="29" width="191" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="Example Title" drawsBackground="YES" usesSingleLineMode="YES" id="1ku-vp-T5y">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
@@ -79,7 +79,7 @@
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="30" drawsBackground="YES" usesSingleLineMode="YES" id="DqU-fT-cIf">
|
||||
<customFormatter key="formatter" id="Lbd-r9-4bc" customClass="StrictUIntFormatter"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
@@ -118,7 +118,7 @@
|
||||
<rect key="frame" x="304" y="31" width="16" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
|
||||
</progressIndicator>
|
||||
<button hidden="YES" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LWE-Y8-ebl">
|
||||
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LWE-Y8-ebl">
|
||||
<rect key="frame" x="302" y="60" width="18" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="roundRect" bezelStyle="roundedRect" image="NSCaution" imagePosition="only" alignment="center" refusesFirstResponder="YES" state="on" imageScaling="proportionallyDown" inset="2" id="FAw-6c-Vij">
|
||||
@@ -129,11 +129,6 @@
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="didClickWarningButton:" target="-2" id="wNa-Cc-jZb"/>
|
||||
<binding destination="-2" name="hidden" keyPath="self.feedError" id="o3F-lJ-LPU">
|
||||
<dictionary key="options">
|
||||
<string key="NSValueTransformerName">NSIsNil</string>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
|
||||
31
baRSS/Preferences/Feeds Tab/OpmlExport.h
Normal file
31
baRSS/Preferences/Feeds Tab/OpmlExport.h
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// 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 <Foundation/Foundation.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class Feed;
|
||||
|
||||
@interface OpmlExport : NSObject
|
||||
+ (void)showImportDialog:(NSWindow*)window withTreeController:(NSTreeController*)tree;
|
||||
+ (void)showExportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc;
|
||||
@end
|
||||
327
baRSS/Preferences/Feeds Tab/OpmlExport.m
Normal file
327
baRSS/Preferences/Feeds Tab/OpmlExport.m
Normal file
@@ -0,0 +1,327 @@
|
||||
//
|
||||
// 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 "OpmlExport.h"
|
||||
#import "FeedMeta+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "FeedDownload.h"
|
||||
#import "Constants.h"
|
||||
|
||||
@implementation OpmlExport
|
||||
|
||||
#pragma mark - Open & Save Panel
|
||||
|
||||
/// Display Open File Panel to select @c .opml file.
|
||||
+ (void)showImportDialog:(NSWindow*)window withContext:(NSManagedObjectContext*)moc success:(nullable void(^)(NSArray<Feed*> *added))block {
|
||||
NSOpenPanel *op = [NSOpenPanel openPanel];
|
||||
op.allowedFileTypes = @[@"opml"];
|
||||
[op beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
|
||||
if (result == NSModalResponseOK) {
|
||||
[self importFeedData:op.URL inContext:moc success:block];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/// 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.allowedFileTypes = @[@"opml"];
|
||||
sp.allowsOtherFileTypes = YES;
|
||||
NSView *radioView = [self radioGroupCreate:@[NSLocalizedString(@"Hierarchical", nil),
|
||||
NSLocalizedString(@"Flattened", nil)]];
|
||||
sp.accessoryView = [self viewByPrependingLabel:NSLocalizedString(@"Export format:", nil) toView:radioView];
|
||||
|
||||
[sp beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
|
||||
if (result == NSModalResponseOK) {
|
||||
BOOL flattened = ([self radioGroupSelection:radioView] == 1);
|
||||
NSString *exportString = [self exportFeedsHierarchical:!flattened inContext:moc];
|
||||
NSError *error;
|
||||
[exportString writeToURL:sp.URL atomically:YES encoding:NSUTF8StringEncoding error:&error];
|
||||
if (error) {
|
||||
[NSApp presentError:error];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/// 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.undoManager beginUndoGrouping];
|
||||
[self showImportDialog:window withContext:moc success:^(NSArray<Feed *> *added) {
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[FeedDownload batchDownloadRSSAndFavicons:added showErrorAlert:YES rssFinished:^(NSArray<Feed *> *successful, BOOL *cancelFavicons) {
|
||||
if (successful.count > 0)
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
// we need to post a reset, since after deletion total unread count is wrong
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountReset object:nil];
|
||||
} finally:^(BOOL successful) {
|
||||
[moc.undoManager endUndoGrouping];
|
||||
if (successful) {
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[tree rearrangeObjects]; // rearrange, because no new items appread instead only icon attrib changed
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Import
|
||||
|
||||
|
||||
/**
|
||||
Ask user for permission to import new items (prior import). User can choose to append or replace existing items.
|
||||
If user chooses to replace existing items, perform core data request to delete all feeds.
|
||||
|
||||
@param document Used to count feed items that will be imported
|
||||
@return @c NO if user clicks 'Cancel' button. @c YES otherwise.
|
||||
*/
|
||||
+ (BOOL)askToAppendOrOverwriteAlert:(RSOPMLItem*)document inContext:(NSManagedObjectContext*)moc {
|
||||
NSUInteger count = [self recursiveNumberOfFeeds:document];
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Import of %lu feed items", nil), count];
|
||||
alert.informativeText = NSLocalizedString(@"Do you want to append or replace existing items?", nil);
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"Import", nil)];
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)];
|
||||
alert.accessoryView = [self radioGroupCreate:@[NSLocalizedString(@"Append", nil),
|
||||
NSLocalizedString(@"Overwrite", nil)]];
|
||||
NSModalResponse code = [alert runModal];
|
||||
if (code == NSAlertSecondButtonReturn) { // cancel button
|
||||
return NO;
|
||||
}
|
||||
if ([self radioGroupSelection:alert.accessoryView] == 1) { // overwrite selected
|
||||
for (FeedGroup *g in [StoreCoordinator sortedListOfRootObjectsInContext:moc]) {
|
||||
[moc deleteObject:g];
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
/**
|
||||
Perform import of @c FeedGroup items.
|
||||
|
||||
@param block Called after import finished. Parameter @c added is the list of inserted @c Feed items.
|
||||
*/
|
||||
+ (void)importFeedData:(NSURL*)fileURL inContext:(NSManagedObjectContext*)moc success:(nullable void(^)(NSArray<Feed*> *added))block {
|
||||
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
||||
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:@"opml-file-import"];
|
||||
RSOPMLParser *parser = [RSOPMLParser parserWithXMLData:xml];
|
||||
[parser parseAsync:^(RSOPMLItem * _Nullable doc, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
[NSApp presentError:error];
|
||||
} else if ([self askToAppendOrOverwriteAlert:doc inContext:moc]) {
|
||||
NSMutableArray<Feed*> *list = [NSMutableArray array];
|
||||
int32_t idx = 0;
|
||||
if (moc.deletedObjects.count == 0) // if there are deleted objects, user choose to overwrite all items
|
||||
idx = (int32_t)[StoreCoordinator numberRootItemsInContext:moc];
|
||||
|
||||
for (RSOPMLItem *item in doc.children) {
|
||||
[self importFeed:item parent:nil index:idx inContext:moc appendToList:list];
|
||||
idx += 1;
|
||||
}
|
||||
if (block) block(list);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
Import single item and recursively repeat import for each child.
|
||||
|
||||
@param item The item to be imported.
|
||||
@param parent The already processed parent item.
|
||||
@param idx @c sortIndex within the @c parent item.
|
||||
@param moc Managed object context.
|
||||
@param list Mutable list where newly inserted @c Feed items will be added.
|
||||
*/
|
||||
+ (void)importFeed:(RSOPMLItem*)item parent:(FeedGroup*)parent index:(int32_t)idx inContext:(NSManagedObjectContext*)moc appendToList:(NSMutableArray<Feed*> *)list {
|
||||
FeedGroupType type = GROUP;
|
||||
if ([item attributeForKey:OPMLXMLURLKey]) {
|
||||
type = FEED;
|
||||
} else if ([item attributeForKey:@"separator"]) { // baRSS specific
|
||||
type = SEPARATOR;
|
||||
}
|
||||
|
||||
FeedGroup *newFeed = [FeedGroup newGroup:type inContext:moc];
|
||||
[newFeed setParent:parent andSortIndex:idx];
|
||||
newFeed.name = (type == SEPARATOR ? @"---" : item.displayName);
|
||||
|
||||
switch (type) {
|
||||
case GROUP:
|
||||
for (NSUInteger i = 0; i < item.children.count; i++) {
|
||||
[self importFeed:item.children[i] parent:newFeed index:(int32_t)i inContext:moc appendToList:list];
|
||||
}
|
||||
break;
|
||||
|
||||
case FEED:
|
||||
@autoreleasepool {
|
||||
FeedMeta *meta = newFeed.feed.meta;
|
||||
meta.url = [item attributeForKey:OPMLXMLURLKey];
|
||||
id refresh = [item attributeForKey:@"refreshInterval"]; // baRSS specific
|
||||
if (refresh) {
|
||||
[meta setRefreshAndUnitFromInterval:(int32_t)[refresh integerValue]];
|
||||
} else {
|
||||
[meta setRefresh:30 unit:RefreshUnitMinutes];
|
||||
}
|
||||
}
|
||||
[list addObject:newFeed.feed];
|
||||
break;
|
||||
|
||||
case SEPARATOR:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Export
|
||||
|
||||
|
||||
/**
|
||||
Initiate export of current core data state. Write opml header and all root items.
|
||||
|
||||
@param flag If @c YES keep parent-child structure intact. If @c NO ignore all parents and add @c Feed items only.
|
||||
@param moc Managed object context.
|
||||
@return Save this string to file.
|
||||
*/
|
||||
+ (NSString*)exportFeedsHierarchical:(BOOL)flag inContext:(NSManagedObjectContext*)moc {
|
||||
NSDictionary *info = @{@"dateCreated" : [NSDate date], @"ownerName" : @"baRSS", OPMLTitleKey : @"baRSS feeds"};
|
||||
RSOPMLItem *doc = [RSOPMLItem itemWithAttributes:info];
|
||||
@autoreleasepool {
|
||||
NSArray<FeedGroup*> *arr = [StoreCoordinator sortedListOfRootObjectsInContext:moc];
|
||||
for (FeedGroup *item in arr) {
|
||||
[self addChild:item toParent:doc hierarchical:flag];
|
||||
}
|
||||
}
|
||||
return [doc exportOPMLAsString];
|
||||
}
|
||||
|
||||
/**
|
||||
Build up @c RSOPMLItem structure recursively. Essentially, re-create same structure as in core data storage.
|
||||
|
||||
@param flag If @c NO don't add groups to export file but continue evaluation of child items.
|
||||
*/
|
||||
+ (void)addChild:(FeedGroup*)item toParent:(RSOPMLItem*)parent hierarchical:(BOOL)flag {
|
||||
RSOPMLItem *child = [RSOPMLItem new];
|
||||
[child setAttribute:item.name forKey:OPMLTitleKey];
|
||||
if (flag || item.type == SEPARATOR || item.feed) {
|
||||
[parent addChild:child]; // dont add item if item is group and hierarchical == NO
|
||||
}
|
||||
|
||||
if (item.type == SEPARATOR) {
|
||||
[child setAttribute:@"true" forKey:@"separator"]; // baRSS specific
|
||||
} else if (item.feed) {
|
||||
[child setAttribute:@"rss" forKey:OPMLTypeKey];
|
||||
[child setAttribute:item.feed.link forKey:OPMLHMTLURLKey];
|
||||
[child setAttribute:item.feed.meta.url forKey:OPMLXMLURLKey];
|
||||
NSNumber *refreshNum = [NSNumber numberWithInteger:[item.feed.meta refreshInterval]];
|
||||
[child setAttribute:refreshNum forKey:@"refreshInterval"]; // baRSS specific
|
||||
} else {
|
||||
for (FeedGroup *subItem in [item sortedChildren]) {
|
||||
[self addChild:subItem toParent:(flag ? child : parent) hierarchical:flag];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#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];
|
||||
}
|
||||
|
||||
/// Count items where @c xmlURL key is set.
|
||||
+ (NSUInteger)recursiveNumberOfFeeds:(RSOPMLItem*)document {
|
||||
if ([document attributeForKey:OPMLXMLURLKey]) {
|
||||
return 1;
|
||||
} else {
|
||||
NSUInteger sum = 0;
|
||||
for (RSOPMLItem *child in document.children) {
|
||||
sum += [self recursiveNumberOfFeeds:child];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
/// Solely used to group radio buttons
|
||||
+ (void)donothing {}
|
||||
|
||||
/// Create a new view with as many @c NSRadioButton items as there are strings. Buttons @c tag is equal to the array index.
|
||||
+ (NSView*)radioGroupCreate:(NSArray<NSString*>*)titles {
|
||||
if (titles.count == 0)
|
||||
return nil;
|
||||
|
||||
NSRect viewRect = NSMakeRect(0, 0, 0, 8);
|
||||
NSInteger idx = (NSInteger)titles.count;
|
||||
NSView *v = [[NSView alloc] init];
|
||||
for (NSString *title in titles.reverseObjectEnumerator) {
|
||||
idx -= 1;
|
||||
NSButton *btn = [NSButton radioButtonWithTitle:title target:self action:@selector(donothing)];
|
||||
btn.tag = idx;
|
||||
btn.frame = NSOffsetRect(btn.frame, 0, viewRect.size.height);
|
||||
viewRect.size.height += btn.frame.size.height + 2; // 2px padding
|
||||
if (viewRect.size.width < btn.frame.size.width)
|
||||
viewRect.size.width = btn.frame.size.width;
|
||||
[v addSubview:btn];
|
||||
if (idx == 0)
|
||||
btn.state = NSControlStateValueOn;
|
||||
}
|
||||
viewRect.size.height += 6; // 8 - 2px padding
|
||||
v.frame = viewRect;
|
||||
return v;
|
||||
}
|
||||
|
||||
/// Loop over all subviews and find the @c NSButton that is selected.
|
||||
+ (NSInteger)radioGroupSelection:(NSView*)view {
|
||||
for (NSButton *btn in view.subviews) {
|
||||
if ([btn isKindOfClass:[NSButton class]] && btn.state == NSControlStateValueOn) {
|
||||
return btn.tag;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// @return New view with @c NSTextField label in the top left corner and @c radioView on the right side.
|
||||
+ (NSView*)viewByPrependingLabel:(NSString*)str toView:(NSView*)radioView {
|
||||
NSTextField *label = [NSTextField textFieldWithString:str];
|
||||
label.editable = NO;
|
||||
label.selectable = NO;
|
||||
label.bezeled = NO;
|
||||
label.drawsBackground = NO;
|
||||
|
||||
NSRect fL = label.frame;
|
||||
NSRect fR = radioView.frame;
|
||||
fL.origin.y += fR.size.height - fL.size.height - 8;
|
||||
fR.origin.x += fL.size.width;
|
||||
label.frame = fL;
|
||||
radioView.frame = fR;
|
||||
|
||||
NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, NSMaxX(fR), NSMaxY(fR))];
|
||||
[view addSubview:label];
|
||||
[view addSubview:radioView];
|
||||
return view;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -26,6 +26,7 @@
|
||||
#import "ModalFeedEdit.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "OpmlExport.h"
|
||||
|
||||
@interface SettingsFeeds ()
|
||||
@property (weak) IBOutlet NSOutlineView *outlineView;
|
||||
@@ -51,27 +52,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
self.dataStore.managedObjectContext = [StoreCoordinator createChildContext];
|
||||
self.dataStore.managedObjectContext.undoManager = self.undoManager;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(faviconDownloadFinished:) name:kNotificationFaviconDownloadFinished object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the backgroud download of a favicon finished.
|
||||
Notification object contains the updated @c Feed (object id).
|
||||
*/
|
||||
- (void)faviconDownloadFinished:(NSNotification*)notify {
|
||||
if ([notify.object isKindOfClass:[NSManagedObjectID class]]) {
|
||||
// TODO: Bug: Freshly ownloaded images are deleted on undo. Remove delete cascade rule?
|
||||
NSManagedObject *mo = [self.dataStore.managedObjectContext objectWithID:notify.object];
|
||||
if (!mo) return;
|
||||
[self.dataStore.managedObjectContext refreshObject:mo mergeChanges:YES];
|
||||
[self.dataStore rearrangeObjects];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UI Button Interaction
|
||||
|
||||
@@ -112,6 +94,24 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
[self showModalForFeedGroup:fg isGroupEdit:YES]; // yes will be overwritten anyway
|
||||
}
|
||||
|
||||
- (IBAction)shareMenu:(NSButton*)sender {
|
||||
if (!sender.menu) {
|
||||
sender.menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Import / Export menu", nil)];
|
||||
sender.menu.autoenablesItems = NO;
|
||||
[sender.menu addItemWithTitle:NSLocalizedString(@"Import Feeds …", nil) action:nil keyEquivalent:@""].tag = 101;
|
||||
[sender.menu addItemWithTitle:NSLocalizedString(@"Export Feeds …", nil) action:nil keyEquivalent:@""].tag = 102;
|
||||
// TODO: Add menus for online sync? email export? etc.
|
||||
}
|
||||
if ([sender.menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0,sender.frame.size.height) inView:sender]) {
|
||||
NSInteger tag = sender.menu.highlightedItem.tag;
|
||||
if (tag == 101) {
|
||||
[OpmlExport showImportDialog:self.view.window withTreeController:self.dataStore];
|
||||
} else if (tag == 102) {
|
||||
[OpmlExport showExportDialog:self.view.window withContext:self.dataStore.managedObjectContext];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Insert & Edit Feed Items / Modal Dialog
|
||||
|
||||
@@ -129,13 +129,13 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
@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;
|
||||
if (fg.type == SEPARATOR) return;
|
||||
[self.undoManager beginUndoGrouping];
|
||||
if (!fg || ![fg isKindOfClass:[FeedGroup class]]) {
|
||||
fg = [self insertFeedGroupAtSelection:(flag ? GROUP : FEED)];
|
||||
}
|
||||
|
||||
ModalEditDialog *editDialog = (fg.typ == GROUP ? [ModalGroupEdit modalWith:fg] : [ModalFeedEdit modalWith:fg]);
|
||||
ModalEditDialog *editDialog = (fg.type == GROUP ? [ModalGroupEdit modalWith:fg] : [ModalFeedEdit modalWith:fg]);
|
||||
|
||||
[self.view.window beginSheet:[editDialog getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
||||
if (returnCode == NSModalResponseOK) {
|
||||
@@ -275,8 +275,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
/// Populate @c NSOutlineView data cells with core data object values.
|
||||
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
|
||||
FeedGroup *fg = [(NSTreeNode*)item representedObject];
|
||||
BOOL isFeed = (fg.typ == FEED);
|
||||
BOOL isSeperator = (fg.typ == SEPARATOR);
|
||||
BOOL isFeed = (fg.type == FEED);
|
||||
BOOL isSeperator = (fg.type == SEPARATOR);
|
||||
BOOL isRefreshColumn = [tableColumn.identifier isEqualToString:@"RefreshColumn"];
|
||||
BOOL refreshDisabled = (!isFeed || fg.refreshStr.length == 0 || [fg.refreshStr characterAtIndex:0] == '0');
|
||||
|
||||
@@ -290,7 +290,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
return cellView; // the refresh cell is already skipped with the above if condition
|
||||
} else {
|
||||
cellView.textField.objectValue = fg.name;
|
||||
cellView.imageView.image = (fg.typ == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]);
|
||||
cellView.imageView.image = (fg.type == GROUP ? [NSImage imageNamed:NSImageNameFolder] : [fg.feed iconImage16]);
|
||||
}
|
||||
// also for refresh column
|
||||
cellView.textField.textColor = (isFeed && refreshDisabled ? [NSColor disabledControlTextColor] : [NSColor controlTextColor]);
|
||||
@@ -303,8 +303,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
|
||||
/// 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];
|
||||
if (aSelector == @selector(undo:)) return [self.undoManager canUndo] && self.undoManager.groupingLevel == 0;
|
||||
if (aSelector == @selector(redo:)) return [self.undoManager canRedo] && self.undoManager.groupingLevel == 0;
|
||||
if (aSelector == @selector(copy:) || aSelector == @selector(enterPressed:)) {
|
||||
BOOL outlineHasFocus = [[self.view.window firstResponder] isKindOfClass:[NSOutlineView class]];
|
||||
BOOL hasSelection = (self.dataStore.selectedNodes.count > 0);
|
||||
@@ -313,7 +313,7 @@ static NSString *dragNodeType = @"baRSS-feed-drag";
|
||||
if (aSelector == @selector(copy:))
|
||||
return YES;
|
||||
// can edit only if selection is not a separator
|
||||
return (((FeedGroup*)self.dataStore.selectedNodes.firstObject.representedObject).typ != SEPARATOR);
|
||||
return (((FeedGroup*)self.dataStore.selectedNodes.firstObject.representedObject).type != SEPARATOR);
|
||||
}
|
||||
return [super respondsToSelector:aSelector];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -208,16 +208,19 @@ CA
|
||||
<binding destination="JPf-gH-wxm" name="enabled" keyPath="canInsert" id="JCF-ey-YUL"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button hidden="YES" toolTip="Import or Export data" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6ul-3K-fOy">
|
||||
<button toolTip="Import or Export data" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6ul-3K-fOy">
|
||||
<rect key="frame" x="295" y="-1" width="25" height="23"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<buttonCell key="cell" type="smallSquare" alternateTitle="Export" bezelStyle="smallSquare" image="NSShareTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nrA-7c-1sL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="shareMenu:" target="-2" id="JJq-7D-Bti"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<point key="canvasLocation" x="27" y="883"/>
|
||||
<point key="canvasLocation" x="27" y="882.5"/>
|
||||
</customView>
|
||||
<viewController id="TaZ-4L-TdU" customClass="ModalFeedEdit"/>
|
||||
</objects>
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface ModalSheet : NSPanel
|
||||
@property (readonly) BOOL closeInitiated;
|
||||
@property (readonly) BOOL didCloseAndSave;
|
||||
@property (readonly) BOOL didCloseAndCancel;
|
||||
|
||||
+ (instancetype)modalWithView:(NSView*)content;
|
||||
- (void)setDoneEnabled:(BOOL)accept;
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
@end
|
||||
|
||||
@implementation ModalSheet
|
||||
@synthesize closeInitiated = _closeInitiated;
|
||||
@synthesize didCloseAndSave = _didCloseAndSave, didCloseAndCancel = _didCloseAndCancel;
|
||||
|
||||
/// 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]; }
|
||||
- (void)didTapCancelButton:(id)sender { [self closeWithResponse:NSModalResponseCancel]; }
|
||||
/// Manually disable 'Done' button if a task is still running.
|
||||
- (void)setDoneEnabled:(BOOL)accept { self.btnDone.enabled = accept; }
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
And removes all subviews (clean up).
|
||||
*/
|
||||
- (void)closeWithResponse:(NSModalResponse)response {
|
||||
_closeInitiated = YES;
|
||||
_didCloseAndSave = (response == NSModalResponseOK);
|
||||
_didCloseAndCancel = (response != NSModalResponseOK);
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user