From af255a43f35d2e05c573773aca66fbd71babfdfb Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 25 Nov 2025 14:02:40 -0500 Subject: [PATCH] Mostly introduce exposed path selection for the Enterprise. --- .../Machine/StaticAnalyser/CSStaticAnalyser.h | 3 +- .../StaticAnalyser/CSStaticAnalyser.mm | 43 ++++--- .../Base.lproj/MachinePicker.xib | 109 +++++++++++------- .../MachinePicker/MachinePicker.swift | 45 +++++++- Storage/FileBundle/FileBundle.cpp | 21 +++- 5 files changed, 159 insertions(+), 62 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 40b8bd850..0d954703e 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -195,7 +195,8 @@ typedef int Kilobytes; speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion - dos:(CSMachineEnterpriseDOS)dos; + dos:(CSMachineEnterpriseDOS)dos + exposedLocalPath:(nullable NSURL *)path; - (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 9f42ae090..19987ec94 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -28,6 +28,8 @@ #include "Analyser/Static/ZX8081/Target.hpp" #include "Analyser/Static/ZXSpectrum/Target.hpp" +#include "Storage/FileBundle/FileBundle.hpp" + #import "Clock_Signal-Swift.h" namespace { @@ -92,25 +94,33 @@ struct PermissionDelegate: public Storage::FileBundle::FileBundle::PermissionDel request.canChooseDirectories = YES; [request setDirectoryURL:[url URLByDeletingLastPathComponent]]; - request.accessoryView = [NSTextField labelWithString:[NSString stringWithFormat: - @"Clock Signal cannot access your files without explicit permission but " - @"%s is trying to use additional files in its folder.\n" - @"Please select 'Grant Permission' if you are willing to let it to do so.", - bundle.key_file()->c_str() - ]]; + request.accessoryView = [NSTextField labelWithString:[&] { + const auto key_file = bundle.key_file(); + + if(key_file) { + return [NSString stringWithFormat: + @"Clock Signal cannot access your files without explicit permission but " + @"%s is trying to use additional files in its folder.\n" + @"Please select 'Grant Permission' if you are willing to let it to do so.", + key_file->c_str() + ]; + } else { + assert(bundle.base_path().has_value()); + return [NSString stringWithFormat: + @"Clock Signal cannot access your files without explicit permission but " + @"your emulated machine is trying to use additional files from %s.\n" + @"Please select 'Grant Permission' if you are willing to let it to do so.", + bundle.base_path()->c_str() + ]; + } + }()]; + request.accessoryViewDisclosed = YES; [request runModal]; selectedURL = request.URL; }); - // Possibly substitute the base path, in case the one returned - // is an indirection out of the sandbox. -// if(![selectedURL isEqual:[url URLByDeletingLastPathComponent]]) { -// NSLog(@"Substituting base path: %@", selectedURL.path); -// bundle.set_base_path(std::string(selectedURL.path.UTF8String)); -// } - // Store bookmark data for potential later retrieval. // That amounts to this application remembering the user's permission. NSError *error; @@ -367,6 +377,7 @@ PermissionDelegate permission_delegate; exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos + exposedLocalPath:(nullable NSURL *)path { self = [super init]; if(self) { @@ -407,6 +418,12 @@ PermissionDelegate permission_delegate; case CSMachineEnterpriseDOSNone: target->dos = Target::DOS::None; break; } + if(path) { + const auto bundle = std::make_shared(path.path.UTF8String); + bundle->set_permission_delegate(&permission_delegate); + target->media.file_bundles.push_back(std::move(bundle)); + } + _targets.push_back(std::move(target)); } return self; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index bba1f8fc6..435535d0f 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -49,7 +49,7 @@ Gw - + @@ -103,7 +103,7 @@ Gw - + @@ -134,7 +134,7 @@ Gw - + @@ -142,7 +142,7 @@ Gw - + @@ -201,7 +201,7 @@ Gw - + @@ -225,7 +225,7 @@ Gw - + @@ -233,7 +233,7 @@ Gw - + @@ -301,7 +301,7 @@ Gw - + @@ -309,7 +309,7 @@ Gw - + @@ -366,7 +366,7 @@ Gw - + @@ -388,7 +388,7 @@ Gw - + @@ -447,7 +447,7 @@ Gw - + @@ -572,11 +572,11 @@ Gw - + - + @@ -590,7 +590,7 @@ Gw - + @@ -603,7 +603,7 @@ Gw - + @@ -617,7 +617,7 @@ Gw - + @@ -632,7 +632,7 @@ Gw - + @@ -644,50 +644,71 @@ Gw - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + @@ -695,11 +716,14 @@ Gw - + + + + @@ -713,6 +737,7 @@ Gw + @@ -722,7 +747,7 @@ Gw - + @@ -782,7 +807,7 @@ Gw - + @@ -790,7 +815,7 @@ Gw - + @@ -845,7 +870,7 @@ Gw - + @@ -883,7 +908,7 @@ Gw - + @@ -925,7 +950,7 @@ Gw - + @@ -933,7 +958,7 @@ Gw - + @@ -1005,7 +1030,7 @@ Gw - + @@ -1013,7 +1038,7 @@ Gw - + @@ -1066,7 +1091,7 @@ Gw - + @@ -1114,7 +1139,7 @@ Gw - + @@ -1155,7 +1180,7 @@ Gw - + @@ -1222,7 +1247,9 @@ Gw + + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index 769bf1541..737649b25 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -15,7 +15,7 @@ import Cocoa // in the interface builder easier. // // I accept that I'll have to rethink this again if the machine list keeps growing. -class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { +class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSPathControlDelegate { @IBOutlet var machineSelector: NSTabView! @IBOutlet var machineNameTable: NSTableView! @@ -58,6 +58,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { @IBOutlet var enterpriseBASICButton: NSPopUpButton! @IBOutlet var enterpriseDOSButton: NSPopUpButton! + @IBOutlet var enterpriseExposePathButton: NSButton! + @IBOutlet var enterprisePathControl: NSPathControl! + // MARK: - Macintosh properties @IBOutlet var macintoshModelTypeButton: NSPopUpButton! @@ -157,6 +160,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { enterpriseBASICButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.enterpriseBASICVersion")) enterpriseDOSButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.enterpriseDOS")) + enterpriseExposePathButton.state = standardUserDefaults.bool(forKey: "new.enterpriseExposeLocalPath") ? .on : .off + establishPathControl(enterprisePathControl, userDefaultsKey: "new.enterpriseExposedLocalPath") + // Macintosh settings macintoshModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel")) @@ -238,6 +244,9 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { standardUserDefaults.set(enterpriseBASICButton.selectedTag(), forKey: "new.enterpriseBASICVersion") standardUserDefaults.set(enterpriseDOSButton.selectedTag(), forKey: "new.enterpriseDOS") + standardUserDefaults.set(enterpriseExposePathButton.state == .on, forKey: "new.enterpriseExposeLocalPath") + storePathControl(enterprisePathControl, userDefaultsKey: "new.enterpriseExposedLocalPath") + // Macintosh settings standardUserDefaults.set(macintoshModelTypeButton.selectedTag(), forKey: "new.macintoshModel") @@ -430,7 +439,8 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { speed: speed, exosVersion: exos, basicVersion: basic, - dos: dos + dos: dos, + exposedLocalPath: enterpriseExposePathButton.state == .on ? enterprisePathControl.url : nil ) case "mac": @@ -532,4 +542,35 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { default: return CSStaticAnalyser() } } + + // MARK: - NSPathControlDelegate (and paths in general) + + func pathControl(_ pathControl: NSPathControl, willDisplay openPanel: NSOpenPanel) { + openPanel.canChooseFiles = false + openPanel.canChooseDirectories = true + } + + func pathControl(_ pathControl: NSPathControl, validateDrop info: any NSDraggingInfo) -> NSDragOperation { + // TODO: accept if the pasteboard contains NSURLPboardType or NSFilenamesPboardType, + // and if the referenced file is a directory. + + return NSDragOperation.link + } + + func establishPathControl(_ pathControl: NSPathControl, userDefaultsKey: String) { + pathControl.url = FileManager.default.homeDirectoryForCurrentUser + if let bookmarkData = UserDefaults.standard.data(forKey: userDefaultsKey) { + var isStale: Bool = false + if let url = try? URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale) { + enterprisePathControl.url = url + } + } + } + + func storePathControl(_ pathControl: NSPathControl, userDefaultsKey: String) { + let url = pathControl.url + if let bookmarkData = try? url?.bookmarkData(options: [.withSecurityScope]) { + UserDefaults.standard.set(bookmarkData, forKey: userDefaultsKey) + } + } } diff --git a/Storage/FileBundle/FileBundle.cpp b/Storage/FileBundle/FileBundle.cpp index e5bbf54bf..eab99efe3 100644 --- a/Storage/FileBundle/FileBundle.cpp +++ b/Storage/FileBundle/FileBundle.cpp @@ -9,20 +9,31 @@ #include "FileBundle.hpp" #include +#include using namespace Storage::FileBundle; LocalFSFileBundle::LocalFSFileBundle(const std::string &to_contain) { - const auto last_separator = to_contain.find_last_of("/\\"); - if(last_separator == std::string::npos) { - key_file_ = to_contain; + struct stat stats; + stat(to_contain.c_str(), &stats); + + if(S_ISDIR(stats.st_mode)) { + set_base_path(to_contain); } else { - base_path_ = to_contain.substr(0, last_separator + 1); - key_file_ = to_contain.substr(last_separator + 1); + const auto last_separator = to_contain.find_last_of("/\\"); + if(last_separator == std::string::npos) { + key_file_ = to_contain; + } else { + base_path_ = to_contain.substr(0, last_separator + 1); + key_file_ = to_contain.substr(last_separator + 1); + } } } std::optional LocalFSFileBundle::key_file() const { + if(key_file_.empty()) { + return std::nullopt; + } return key_file_; }