From 3d5a2951bb2a7af7b2381d08b0fec7b6f645818a Mon Sep 17 00:00:00 2001 From: Kelvin Sherlock Date: Mon, 7 Jun 2021 00:34:26 -0400 Subject: [PATCH] Squashed commit of the following: commit 78c81626670fdf41fa6bdd71a4243a89a0746615 Author: Kelvin Sherlock Date: Mon Jun 7 00:33:48 2021 -0400 check if software set has a particular entry. commit ef5ab6b6948dc3bbbe2947ea099fcacd08435e86 Author: Kelvin Sherlock Date: Sun Jun 6 22:20:34 2021 -0400 fix scroller background on recent disk images window. commit dee56fa50e87299b396b48361bd0a780aaaaa768 Author: Kelvin Sherlock Date: Sun Jun 6 21:26:23 2021 -0400 update cheat sheet javascript to work with 10.11 * => functions not supported * NodeList.prototype.forEach not supported. commit b00cc05413f4ebd6d6d58f96e24303008608f3a6 Author: Kelvin Sherlock Date: Sun Jun 6 17:10:41 2021 -0400 default full machine name for bookmark entry. commit a671cafdc98051b56b12cdd3ccd13c22f54f605a Author: Kelvin Sherlock Date: Sun Jun 6 15:39:32 2021 -0400 loading a bookmark wasn't updating the media. commit 3000e0eb1b10bede3345aaab8478e9ec209f328c Author: Kelvin Sherlock Date: Sun Jun 6 15:38:53 2021 -0400 bump copyright year. commit 45222dacd4aa0047fae63a9112509de57139df63 Author: Kelvin Sherlock Date: Sun Jun 6 13:38:23 2021 -0400 add reset w/ value for setting the item explicitely. commit cc7fde1253b71c4d8655eb4c010bbf4e61333a15 Author: Kelvin Sherlock Date: Sun Jun 6 13:37:48 2021 -0400 add checkboxes for bitbanger/share directory. The general idea is it's easier to toggle a checkbox than to type/retype a path. commit 5674b2d7f6b0e2f0b973197bf3493ad61bf46428 Author: Kelvin Sherlock Date: Sat Jun 5 19:11:43 2021 -0400 commentary on searches with diacritics. commit ec60634dcd9c573130dc34673b4d3fe597ea2b42 Author: Kelvin Sherlock Date: Sat Jun 5 19:11:22 2021 -0400 clean up auto-complete a little bit when setting a value directly. commit 1a182bbdab237c89d355d8294b5a4a64b785783a Author: Kelvin Sherlock Date: Sat Jun 5 13:08:29 2021 -0400 fix text color when value is set. There are still some bugs relating to multiple copies of the value being stored. commit 49c0bc15c73446259d8cc151cf52d6058644db76 Author: Kelvin Sherlock Date: Sat Jun 5 12:09:44 2021 -0400 reset all controls first. commit 059797ad85b057e296cc707b4645f839bfccac13 Author: Kelvin Sherlock Date: Sat Jun 5 10:52:06 2021 -0400 more bookmark loading. commit e5a612d9f8e7414dd15c66dbaa540b637765eeec Author: Kelvin Sherlock Date: Fri Jun 4 23:52:38 2021 -0400 bookmark - restore the software commit f9411a1e84df7bd46e352cc5ca995b585c2a0523 Author: Kelvin Sherlock Date: Fri Jun 4 23:52:25 2021 -0400 clean up software / name logic. commit f628d99e4a70d65ea8703f3c182103cd3bfea969 Author: Kelvin Sherlock Date: Fri Jun 4 00:21:08 2021 -0400 load bookmark... commit 0b248e6aad16cba848f88d294a1366cea5760aa0 Author: Kelvin Sherlock Date: Fri Jun 4 00:20:42 2021 -0400 stringValue can't be nil. commit 94aac38af45c3c95cb86b176393a469279c2539e Author: Kelvin Sherlock Date: Thu Jun 3 23:04:37 2021 -0400 add bookmark menu commit 6215a0df120b6cd97f1ffea2d2db9aaa5ae61f29 Author: Kelvin Sherlock Date: Thu Jun 3 23:03:29 2021 -0400 slot view needs to know the machine. commit d348c15dc56d9cee01e5417435dd236e0e541902 Author: Kelvin Sherlock Date: Thu Jun 3 23:02:58 2021 -0400 transformer to enable/disable control based on string length. commit e14336a0094af7aa84e91da69ff59ebb9e8eea9b Author: Kelvin Sherlock Date: Thu Jun 3 23:02:14 2021 -0400 shut up compiler warning. commit 4baf545245d8d3debf8f436b36b14cca23bf7a33 Author: Kelvin Sherlock Date: Thu Jun 3 23:01:15 2021 -0400 bookmark manager commit 0f3e6c83075a145a456728b57a499cfa35707002 Author: Kelvin Sherlock Date: Mon May 31 23:54:29 2021 -0400 more (untested) bookmark code commit 8fdb149eb38952b13c4a78d7a6940c43d90870f1 Author: Kelvin Sherlock Date: Mon May 31 16:13:43 2021 -0400 start of bookmarking support. Untested. commit 787eac87f6ac13cd677630cbaf2538cecab27b1f Author: Kelvin Sherlock Date: Mon May 31 16:12:45 2021 -0400 shut up warnings about content clipping. maybe it's a 10.11 thing. The size was chosen by interface builder. --- Ample.xcodeproj/project.pbxproj | 8 + Ample/Ample.h | 9 + Ample/AppDelegate.m | 5 + Ample/AutocompleteControl.m | 79 ++++++- Ample/Base.lproj/DiskImages.xib | 4 +- Ample/Base.lproj/LaunchWindow.xib | 132 +++++++++--- Ample/Base.lproj/MainMenu.xib | 21 ++ Ample/BookmarkManager.h | 34 +++ Ample/BookmarkManager.m | 214 ++++++++++++++++++ Ample/Info.plist | 2 +- Ample/LaunchWindowController.m | 348 +++++++++++++++++++++++++++--- Ample/MachineViewController.h | 5 + Ample/MachineViewController.m | 62 ++++++ Ample/MediaViewController.h | 6 +- Ample/MediaViewController.m | 115 +++++++++- Ample/Resources/CheatSheet.html | 13 +- Ample/Slot.h | 3 +- Ample/Slot.m | 63 +++++- Ample/SlotViewController.h | 8 +- Ample/SlotViewController.m | 70 +++++- Ample/SoftwareList.h | 4 + Ample/SoftwareList.m | 70 +++++- Ample/TableCellView.m | 2 + Ample/Transformers.h | 3 + Ample/Transformers.m | 19 ++ 25 files changed, 1205 insertions(+), 94 deletions(-) create mode 100644 Ample/BookmarkManager.h create mode 100644 Ample/BookmarkManager.m diff --git a/Ample.xcodeproj/project.pbxproj b/Ample.xcodeproj/project.pbxproj index 1d83da4..c9b292c 100644 --- a/Ample.xcodeproj/project.pbxproj +++ b/Ample.xcodeproj/project.pbxproj @@ -64,6 +64,8 @@ B6152B5B25F5B57E00605E6E /* Media.m in Sources */ = {isa = PBXBuildFile; fileRef = B6152B5925F5B57E00605E6E /* Media.m */; }; B615A99F26640940001FBF99 /* SlotView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B6E9A18125088B36005E7525 /* SlotView.xib */; }; B615A9A026640A70001FBF99 /* SlotViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B6E9A17F25088B1B005E7525 /* SlotViewController.m */; }; + B63005332666D6940014C381 /* BookmarkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B63005322666D6940014C381 /* BookmarkManager.m */; }; + B63005342666D6940014C381 /* BookmarkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B63005322666D6940014C381 /* BookmarkManager.m */; }; B6374AC4260EBBCF0045CA16 /* pty_shell.c in Sources */ = {isa = PBXBuildFile; fileRef = B6374AB6260EBB970045CA16 /* pty_shell.c */; }; B6374AC5260EBC5A0045CA16 /* pty_shell in CopyFiles */ = {isa = PBXBuildFile; fileRef = B6374ABD260EBBC90045CA16 /* pty_shell */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B6374AD1260ECB400045CA16 /* macclas2.plist in Resources */ = {isa = PBXBuildFile; fileRef = B6374AC9260ECB3F0045CA16 /* macclas2.plist */; }; @@ -375,6 +377,8 @@ B6152B5525F4549F00605E6E /* Slot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Slot.m; sourceTree = ""; }; B6152B5825F5B4F100605E6E /* Media.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Media.h; sourceTree = ""; }; B6152B5925F5B57E00605E6E /* Media.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Media.m; sourceTree = ""; }; + B63005312666D6940014C381 /* BookmarkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BookmarkManager.h; sourceTree = ""; }; + B63005322666D6940014C381 /* BookmarkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BookmarkManager.m; sourceTree = ""; }; B6374AB6260EBB970045CA16 /* pty_shell.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = pty_shell.c; sourceTree = ""; }; B6374ABD260EBBC90045CA16 /* pty_shell */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = pty_shell; sourceTree = BUILT_PRODUCTS_DIR; }; B6374AC9260ECB3F0045CA16 /* macclas2.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = macclas2.plist; sourceTree = ""; }; @@ -690,6 +694,8 @@ B608E17E2502FE0C00D53465 /* TransparentScroller.m */, B66D0FE62611386B000902F1 /* SoftwareList.m */, B66D0FE926113AA8000902F1 /* SoftwareList.h */, + B63005312666D6940014C381 /* BookmarkManager.h */, + B63005322666D6940014C381 /* BookmarkManager.m */, B6BA563A251685DA00B0C47D /* Window Controllers */, B6B9EA652506A5550080E70D /* EjectButton.h */, B6B9EA642506A5550080E70D /* EjectButton.m */, @@ -1154,6 +1160,7 @@ B60A6E1424EE0AE2004B7EEF /* FlippedView.m in Sources */, B6BA258024E99BE9005FB8FF /* AppDelegate.m in Sources */, B6004DF024FB05D600D38596 /* LogWindowController.m in Sources */, + B63005332666D6940014C381 /* BookmarkManager.m in Sources */, B66236A924FD9A34006CABD7 /* PreferencesWindowController.m in Sources */, B63C1F0F25B1447C0016A611 /* CheatSheetWindowController.m in Sources */, B64AF1F2250ECB2E00A09B9B /* DiskImagesWindowController.m in Sources */, @@ -1183,6 +1190,7 @@ B6E4B5B324FDE2670094A35C /* MediaViewController.m in Sources */, B64AF1F7250ED5E400A09B9B /* TableCellView.m in Sources */, B6E4B5B424FDE2670094A35C /* FlippedView.m in Sources */, + B63005342666D6940014C381 /* BookmarkManager.m in Sources */, B615A9A026640A70001FBF99 /* SlotViewController.m in Sources */, B6665C15265A0E3E00254939 /* AutocompleteControl.m in Sources */, B6E4B5B524FDE2670094A35C /* AppDelegate.m in Sources */, diff --git a/Ample/Ample.h b/Ample/Ample.h index 44968f5..5dd804a 100644 --- a/Ample/Ample.h +++ b/Ample/Ample.h @@ -40,4 +40,13 @@ extern NSString *kDownloadExtension; extern NSString *kDefaultDownloadURL; extern NSString *kDefaultDownloadExtension; + +@protocol Bookmark +-(BOOL)loadBookmark: (NSDictionary *)bookmark; +-(BOOL)saveBookmark: (NSMutableDictionary *)bookmark; + +-(void)willLoadBookmark: (NSDictionary *)bookmark; +-(void)didLoadBookmark: (NSDictionary *)bookmark; +@end + #endif /* Ample_h */ diff --git a/Ample/AppDelegate.m b/Ample/AppDelegate.m index 518f876..fab1b8a 100644 --- a/Ample/AppDelegate.m +++ b/Ample/AppDelegate.m @@ -13,6 +13,7 @@ #import "DiskImagesWindowController.h" #import "CheatSheetWindowController.h" #import "Transformers.h" +#import "BookmarkManager.h" #import "LogWindowController.h" @@ -40,6 +41,10 @@ RegisterTransformers(); + BookmarkManager *bm = [BookmarkManager sharedManager]; + [bm loadBookmarks]; + [bm updateMenu]; + path = [bundle pathForResource: @"Defaults" ofType: @"plist"]; dict = [NSDictionary dictionaryWithContentsOfFile: path]; diff --git a/Ample/AutocompleteControl.m b/Ample/AutocompleteControl.m index 4bc23ae..301a5e2 100644 --- a/Ample/AutocompleteControl.m +++ b/Ample/AutocompleteControl.m @@ -32,6 +32,10 @@ Todo -- @property (weak) AutocompleteControl *parent; -(void)reset; +-(void)reset: (id)value; +-(void)setItems:(NSArray> *)items; +-(void)setItems:(NSArray> *)items value: (id)value; + @end @@ -96,27 +100,37 @@ Todo -- } -(void)setStringValue:(NSString *)stringValue { - [super setStringValue: stringValue]; - if (_value && [[_value menuTitle] isEqualToString: stringValue] == NO) + [super setStringValue: stringValue ? stringValue : @""]; + if (_value && [[_value menuTitle] isEqualToString: stringValue] == NO) { + // post change notification? _value = nil; + [_menuView reset]; + } + [self fixTextColor: _editing]; // todo -- search for a matching item, update text color. } +// todo -- _menuView has second copy of value, need to update that. -(void)setObjectValue:(id)objectValue { if (_value == objectValue) return; if (![objectValue conformsToProtocol: @protocol(AutocompleteItem)]) { _value = nil; + [_menuView reset]; [super setStringValue: @""]; + [self fixTextColor: _editing]; return; } _value = objectValue; if (!_value) [super setStringValue: @""]; // else { [super setStringValue: [_value menuTitle]]; - NSArray *array = [_autocompleteDelegate autocomplete: self completionsForItem: _value]; - [_menuView setItems: array]; + [_menuView reset: _value]; + // TODO -- menu view currently uses text search. + //NSArray *array = [_autocompleteDelegate autocomplete: self completionsForItem: _value]; + //[_menuView setItems: array value: _value]; } + [self fixTextColor: NO]; } -(BOOL)valid { @@ -377,6 +391,23 @@ Todo -- return YES; } + if (commandSelector == @selector(scrollPageDown:)) { + if ([_panel isVisible]) { + [_menuView scrollPageDown: textView]; + } else { + [self updateSuggestions]; + } + return YES; + } + if (commandSelector == @selector(scrollPageUp:)) { + if ([_panel isVisible]) { + [_menuView scrollPageUp: textView]; + } else { + [self updateSuggestions]; + } + return YES; + } + //NSLog(@"%@", NSStringFromSelector(commandSelector)); return NO; } @@ -468,14 +499,24 @@ static CGFloat HeightForItems(NSUInteger count) { _items = nil; } --(void)setItems:(NSArray *)items { - if (_items == items) return; +-(void)reset: (id)value { + [self invalidateRow: _index]; + _index = -1; + _items = nil; + _value = value; +} + +-(void)setItems:(NSArray *)items value: (id )value { + if (_items == items && _value == value) return; _items = [items copy]; _index = -1; _count = [items count]; - + _value = value; - if (!_items) return; + if (!_items) { + _value = nil; + return; + } // also check enabled status.... if (_value) { @@ -502,8 +543,6 @@ static CGFloat HeightForItems(NSUInteger count) { } } - - NSInteger displayCount = MIN(_count, MAX_DISPLAY_ITEMS); CGFloat newHeight = HeightForItems(displayCount) + 8 ; // 4px top/bottom NSWindow *window = [self window]; @@ -539,6 +578,14 @@ static CGFloat HeightForItems(NSUInteger count) { //NSLog(@"%@", NSStringFromRect(wFrame)); } +-(void)setItems:(NSArray> *)items { + + if (_items == items) return; + [self setItems: items value: _value]; +} + + + -(id)itemAtPoint: (NSPoint)point indexPtr: (NSInteger *)indexPtr { NSInteger index = floor(point.y / MENU_HEIGHT); @@ -636,6 +683,18 @@ enum { [_parent selectItem: _value withSelector: _cmd]; } +-(void)scrollPageUp:(id)sender { + if (_count == 0 || _index <= 0) return; + + +} + +-(void)scrollPageDown:(id)sender { + if (_count == 0 || _index == _count - 1) return; + +} + + -(void)insertNewline:(id)sender { if (_value) { [_parent selectItem: _value withSelector: _cmd]; diff --git a/Ample/Base.lproj/DiskImages.xib b/Ample/Base.lproj/DiskImages.xib index b587a30..12088cb 100644 --- a/Ample/Base.lproj/DiskImages.xib +++ b/Ample/Base.lproj/DiskImages.xib @@ -27,7 +27,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/Ample/Base.lproj/LaunchWindow.xib b/Ample/Base.lproj/LaunchWindow.xib index 56ef02c..7c7f03d 100644 --- a/Ample/Base.lproj/LaunchWindow.xib +++ b/Ample/Base.lproj/LaunchWindow.xib @@ -8,6 +8,8 @@ + + @@ -123,7 +125,7 @@ DQ - + @@ -239,7 +241,7 @@ DQ - + @@ -247,7 +249,7 @@ DQ - + @@ -300,7 +302,7 @@ DQ - + @@ -317,7 +319,7 @@ DQ - + @@ -342,7 +344,7 @@ DQ - + @@ -378,13 +380,14 @@ DQ - - + + - + + socket.127.0.0.1:23 or /path/to/file @@ -394,37 +397,42 @@ DQ - - + + - + + /path/to/directory/ - - + + @@ -487,6 +495,78 @@ DQ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bookmark name + + + + + + + + diff --git a/Ample/Base.lproj/MainMenu.xib b/Ample/Base.lproj/MainMenu.xib index 43d1ed4..2141885 100644 --- a/Ample/Base.lproj/MainMenu.xib +++ b/Ample/Base.lproj/MainMenu.xib @@ -19,6 +19,11 @@ + + + + + @@ -389,6 +394,22 @@ + + + + + + + + + + + + + + + + diff --git a/Ample/BookmarkManager.h b/Ample/BookmarkManager.h new file mode 100644 index 0000000..971bc33 --- /dev/null +++ b/Ample/BookmarkManager.h @@ -0,0 +1,34 @@ +// +// BookmarkManager.h +// Ample +// +// Created by Kelvin Sherlock on 6/1/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#import + +@class NSMenu; + +NS_ASSUME_NONNULL_BEGIN + +@interface BookmarkManager : NSObject + +@property (weak) IBOutlet NSMenu *menu; + ++(instancetype)sharedManager; + +-(void)loadBookmarks; +-(void)updateMenu; + +-(BOOL)validateName: (NSString *)name; + +-(BOOL)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name; +-(NSDictionary *)loadBookmarkFromURL: (NSURL *)url; + +-(BOOL)saveDefault: (NSDictionary *)bookmark; +-(NSDictionary *)loadDefault; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Ample/BookmarkManager.m b/Ample/BookmarkManager.m new file mode 100644 index 0000000..3db482b --- /dev/null +++ b/Ample/BookmarkManager.m @@ -0,0 +1,214 @@ +// +// BookmarkManager.m +// Ample +// +// Created by Kelvin Sherlock on 6/1/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#import "BookmarkManager.h" +#import "Ample.h" + +@interface BookmarkManager () { + NSArray *_urls; + NSURL *_bookmarkDirectory; +} + +@end + +@implementation BookmarkManager + +static BookmarkManager *singleton = nil; + +-(void)awakeFromNib { + if (!singleton) singleton = self; +} + ++(instancetype)sharedManager { + if (!singleton) singleton = [BookmarkManager new]; + return singleton; +} + +-(instancetype)init { + if (singleton) return singleton; + return [super init]; +} + +-(NSURL *)bookmarkDirectory { + + if (_bookmarkDirectory) return _bookmarkDirectory; + NSFileManager *fm = [NSFileManager defaultManager]; + + NSURL *url = SupportDirectory(); + url = [url URLByAppendingPathComponent: @"Bookmarks"]; + NSError *error = nil; + [fm createDirectoryAtURL: url withIntermediateDirectories: YES attributes: nil error: &error]; + if (error) NSLog(@"%@", error); + _bookmarkDirectory = url; + return url; +} + +/* disallow leading . + * disallow : or / characters. + */ +-(BOOL)validateName: (NSString *)name { + + enum { kMaxLength = 128 }; + unichar buffer[kMaxLength]; + NSUInteger length = [name length]; + if (length == 0 || length > kMaxLength) return NO; + [name getCharacters: buffer range: NSMakeRange(0, length)]; + if (buffer[0] == '.') return NO; + for (unsigned i = 0; i < length; ++i) { + unichar c = buffer[i]; + if (c == ':' || c == '/') return NO; + } + return YES; +} + + +-(NSDictionary *)loadDefault { + NSURL *url = [self bookmarkDirectory]; + url = [url URLByAppendingPathComponent: @".Default"]; + + NSDictionary *d; + + if (@available(macOS 10.13, *)) { + NSError *error = nil; + d = [NSDictionary dictionaryWithContentsOfURL: url error: &error]; + if (!d) NSLog(@"Error loading %@: %@", url, error); + } else { + d = [NSDictionary dictionaryWithContentsOfURL: url]; + if (!d) NSLog(@"Error loading %@", url); + } + return d; +} + +/* save as .Default */ +-(BOOL)saveDefault: (NSDictionary *)bookmark { + + NSURL *url = [self bookmarkDirectory]; + url = [url URLByAppendingPathComponent: @".Default"]; + + NSError *error = nil; + BOOL ok = NO; + if (@available(macOS 10.13, *)) { + ok = [bookmark writeToURL: url error: &error]; + if (!ok) NSLog(@"%@", error); + } else { + ok = [bookmark writeToURL: url atomically: YES]; + } + return ok; +} + +-(BOOL)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name { + + NSError *error; + NSData *data = [NSPropertyListSerialization dataWithPropertyList: bookmark + format: NSPropertyListXMLFormat_v1_0 + options: 0 + error: &error]; + + + + NSURL *base = [self bookmarkDirectory]; + + NSURL *url = [base URLByAppendingPathComponent: name]; + + BOOL ok = [data writeToURL: url options: NSDataWritingWithoutOverwriting error: &error]; + + if (!ok) { + for (unsigned i = 1 ; i < 100; ++i) { + NSString *tmp = [name stringByAppendingFormat: @"(%d)", i]; + [base URLByAppendingPathComponent: tmp]; + + ok = [data writeToURL: url options: NSDataWritingWithoutOverwriting error: &error]; + if (ok) { + name = tmp; + break; + } + } + } + if (!ok) return NO; + + if (!_menu) return YES; // ? + + NSUInteger ix = [_urls indexOfObjectPassingTest: ^BOOL(NSURL *object, NSUInteger index, BOOL *stop){ + NSString *path = [object lastPathComponent]; + return [name caseInsensitiveCompare: path] == NSOrderedAscending; + }]; + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: name action: @selector(bookmarkMenu:) keyEquivalent: @""]; + [item setRepresentedObject: url]; + + if (ix == NSNotFound) { + _urls = [_urls arrayByAddingObject: url]; + [_menu addItem: item]; + } else { + + NSInteger n = [_menu numberOfItems]; + [_menu insertItem: item atIndex: n - [_urls count] + ix]; + NSMutableArray *tmp = [_urls mutableCopy]; + + [tmp insertObject: url atIndex: ix]; + } + + return YES; +} + +-(NSDictionary *)loadBookmarkFromURL: (NSURL *)url { + + NSDictionary *d; + + if (@available(macOS 10.13, *)) { + NSError *error = nil; + d = [NSDictionary dictionaryWithContentsOfURL: url error: &error]; + if (!d) NSLog(@"Error loading %@: %@", url, error); + } else { + d = [NSDictionary dictionaryWithContentsOfURL: url]; + if (!d) NSLog(@"Error loading %@", url); + } + return d; +} + + +-(void)loadBookmarks { + + NSURL *url = [self bookmarkDirectory]; + + NSFileManager *fm = [NSFileManager defaultManager]; + + NSError *error = nil; + + NSArray *files = [fm contentsOfDirectoryAtURL: url + includingPropertiesForKeys: nil + options: NSDirectoryEnumerationSkipsHiddenFiles + error: &error]; + + // bleh, has to create 2 new NSStrings for every comparison + files = [files sortedArrayUsingComparator: ^(NSURL *a, NSURL *b){ + NSString *aa = [a lastPathComponent]; + NSString *bb = [b lastPathComponent]; + return [aa caseInsensitiveCompare: bb]; + }]; + + + _urls = files; +} + +-(void)updateMenu { + + NSArray *menus = [_menu itemArray]; + for (NSMenuItem *item in [menus reverseObjectEnumerator]) { + if ([item tag] == 0xdeadbeef) [_menu removeItem: item]; + } + for (NSURL *url in _urls) { + NSString *title = [url lastPathComponent]; // [[url lastPathComponent] stringByDeletingPathExtension]; + + NSMenuItem *item = [_menu addItemWithTitle: title action: @selector(bookmarkMenu:) keyEquivalent: @""]; + [item setRepresentedObject: url]; + [item setTag: 0xdeadbeef]; + } +} + +@end diff --git a/Ample/Info.plist b/Ample/Info.plist index f9d9259..e2ee9c3 100644 --- a/Ample/Info.plist +++ b/Ample/Info.plist @@ -50,7 +50,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2020 Kelvin Sherlock. All rights reserved. + Copyright © 2020-2021 Kelvin Sherlock. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass diff --git a/Ample/LaunchWindowController.m b/Ample/LaunchWindowController.m index 627984e..9da68c6 100644 --- a/Ample/LaunchWindowController.m +++ b/Ample/LaunchWindowController.m @@ -15,6 +15,7 @@ #import "AutocompleteControl.h" #import "SoftwareList.h" +#import "BookmarkManager.h" #include #include @@ -23,7 +24,9 @@ static NSString *kMyContext = @"kMyContext"; static NSString *kContextMachine = @"kContextMachine"; -@interface LaunchWindowController () +@interface LaunchWindowController () { + BOOL _loadingBookmark; +} @property (strong) IBOutlet MediaViewController *mediaController; @property (strong) IBOutlet SlotViewController *slotController; @property (strong) IBOutlet MachineViewController *machineViewController; @@ -41,20 +44,22 @@ static NSString *kContextMachine = @"kContextMachine"; @property BOOL mameSquarePixels; @property BOOL mameMouse; @property BOOL mameSamples; +@property BOOL mameBGFX; @property BOOL mameAVI; @property BOOL mameWAV; @property BOOL mameVGM; +@property BOOL mameBitBanger; +@property BOOL mameShareDirectory; @property NSString *mameAVIPath; @property NSString *mameWAVPath; @property NSString *mameVGMPath; -@property NSString *mameShareDirectory; -@property NSString *mameBitBanger; +@property NSString *mameShareDirectoryPath; +@property NSString *mameBitBangerPath; @property NSInteger mameSpeed; -@property BOOL mameBGFX; @property NSInteger mameBackend; @property NSInteger mameEffects; @@ -64,6 +69,12 @@ static NSString *kContextMachine = @"kContextMachine"; @property (weak) IBOutlet AutocompleteControl *softwareListControl; @property SoftwareSet *softwareSet; @property Software *software; + + + +@property (strong) IBOutlet NSWindow *addBookmarkWindow; +@property (strong) NSString *bookmarkName; +@property (weak) IBOutlet NSTextField *bookmarkTextField; @end @interface LaunchWindowController (SoftwareList) @@ -72,17 +83,84 @@ static NSString *kContextMachine = @"kContextMachine"; @end + +@interface LaunchWindowController (Bookmark) + +-(IBAction)addBookmark:(id)sender; + +@end + +#define SIZEOF(x) (sizeof(x) / sizeof(x[0])) +static NSString *BackendStrings[] = { + @"", + @"metal", + @"opengl", +}; + +static NSString *EffectsStrings[] = { + @"-", + @"unfiltered", + @"hlsl", + @"crt-geom", + @"crt-geom-deluxe", + @"lcd-grid", +}; + + +static int BackEndIndex(NSString *str) { + if (!str) return -1; + for (int i = 1; i < SIZEOF(BackendStrings); ++i) { + if ([str isEqualToString: BackendStrings[i]]) return i; + } + return -1; +} + +static int EffectsIndex(NSString *str) { + if (!str) return -1; + for (int i = 1; i < SIZEOF(EffectsStrings); ++i) { + if ([str isEqualToString: EffectsStrings[i]]) return i; + } + return -1; +} + + @implementation LaunchWindowController -(NSString *)windowNibName { return @"LaunchWindow"; } --(void)windowWillLoad { +-(void)reset { + [self setMameMachine: nil]; + [self setMameSpeed: 1]; [self setMameBGFX: YES]; [self setMameMouse: NO]; [self setMameSamples: YES]; + [self setMameSquarePixels: NO]; + [self setMameDebug: NO]; + + [self setMameBackend: 0]; + [self setMameEffects: 0]; + + [self setMameBitBangerPath: nil]; + [self setMameShareDirectoryPath: nil]; + [self setMameAVIPath: nil]; + [self setMameWAVPath: nil]; + [self setMameVGMPath: nil]; + + [self setMameAVI: NO]; + [self setMameWAV: NO]; + [self setMameVGM: NO]; + [self setMameBitBanger: NO]; + [self setMameShareDirectory: NO]; + + [self setSoftware: nil]; +} + +-(void)windowWillLoad { + + [self reset]; } - (void)windowDidLoad { @@ -97,14 +175,16 @@ static NSString *kContextMachine = @"kContextMachine"; NSArray *keys = @[ - @"mameMachine", @"mameSquarePixels", @"mameWindowMode", + //@"mameMachine", // - handled + @"mameSquarePixels", @"mameWindowMode", @"mameMouse", @"mameSamples", @"mameDebug", @"mameSpeed", @"mameAVI", @"mameAVIPath", @"mameWAV", @"mameWAVPath", @"mameVGM", @"mameVGMPath", - @"mameShareDirectory", @"mameBitBanger", + @"mameShareDirectory", @"mameShareDirectoryPath", + @"mameBitBanger", @"mameBitBangerPath", @"mameBGFX", @"mameBackend", @"mameEffects", @"software", ]; @@ -132,8 +212,11 @@ static NSString *kContextMachine = @"kContextMachine"; -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == (__bridge void *)kMyContext) { + if (_loadingBookmark) return; [self buildCommandLine]; } else if (context == (__bridge void *)kContextMachine) { + if (_loadingBookmark) return; + NSString *machine = [_machineViewController machine]; [self setMameMachine: machine]; [_slotController setMachine: machine]; @@ -298,11 +381,8 @@ static NSString *ShellQuote(NSString *s) { [argv addObject: _mameMachine]; if (_software) { - // todo -- need to include source as well. - NSString *name = [_software name]; - if (![_softwareSet nameIsUnique: name]) - name = [_software fullName]; - [argv addObject: name]; + NSString *name = [_softwareSet nameForSoftware: _software]; + if (name) [argv addObject: name]; } // -confirm_quit? @@ -365,25 +445,12 @@ static NSString *ShellQuote(NSString *s) { if (_mameBGFX) { if (_mameBackend) { - static NSString *Names[] = { - @"-", - @"metal", - @"opengl", - }; [argv addObject: @"-bgfx_backend"]; - [argv addObject: Names[_mameBackend]]; + [argv addObject: BackendStrings[_mameBackend]]; } if (_mameEffects) { - static NSString *Names[] = { - @"-", - @"unfiltered", - @"hlsl", - @"crt-geom", - @"crt-geom-deluxe", - @"lcd-grid", - }; [argv addObject: @"-bgfx_screen_chains"]; - [argv addObject: Names[_mameEffects]]; + [argv addObject: EffectsStrings[_mameEffects]]; } } else { @@ -433,14 +500,14 @@ static NSString *ShellQuote(NSString *s) { } } - if (_mameShareDirectory && [_mameShareDirectory length]) { + if (_mameShareDirectory && [_mameShareDirectoryPath length]) { [argv addObject: @"-share_directory"]; - [argv addObject: _mameShareDirectory]; + [argv addObject: _mameShareDirectoryPath]; } - if (_mameBitBanger && [_mameBitBanger length]) { + if (_mameBitBanger && [_mameBitBangerPath length]) { [argv addObject: @"-bitbanger"]; - [argv addObject: _mameBitBanger]; + [argv addObject: _mameBitBangerPath]; } [self setCommandLine: JoinArguments(argv, nil)]; @@ -453,7 +520,12 @@ static NSString *ShellQuote(NSString *s) { if (cmd == @selector(exportShellScript:)) { return [_args count] ? YES : NO; } - return [super validateMenuItem: menuItem]; + if (cmd == @selector(addBookmark:)) { + return _mameMachine ? YES : NO; + } + + return YES; + //return [super validateMenuItem: menuItem]; // not implemented? } # pragma mark - IBActions @@ -512,6 +584,14 @@ static NSString *ShellQuote(NSString *s) { }]; } + +-(IBAction)reset:(id)sender { + + [self reset]; + [_slotController resetSlots: sender]; + [_mediaController reset: sender]; +} + @end @@ -541,3 +621,207 @@ static NSString *ShellQuote(NSString *s) { } @end + + +@implementation LaunchWindowController (Bookmark) + +-(IBAction)addBookmark:(id)sender { + + if (!_mameMachine) return; + + NSString *name = nil; + NSDictionary *d = MameMachine(_mameMachine); + if (d) name = [d objectForKey:@"description"]; + if (!name) name = _mameMachine; + + + if (_software) { + name = [name stringByAppendingFormat: @" - %@", [_software title]]; + } + [self setBookmarkName: name]; + [_bookmarkTextField selectText: nil]; + [[self window] beginSheet: _addBookmarkWindow completionHandler: nil]; +} + +-(IBAction)bookmarkCancel:(id)sender { + [[self window] endSheet: _addBookmarkWindow]; + [_addBookmarkWindow orderOut: nil]; +} + +-(IBAction)bookmarkSave:(id)sender { + + + BookmarkManager *bm = [BookmarkManager sharedManager]; + + if (![bm validateName: _bookmarkName]) { + [_bookmarkTextField selectText: nil]; + NSBeep(); + return; + } + + + //NSLog(@"%@", _bookmarkName); + NSDictionary *d = [self makeBookmark]; + //NSLog(@"%@", d); + + [bm saveBookmark: d name: _bookmarkName]; + + [[self window] endSheet: _addBookmarkWindow]; + [_addBookmarkWindow orderOut: nil]; + [self setBookmarkName: nil]; +} + + +-(IBAction)bookmarkMenu:(id)sender { + + Class StringClass = [NSString class]; + Class NumberClass = [NSNumber class]; + + NSURL *url = [sender representedObject]; + if (!url) return; + + NSDictionary *d = [NSDictionary dictionaryWithContentsOfURL: url]; + if (!d) return; // oops... + + NSString *machine = [d objectForKey: @"machine"]; + if (!machine) return; + + _loadingBookmark = YES; + [_machineViewController willLoadBookmark: d]; + [_slotController willLoadBookmark: d]; + [_mediaController willLoadBookmark: d]; + + [self reset]; + + [self setMameMachine: machine]; + [self updateSoftwareList]; + [_softwareListControl setObjectValue: nil]; // will reload the completion list. + + NSString *str; + + str = [d objectForKey: @"software"]; + if ([str isKindOfClass: StringClass]) { + Software *s = [_softwareSet softwareForName: str]; + if (s) { + [_softwareListControl setObjectValue: s]; + [self setSoftware: s]; + } + + } + + // Boolean values. + NSNumber *n; +#define _(a,b) n = [d objectForKey: a]; if ([n isKindOfClass: NumberClass]) [self b : [n boolValue]] + + _(@"debug", setMameDebug); + _(@"squarePixels", setMameSquarePixels); + _(@"mouse", setMameMouse); + _(@"samples", setMameSamples); + _(@"bgfx", setMameBGFX); + + // numeric values + // check if in range? +#undef _ + #define _(a,b) n = [d objectForKey: a]; if ([n isKindOfClass: NumberClass]) [self b : [n intValue]] + + _(@"windowMode", setMameWindowMode); + _(@"speed", setMameSpeed); + + + // string values +#undef _ + #define _(a,b) str = [d objectForKey: a]; if ([str isKindOfClass: StringClass]) [self b : str] + + _(@"shareDirectory", setMameShareDirectoryPath); + _(@"bitBanger", setMameBitBangerPath); + if ([_mameShareDirectoryPath length]) [self setMameShareDirectory: YES]; + if ([_mameBitBangerPath length]) [self setMameBitBanger: YES]; + + _(@"AVIPath", setMameAVIPath); + _(@"WAVPath", setMameWAVPath); + _(@"VGMPath", setMameVGMPath); + if ([_mameAVIPath length]) [self setMameAVI: YES]; + if ([_mameVGMPath length]) [self setMameVGM: YES]; + if ([_mameWAVPath length]) [self setMameWAV: YES]; + + + str = [d objectForKey: @"backend"]; + if ([str isKindOfClass: [NSString class]]) { + int ix = BackEndIndex(str); + if (ix >= 0) [self setMameBackend: ix]; + } + + str = [d objectForKey: @"effects"]; + if ([str isKindOfClass: [NSString class]]) { + int ix = EffectsIndex(str); + if (ix >= 0) [self setMameEffects: ix]; + } + + [_machineViewController loadBookmark: d]; + [_slotController loadBookmark: d]; + [_mediaController loadBookmark: d]; + + [_machineViewController didLoadBookmark: d]; + [_slotController didLoadBookmark: d]; + [_mediaController didLoadBookmark: d]; + + _loadingBookmark = NO; + + [self buildCommandLine]; +} + +-(NSDictionary *)makeBookmark { + + [[self window] makeFirstResponder: nil]; + + NSMutableDictionary *dict = [NSMutableDictionary new]; + + [dict setObject: _mameMachine forKey: @"machine"]; + [dict setObject: @232 forKey: @"version"]; + [_machineViewController saveBookmark: dict]; + [_slotController saveBookmark: dict]; + [_mediaController saveBookmark: dict]; + + + // Boolean values +#undef _ +#define _(v,k) [dict setObject: v ? (NSObject *)kCFBooleanTrue : (NSObject *)kCFBooleanFalse forKey: k] + + _(_mameDebug, @"debug"); + _(_mameSquarePixels, @"squarePixels"); + _(_mameMouse, @"mouse"); + _(_mameSamples, @"samples"); + _(_mameBGFX, @"bgfx"); + + // numeric values + #undef _ + #define _(v,k) [dict setObject: @(v) forKey: k] + _(_mameWindowMode, @"windowMode"); + _(_mameSpeed, @"speed"); + + // String values +#undef _ +#define _(v,k) [dict setObject: v forKey: k] + + if (_mameAVI && [_mameAVIPath length]) _(_mameAVIPath, @"AVIPath"); + if (_mameWAV && [_mameWAVPath length]) _(_mameWAVPath, @"WAVPath"); + if (_mameVGM && [_mameVGMPath length]) _(_mameVGMPath, @"VGMPath"); + + if (_mameShareDirectory && [_mameShareDirectoryPath length]) _(_mameShareDirectoryPath, @"shareDirectory"); + if (_mameBitBanger && [_mameBitBangerPath length]) _(_mameBitBangerPath, @"bitBanger"); + + + if (_software) _([_software fullName], @"software"); + + + if (_mameBackend) _(BackendStrings[_mameBackend], @"backend"); + if (_mameEffects) _(EffectsStrings[_mameEffects], @"effects"); + + + return dict; + +#undef _ +} + + +@end diff --git a/Ample/MachineViewController.h b/Ample/MachineViewController.h index 5f00a42..74aade3 100644 --- a/Ample/MachineViewController.h +++ b/Ample/MachineViewController.h @@ -8,6 +8,7 @@ #import #import +#import "Ample.h" NS_ASSUME_NONNULL_BEGIN @@ -17,4 +18,8 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface MachineViewController (Bookmark) + +@end + NS_ASSUME_NONNULL_END diff --git a/Ample/MachineViewController.m b/Ample/MachineViewController.m index a961f4b..035d1ba 100644 --- a/Ample/MachineViewController.m +++ b/Ample/MachineViewController.m @@ -110,5 +110,67 @@ return [data count]; } +@end + +@implementation MachineViewController (Bookmark) + +-(BOOL)loadBookmark: (NSDictionary *)bookmark { + + NSBrowser *browser = (NSBrowser *)[self view]; + NSString *machine = [bookmark objectForKey: @"machine"]; + + NSIndexPath *path = nil; + NSUInteger ix[2] = {0, 0 }; + for (NSDictionary *d in _data) { + + NSArray *children = [d objectForKey: @"children"]; + + for (NSDictionary *dd in children) { + NSString *value = [dd objectForKey: @"value"]; + + if ([machine isEqualToString: value]) { + path = [NSIndexPath indexPathWithIndexes: ix length: 2]; + [browser selectRow: ix[0] inColumn: 0]; + [browser selectRow: ix[1] inColumn: 1]; + + //[browser setSelectionIndexPath: path]; + return YES; + } + ++ix[1]; + } + ix[1] = 0; + + + // check parent after. + NSString *value = [d objectForKey: @"value"]; + if ([machine isEqualToString: value]) { + path = [NSIndexPath indexPathWithIndexes: ix length: 1]; + [browser selectRow: ix[0] inColumn: 0]; + // "setSelectionIndexPath: is not supported for browsers with matrix delegates." + //[browser setSelectionIndexPath: path]; + return YES; + } + + + + + + ++ix[0]; + + } + NSLog(@"MachineViewController: Unable to find %@", machine); + return NO; +} + +-(BOOL)saveBookmark: (NSMutableDictionary *)bookmark { + // machine saved in parent. + return YES; +} + +-(void)willLoadBookmark:(NSDictionary *)bookmark { +} + +-(void)didLoadBookmark:(NSDictionary *)bookmark { +} @end diff --git a/Ample/MediaViewController.h b/Ample/MediaViewController.h index e8e9ac4..d7571dc 100644 --- a/Ample/MediaViewController.h +++ b/Ample/MediaViewController.h @@ -8,6 +8,7 @@ #import #import "Media.h" +#import "Ample.h" NS_ASSUME_NONNULL_BEGIN @@ -20,8 +21,11 @@ NS_ASSUME_NONNULL_BEGIN - (IBAction)ejectAction:(id)sender; - (IBAction)pathAction:(id)sender; +-(IBAction)reset:(id)sender; -//-(void)setMedia: (NSDictionary *)media; +@end + +@interface MediaViewController (Bookmark) @end diff --git a/Ample/MediaViewController.m b/Ample/MediaViewController.m index 1827451..73f8cb3 100644 --- a/Ample/MediaViewController.m +++ b/Ample/MediaViewController.m @@ -262,6 +262,8 @@ MediaCategory *_data[CATEGORY_COUNT]; NSArray *_root; Media _media; + + BOOL _loadingBookmark; } @end @@ -345,8 +347,10 @@ enum { // todo - switch this to use removeItemsAtIndexes:inParent:withAnimation: // and insertItemsAtIndexes:inParent:withAnimation: - [_outlineView reloadData]; - [_outlineView expandItem: nil expandChildren: YES]; + if (!_loadingBookmark) { + [_outlineView reloadData]; + [_outlineView expandItem: nil expandChildren: YES]; + } } -(void)setMedia: (Media)media { @@ -372,7 +376,29 @@ x = media.name; cat = _data[index]; delta |= [cat setItemCount: x] if (delta) { [self rebuildRoot]; - [self rebuildArgs]; + if (!_loadingBookmark) [self rebuildArgs]; + } +} + +-(void)resetDiskImages { + + BOOL delta = NO; + for (unsigned j = 0; j < CATEGORY_COUNT; ++j) { + + MediaCategory *cat = _data[j]; + NSInteger count = [cat count]; + for (NSInteger i = 0; i < count; ++i) { + + MediaItem *item = [cat objectAtIndex: i]; + NSURL *url = [item url]; + if (!url) continue; + [item setUrl: nil]; + delta = YES; + } + } + if (delta) { + [self rebuildRoot]; + if (!_loadingBookmark) [self rebuildArgs]; } } @@ -641,4 +667,87 @@ static NSString *kDragType = @"private.ample.media"; [self rebuildArgs]; } + +-(IBAction)reset:(id)sender { + [self resetDiskImages]; +} + +@end + +@implementation MediaViewController (Bookmark) + +-(void)willLoadBookmark:(NSDictionary *)bookmark { + _loadingBookmark = YES; + [self resetDiskImages]; +} +-(void)didLoadBookmark:(NSDictionary *)bookmark { + _loadingBookmark = NO; + + + [self rebuildRoot]; + [self rebuildArgs]; +} + + +-(BOOL)loadBookmark: (NSDictionary *)bookmark { + + + // if order of indexes change, would need to do a version check. + + NSArray *media = [bookmark objectForKey: @"media"]; + unsigned ix = 0; + for (NSArray *a in media) { + if (ix >= CATEGORY_COUNT) { + NSLog(@"MediaViewController: too many categories."); + break; + } + MediaCategory *cat = _data[ix++]; + NSInteger count = [cat count]; + unsigned i = 0; + for (NSString *path in a) { + if (i >= count) { + NSLog(@"MediaViewController: too many files."); + break; // + } + MediaItem *item = [cat objectAtIndex: i++]; + NSURL *url = nil; + if ([path length]) + url = [NSURL fileURLWithPath: path]; + + [item setUrl: url]; + } + } + return YES; + +} + +-(BOOL)saveBookmark: (NSMutableDictionary *)bookmark { + + NSMutableArray *media = [NSMutableArray arrayWithCapacity: CATEGORY_COUNT]; + + for (unsigned ix = 0; ix < CATEGORY_COUNT; ++ix) { + + MediaCategory *cat = _data[ix]; + NSInteger count = [cat validCount]; + + NSMutableArray *array = [NSMutableArray new]; + for (NSInteger i = 0; i < count; ++i) { + + MediaItem *item = [cat objectAtIndex: i]; + NSURL *url = [item url]; + NSString *s = @""; + if (url) + s = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding]; + + [array addObject: s]; + } + [media addObject: array]; + } + + [bookmark setObject: media forKey: @"media"]; + + return YES; +} + + @end diff --git a/Ample/Resources/CheatSheet.html b/Ample/Resources/CheatSheet.html index b657a52..e0b7eb5 100644 --- a/Ample/Resources/CheatSheet.html +++ b/Ample/Resources/CheatSheet.html @@ -56,20 +56,25 @@ }