ample/Ample/BookmarkManager.m
2022-04-01 11:45:14 -04:00

652 lines
17 KiB
Objective-C

//
// BookmarkManager.m
// Ample
//
// Created by Kelvin Sherlock on 6/1/2021.
// Copyright © 2021 Kelvin Sherlock. All rights reserved.
//
#import "BookmarkManager.h"
#import "Ample.h"
#import "Bookmark.h"
#import "DiskImage.h"
#import "Menu.h"
@interface BookmarkManager () {
NSPersistentStoreCoordinator *_psc;
NSManagedObjectContext *_moc;
NSManagedObjectModel *_mom;
NSPersistentStore *_store;
NSFetchRequest *_defaultRequest;
NSURL *_bookmarkDirectory;
NSArrayController *_items;
NSUInteger _newMenuGeneration;
NSUInteger _currentMenuGeneration;
Bookmark *_currentBookmark;
}
@end
@interface BookmarkManager (MenuDelegate) <NSMenuDelegate>
@end
@implementation BookmarkManager
static BookmarkManager *singleton = nil;
-(void)awakeFromNib {
if (!singleton) {
singleton = self;
if (!_items) [self initMenus];
}
}
+(instancetype)sharedManager {
if (!singleton) singleton = [BookmarkManager new];
return singleton;
}
-(instancetype)init {
if (singleton) return singleton;
if ((self = [super init])) {
[self initCoreData];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver: self selector: @selector(willTerminate:) name: NSApplicationWillTerminateNotification object: nil];
[nc addObserver: self selector: @selector(diskImageAdded:) name: kNotificationDiskImageAdded object: nil];
_newMenuGeneration = 1;
_currentMenuGeneration = 0;
}
//singleton = self;
return self;
}
-(NSManagedObjectContext *)managedObjectContext {
return _moc;
}
-(void)initCoreData {
NSError *error;
BOOL new = NO;
NSBundle *bundle = [NSBundle mainBundle];
NSURL *url = [bundle URLForResource: @"Ample" withExtension: @"momd"];
_mom = [[NSManagedObjectModel alloc] initWithContentsOfURL: url];
_psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: _mom];
_moc = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSMainQueueConcurrencyType];
[_moc setPersistentStoreCoordinator: _psc];
//[_moc setMergePolicy: [NSMergePolicy rollbackMergePolicy]];
url = SupportDirectory();
#if 0
url = [url URLByAppendingPathComponent: @"Ample.db"];
if (![url checkResourceIsReachableAndReturnError: &error])
new = YES;
_store = [_psc addPersistentStoreWithType: NSSQLiteStoreType
configuration: nil
URL: url
options: nil
error: &error];
#else
url = [url URLByAppendingPathComponent: @"Ample.xml"];
if (![url checkResourceIsReachableAndReturnError: &error])
new = YES;
_store = [_psc addPersistentStoreWithType: NSXMLStoreType
configuration: nil
URL: url
options: nil
error: &error];
#endif
_defaultRequest = [Bookmark fetchRequest];
[_defaultRequest setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
if (new) {
[self convertLegacyBookmarks];
[self convertLegacyDiskImages];
}
}
-(void)willTerminate: (NSNotification *)notification {
NSError *error;
if (![_moc save: &error]) {
NSLog(@"%@", error);
}
}
-(void)convertLegacyBookmarks {
//NSEntityDescription *e;
//e = [NSEntityDescription entityForName: @"Bookmark" inManagedObjectContext: moc];
NSURL *url = [self bookmarkDirectory];
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *files = [fm contentsOfDirectoryAtURL: url
includingPropertiesForKeys: nil
options: NSDirectoryEnumerationSkipsHiddenFiles
error: &error];
NSDate *now = [NSDate date];
for (NSURL *url in files) {
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL: url];
//NSData *data = [NSPropertyListSerialization dataWithPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 options: 0 error: &error];
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
[b setName: [url lastPathComponent]];
[b setDictionary: dict];
//[b setData: data];
[b setMachine: [dict objectForKey: @"machine"]];
[b setCreated: now];
}
// default...
url = [url URLByAppendingPathComponent: @".Default"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL: url];
if (dict) {
//NSData *data = [NSPropertyListSerialization dataWithPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 options: 0 error: &error];
NSString *name = [self uniqueBookmarkName: @"Default"];
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
[b setName: name];
[b setAutomatic: YES];
[b setDictionary: dict];
//[b setData: data];
[b setMachine: [dict objectForKey: @"machine"]];
[b setCreated: now];
}
if (![_moc save: &error]) {
NSLog(@"%@", error);
}
}
-(void)convertLegacyDiskImages {
NSError *error;
NSURL *sd = SupportDirectory();
NSURL *url = [sd URLByAppendingPathComponent: @"RecentDiskImages.plist"];
NSArray *array = [NSArray arrayWithContentsOfURL: url];
if (!array) return;
for (NSDictionary *d in array) {
NSManagedObject *o = [NSEntityDescription insertNewObjectForEntityForName: @"DiskImage" inManagedObjectContext: _moc];
[o setValue: [d objectForKey: @"date"] forKey: @"added"];
[o setValue: [d objectForKey: @"date"] forKey: @"accessed"];
[o setValue: [d objectForKey: @"path"] forKey: @"path"];
[o setValue: [d objectForKey: @"size"] forKey: @"size"];
}
if (![_moc save: &error]) {
NSLog(@"%@", error);
}
}
-(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;
}
-(Bookmark *)defaultBookmark {
NSFetchRequest *req;
NSError *error;
NSArray *array;
req = [Bookmark fetchRequest];
[req setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
array = [_moc executeFetchRequest: req error: &error];
return [array firstObject];
}
-(NSDictionary *)loadDefault {
Bookmark *b = [self defaultBookmark];
return [b dictionary];
}
/* save as .Default */
-(NSError *)saveDefault: (NSDictionary *)bookmark {
return nil;
#if 0
/* check if it already exists */
NSFetchRequest *req;
NSError *error;
NSArray *array;
Bookmark *b;
NSDate *now = [NSDate date];
BOOL ok;
req = [[NSFetchRequest alloc] initWithEntityName: @"Default"];
array = [_moc executeFetchRequest: req error: &error];
b = [array firstObject];
if (b) {
[b setModified: now];
} else {
b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Default" inManagedObjectContext: _moc];
[b setName: @"Default"];
[b setCreated: now];
}
[b setDictionary: bookmark];
[b setMachine: [bookmark objectForKey: @"machine"]];
ok = [_moc save: &error];
if (!ok) NSLog(@"%@", error);
return error;
#if 0
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;
#endif
#endif
}
-(NSError *)setAutomatic: (Bookmark *)bookmark {
NSError *error = nil;
NSFetchRequest *req = [Bookmark fetchRequest];
[req setPredicate: [NSPredicate predicateWithFormat: @"automatic == TRUE"]];
NSArray *array = [_moc executeFetchRequest: req error: &error];
for (Bookmark *b in array) {
if (b != bookmark) [b setAutomatic: NO];
}
[bookmark setAutomatic: YES];
if (error) return error;
[_moc save: &error];
return error;
}
-(NSError *)saveBookmark: (NSDictionary *)bookmark name: (NSString *)name automatic: (BOOL)automatic {
NSDate *now = [NSDate date];
NSError *error;
BOOL ok;
Bookmark *b = (Bookmark *)[NSEntityDescription insertNewObjectForEntityForName: @"Bookmark" inManagedObjectContext: _moc];
[b setName: name];
[b setDictionary: bookmark];
[b setMachine: [bookmark objectForKey: @"machine"]];
[b setCreated: now];
[b setAutomatic: automatic];
ok = [b validateForInsert: &error];
if (!ok) {
// will be useful, eg "name is too long"
// keys: NSValidationErrorObject, NSLocalizedDescription, NSValidationErrorKey, NSValidationErrorValue
//NSLog(@"%@", error);
[_moc deleteObject: b];
return error;
}
ok = [_moc save: &error];
if (!ok) {
//NSLog(@"%@", error);
[_moc deleteObject: b];
error = [NSError errorWithDomain: @"Ample" code: 0 userInfo: @{ NSLocalizedDescriptionKey: @"Duplicate name" }];
return error;
}
if (automatic) {
[self setAutomatic: b];
}
return nil;
}
-(void)loadBookmarks {
NSSortDescriptor *s = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES selector: @selector(caseInsensitiveCompare:)];
_items = [NSArrayController new];
[_items setManagedObjectContext: _moc];
[_items setAvoidsEmptySelection: NO];
[_items setAutomaticallyPreparesContent: YES];
[_items setAutomaticallyRearrangesObjects: YES];
[_items setEntityName: @"Bookmark"];
[_items setSortDescriptors: @[ s ]];
[_items fetch: nil];
}
/* extract the number from a trailing " (%d)" */
static int extract_number(NSString *s, NSInteger offset) {
unichar buffer[32];
NSInteger len = [s length] - offset;
unichar c;
int i;
int n = 0;
if (len < 4) return -1; /* " (1)"*/
if (len > 8) return -1; /* " (99999)" */
NSRange r = NSMakeRange(offset, len);
[s getCharacters: buffer range: r];
buffer[len] = 0;
i = 0;
if (buffer[i++] != ' ') return -1;
if (buffer[i++] != '(') return -1;
c = buffer[i++];
if (c < '1' || c > '9') return -1;
n = c - '0';
for (;;) {
c = buffer[i];
if (c < '0' || c > '9') break;
n = n * 10 + (c - '0');
++i;
}
if (buffer[i++] != ')') return -1;
if (buffer[i++] != 0) return -1;
return n;
}
-(NSString *)uniqueBookmarkName: (NSString *)name {
NSInteger length = [name length];
NSError *error = nil;
NSPredicate *p = [NSPredicate predicateWithFormat: @"name BEGINSWITH %@", name];
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName: @"Bookmark"];
[req setPredicate: p];
NSArray *array = [_moc executeFetchRequest: req error: &error];
if (![array count]) return name;
uint64_t bits = 1; /* mark 0 as unavailable */
NSInteger max = 0;
BOOL exact = NO;
for (Bookmark *b in array) {
NSString *s = [b name];
if ([name isEqualToString: s]) {
exact = YES;
continue;
}
int n = extract_number(s, length);
if (n < 1) continue;
if (n > max) max = n;
if (n < 64)
bits |= (1 << n);
}
if (!exact) return name;
if (bits == (uint64_t)-1) {
return [name stringByAppendingFormat: @" (%u)", (int)(max + 1)];
}
#if 1
int ix = 0;
while (bits & 0x01) {
++ix;
bits >>= 1;
}
#else
// this doesn't work correctly.
int ix = __builtin_ffsll(~bits);
#endif
return [name stringByAppendingFormat: @" (%u)", ix];
}
-(BOOL)addDiskImage: (NSObject *)pathOrURL {
NSError *error;
NSString *path = nil;
NSURL *url = nil;
if ([pathOrURL isKindOfClass: [NSString class]]) {
path = (NSString *)pathOrURL;
} else if ([pathOrURL isKindOfClass: [NSURL class]]){
url = (NSURL *)pathOrURL;
path = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding];
}
if (!path) return NO;
NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attr = [fm attributesOfItemAtPath: path error: &error];
if (error) {
NSLog(@"%@ : %@", path, error);
return NO;
}
NSNumber *size = [attr objectForKey: NSFileSize];
NSDate *now = [NSDate date];
NSPredicate *p = [NSPredicate predicateWithFormat: @"path = %@", path];
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName: @"DiskImage"];
[req setPredicate: p];
NSArray *array = [_moc executeFetchRequest: req error: &error];
BOOL found = 0;
for (NSManagedObject *o in array) {
found = YES;
[o setValue: now forKey: @"accessed"];
}
if (found) return NO;
DiskImage *o = [NSEntityDescription insertNewObjectForEntityForName: @"DiskImage" inManagedObjectContext: _moc];
[o setPath: path];
[o setAdded: now];
[o setAccessed: now];
[o setSize: [size longLongValue]];
[o updatePath];
if (![_moc save: &error]) {
NSLog(@"%@", error);
[_moc deleteObject: o];
}
return YES;
}
-(void)diskImageAdded: (NSNotification *)notification {
NSURL *url = [notification object];
if (url) [self addDiskImage: url];
}
static NSString *kMenuContext = @"";
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == (__bridge void * _Nullable)(kMenuContext)) {
//NSLog(@"observeValueForKeyPath %@", keyPath);
_newMenuGeneration++;
return;
}
[super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
}
-(void)initMenus {
if (!_items) {
[self loadBookmarks];
[_items addObserver: self forKeyPath: @"arrangedObjects.name" options: 0 context: (__bridge void * _Nullable)(kMenuContext)];
[_items addObserver: self forKeyPath: @"arrangedObjects.automatic" options: 0 context: (__bridge void * _Nullable)(kMenuContext)];
}
}
-(IBAction)bookmarkMenu:(id)sender
{
}
-(Bookmark *)currentBookmark {
return _currentBookmark;
}
-(void)setCurrentBookmark:(Bookmark *)currentBookmark {
if (currentBookmark == _currentBookmark) return;
_currentBookmark = currentBookmark;
_newMenuGeneration++;
}
-(void)menuNeedsUpdate:(NSMenu *)menu {
if (_currentMenuGeneration == _newMenuGeneration) return;
_currentMenuGeneration = _newMenuGeneration;
if (_currentBookmark && _updateMenuItem) {
NSString *title = [NSString stringWithFormat: @"Update “%@”", [_currentBookmark name]];
[_updateMenuItem setHidden: NO];
[_updateMenuItem setTitle: title];
[_updateMenuItem setRepresentedObject: _currentBookmark];
} else {
[_updateMenuItem setHidden: YES];
[_updateMenuItem setRepresentedObject: nil];
}
NSArray *menus = [menu itemArray];
for (NSMenuItem *item in [menus reverseObjectEnumerator]) {
if ([item tag] == 0xdeadbeef) [_menu removeItem: item];
}
NSArray *array = [_items arrangedObjects];
for (Bookmark *b in array) {
NSString *title = [b name];
NSMenuItem *item = [menu addItemWithTitle: title action: @selector(bookmarkMenu:) keyEquivalent: @""];
[item setRepresentedObject: b];
[item setTag: 0xdeadbeef];
if ([b automatic]) {
[item setOnStateImage: [NSImage imageNamed: NSImageNameStatusAvailable]];
[item setState: NSOnState];
}
//if ([b automatic]) [item setAttributedTitle: ItalicMenuString([b name])];
//[item setState: [b automatic] ? NSMixedState : NSOffState];
}
}
@end
/* MacOS 12.1+ doesn't like class clusters in nibs -
[General] This coder is expecting the replaced object 0x600000938f60 to be returned from
NSClassSwapper.initWithCoder instead of <BookmarkManager: 0x600000905da0>
*/
@interface BookmarkManagerProxy : NSProxy {
BookmarkManager *_target;
}
@end
@implementation BookmarkManagerProxy
-(id)init {
_target = [BookmarkManager sharedManager];
return self;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_target methodSignatureForSelector: sel];
}
+(BOOL)respondsToSelector:(SEL)aSelector {
return [BookmarkManager respondsToSelector: aSelector];
}
-(void)forwardInvocation:(NSInvocation *)invocation {
if ([_target respondsToSelector: [invocation selector]]) {
[invocation invokeWithTarget: _target];
} else {
[super forwardInvocation: invocation];
}
}
@end