- Moved modal sheet to its own ViewController

- Warning Indicator for non parsable URLs
- Popover for error description
- Modal Controller handles CoreData update
- Bugfixes
This commit is contained in:
relikd
2018-08-10 02:59:24 +02:00
parent 7d3c7d7da9
commit d1afaef1de
16 changed files with 584 additions and 277 deletions

View File

@@ -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 = "<group>"; };
54ACC29621061FBA0020715F /* Preferences.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Preferences.h; sourceTree = "<group>"; };
54ACC29721061FBA0020715F /* Preferences.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Preferences.m; sourceTree = "<group>"; };
54E8831D211B509D00064188 /* ModalFeedEdit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModalFeedEdit.h; sourceTree = "<group>"; };
54E8831E211B509D00064188 /* ModalFeedEdit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEdit.m; sourceTree = "<group>"; };
54E8831F211B509D00064188 /* ModalFeedEdit.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ModalFeedEdit.xib; sourceTree = "<group>"; };
54EC3E1D211D03C100E314F4 /* FeedConfig+Print.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FeedConfig+Print.h"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -81,28 +87,27 @@
name = Frameworks;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
@@ -157,6 +162,19 @@
path = baRSS;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 */,
);

View File

@@ -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

View File

@@ -24,4 +24,5 @@
@interface NewsController : NSObject
+ (void)downloadFeed:(NSString*)url withBlock:(nullable void (^)(NSDictionary* result, NSError* error))block;
@end

View File

@@ -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

View File

@@ -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 */

View File

@@ -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 <Cocoa/Cocoa.h>
@protocol ModalFeedConfigEdit <NSObject>
- (void)updateRepresentedObject; // must call [item.managedObjectContext refreshAllObjects]
@end
@interface ModalFeedEdit : NSViewController <ModalFeedConfigEdit, NSTextFieldDelegate>
@end
@interface ModalGroupEdit : NSViewController <ModalFeedConfigEdit>
@end

View File

@@ -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

View File

@@ -0,0 +1,175 @@
<?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">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ModalFeedEdit">
<connections>
<outlet property="name" destination="ab8-rr-HbK" id="J4T-Zl-KF3"/>
<outlet property="refreshNum" destination="cNl-ht-xws" id="3cA-TW-qi5"/>
<outlet property="refreshUnit" destination="TUi-VS-ge4" id="dr6-GW-gU0"/>
<outlet property="spinnerName" destination="Afo-pQ-8Qx" id="DVx-vd-Zer"/>
<outlet property="spinnerURL" destination="H0a-x4-o4X" id="MgB-RI-yP5"/>
<outlet property="url" destination="Asm-D9-ZfT" id="3gO-Xc-2KJ"/>
<outlet property="view" destination="i0K-k8-GMU" id="qcu-Oh-rOj"/>
<outlet property="warningIndicator" destination="LWE-Y8-ebl" id="j9x-OY-2th"/>
<outlet property="warningPopover" destination="stq-gJ-ra0" id="rJy-GV-PHk"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<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"/>
<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"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="URL" id="6wE-lP-4xC">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</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"/>
<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="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="-2" id="R3c-aF-If2"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kVL-HV-oxU" userLabel="Name Label">
<rect key="frame" x="-2" y="31" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name" id="2ls-F4-oUL">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ab8-rr-HbK">
<rect key="frame" x="107" y="29" width="193" 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="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Tc-as-s1U" userLabel="Refresh Label">
<rect key="frame" x="-2" y="2" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Refresh" id="2IV-ec-RfH">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNl-ht-xws">
<rect key="frame" x="107" y="0.0" width="85" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<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="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TUi-VS-ge4">
<rect key="frame" x="198" y="-3" width="125" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Minutes" bezelStyle="rounded" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" altersStateOfSelectedItem="NO" selectedItem="CsM-KR-zzs" id="O0p-Tc-KQ1">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="7hX-7Y-rtT">
<items>
<menuItem title="Seconds" keyEquivalent="s" id="VD1-1h-Hdh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Minutes" state="on" keyEquivalent="m" id="CsM-KR-zzs">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Hours" keyEquivalent="h" id="Nqd-L9-4V8">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Days" keyEquivalent="d" id="5c2-Mb-3aw">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Weeks" keyEquivalent="w" id="mJE-8n-iKF">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="H0a-x4-o4X">
<rect key="frame" x="304" y="60" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
</progressIndicator>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="Afo-pQ-8Qx">
<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">
<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">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="cellTitle"/>
<string key="keyEquivalent">i</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</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>
<point key="canvasLocation" x="-137" y="586.5"/>
</customView>
<viewController id="xTH-2c-Ppt" userLabel="Popover View Controller">
<connections>
<outlet property="view" destination="bVj-RM-sjw" id="TP8-Eb-GVO"/>
</connections>
</viewController>
<popover behavior="t" id="stq-gJ-ra0">
<connections>
<outlet property="contentViewController" destination="xTH-2c-Ppt" id="ODh-uM-ARs"/>
</connections>
</popover>
<customView id="bVj-RM-sjw" userLabel="Popover View">
<rect key="frame" x="0.0" y="0.0" width="300" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField wantsLayer="YES" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nCT-Lc-wce">
<rect key="frame" x="2" y="2" width="296" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<textFieldCell key="cell" truncatesLastVisibleLine="YES" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" id="YJs-n4-Lxb">
<font key="font" metaFont="system"/>
<string key="title">Couldn't load Feed
An additional line
and a third</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="14" y="477"/>
</customView>
</objects>
<resources>
<image name="NSCaution" width="32" height="32"/>
</resources>
</document>

View File

@@ -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<ModalFeedConfigEdit> *modalController;
@property (strong) NSArray<NSTreeNode*> *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<FeedConfig*> *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<NSTreeNode*> *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];
}
}

View File

@@ -11,8 +11,6 @@
<outlet property="dataStore" destination="JPf-gH-wxm" id="9qy-D6-L4R"/>
<outlet property="outlineView" destination="wP9-Vd-f79" id="nKf-fc-7Np"/>
<outlet property="view" destination="zfc-Ie-Sdx" id="65R-bK-FDI"/>
<outlet property="viewModalEditFeed" destination="0p5-Fv-ym4" id="C7K-zb-25M"/>
<outlet property="viewModalEditGroup" destination="ZZI-U3-ymX" id="G34-k8-YYo"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
@@ -221,120 +219,7 @@ CA
</subviews>
<point key="canvasLocation" x="27" y="883"/>
</customView>
<customView id="0p5-Fv-ym4" userLabel="Modal Edit Feed" customClass="ModalFeedEdit">
<rect key="frame" x="0.0" y="0.0" width="320" height="79"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qGE-iP-bhd" userLabel="URL Label">
<rect key="frame" x="-2" y="60" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="URL" id="20W-UE-O54">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dBf-Ic-aFx">
<rect key="frame" x="107" y="58" width="213" 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="9uG-PC-ydS">
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Gwc-hR-gQU" userLabel="Name Label">
<rect key="frame" x="-2" y="31" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Name" id="ZZt-vJ-gqR">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sif-B7-ReQ">
<rect key="frame" x="107" y="29" width="213" 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="B6j-EO-17H">
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OtU-zo-1Fb" userLabel="Refresh Label">
<rect key="frame" x="-2" y="2" width="103" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Refresh" id="XDh-kX-OcM">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kGm-Pt-t1H">
<rect key="frame" x="107" y="0.0" width="85" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="30" drawsBackground="YES" usesSingleLineMode="YES" id="rm9-UV-YhG">
<customFormatter key="formatter" id="Ubj-jG-b6k" customClass="StrictUIntFormatter"/>
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mB4-d4-lQz" userLabel="TimeUnit">
<rect key="frame" x="198" y="-3" width="125" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Seconds" bezelStyle="rounded" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" altersStateOfSelectedItem="NO" selectedItem="KZL-Pi-rjI" id="0ux-Qa-zbq">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" showsStateColumn="NO" autoenablesItems="NO" id="Snb-Bo-1GG">
<items>
<menuItem title="Seconds" keyEquivalent="s" id="TnX-Hk-ZFr">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Minutes" keyEquivalent="m" id="KZL-Pi-rjI">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Hours" keyEquivalent="h" id="uwN-nk-aaQ">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Days" keyEquivalent="d" id="XCA-xq-97w">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Weeks" keyEquivalent="w" id="r9d-8y-QrY">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</subviews>
<connections>
<outlet property="refreshNum" destination="kGm-Pt-t1H" id="OJY-yn-Mye"/>
<outlet property="refreshUnit" destination="mB4-d4-lQz" id="xsk-oZ-up1"/>
<outlet property="title" destination="Sif-B7-ReQ" id="vdX-oR-Lvv"/>
<outlet property="url" destination="dBf-Ic-aFx" id="DqH-R5-Sfe"/>
</connections>
<point key="canvasLocation" x="27" y="1131"/>
</customView>
<customView id="ZZI-U3-ymX" userLabel="Modal Edit Group" customClass="ModalGroupEdit">
<rect key="frame" x="0.0" y="0.0" width="260" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zl1-tn-hla">
<rect key="frame" x="0.0" y="0.0" width="260" 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" title="Group Name" placeholderString="Group Name" drawsBackground="YES" usesSingleLineMode="YES" id="afG-YW-7CW">
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="title" destination="zl1-tn-hla" id="R4x-bv-CHB"/>
</connections>
<point key="canvasLocation" x="27" y="1229"/>
</customView>
<viewController id="TaZ-4L-TdU" customClass="ModalFeedEdit"/>
</objects>
<resources>
<image name="NSActionTemplate" width="14" height="14"/>

View File

@@ -219,7 +219,7 @@
</popUpButtonCell>
</popUpButton>
</subviews>
<point key="canvasLocation" x="-20" y="903.5"/>
<point key="canvasLocation" x="91" y="-163"/>
</customView>
</objects>
<resources>

View File

@@ -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

View File

@@ -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