1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-19 08:31:11 +00:00

Merge pull request #939 from TomHarte/DragAndDropState

Accept insertion of state snapshots into existing windows
This commit is contained in:
Thomas Harte 2021-05-16 20:47:36 -04:00 committed by GitHub
commit 488c2aed51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 105 deletions

View File

@ -74,7 +74,7 @@
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--new=amstradcpc" argument = "--new=amstradcpc"
isEnabled = "NO"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col&quot;" argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col&quot;"
@ -106,11 +106,11 @@
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages" argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
isEnabled = "NO"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--help" argument = "--help"
isEnabled = "YES"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--model=cpc6128" argument = "--model=cpc6128"

View File

@ -87,6 +87,14 @@ class MachineDocument:
} }
} }
private func dismissPanels() {
activityPanel?.setIsVisible(false)
activityPanel = nil
optionsPanel?.setIsVisible(false)
optionsPanel = nil
}
override func close() { override func close() {
// Close any dangling sheets. // Close any dangling sheets.
// //
@ -105,11 +113,7 @@ class MachineDocument:
machine?.stop() machine?.stop()
// Dismiss panels. // Dismiss panels.
activityPanel?.setIsVisible(false) dismissPanels()
activityPanel = nil
optionsPanel?.setIsVisible(false)
optionsPanel = nil
// End the update cycle. // End the update cycle.
actionLock.lock() actionLock.lock()
@ -137,19 +141,23 @@ class MachineDocument:
func configureAs(_ analysis: CSStaticAnalyser) { func configureAs(_ analysis: CSStaticAnalyser) {
self.machineDescription = analysis self.machineDescription = analysis
actionLock.lock()
drawLock.lock()
let missingROMs = NSMutableArray() let missingROMs = NSMutableArray()
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) { if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
self.machine = machine self.machine = machine
setupActivityDisplay()
machine.setVolume(userDefaultsVolume()) machine.setVolume(userDefaultsVolume())
setupMachineOutput() setupMachineOutput()
} else { } else {
// Store the selected machine and list of missing ROMs, and // Store the selected machine and list of missing ROMs, and
// show the missing ROMs dialogue. // show the missing ROMs dialogue.
self.missingROMs = missingROMs.map({$0 as! CSMissingROM}) self.missingROMs = missingROMs.map({$0 as! CSMissingROM})
requestRoms() requestRoms()
} }
actionLock.unlock()
drawLock.unlock()
} }
enum InteractionMode { enum InteractionMode {
@ -200,6 +208,9 @@ class MachineDocument:
let aspectRatio = self.aspectRatio() let aspectRatio = self.aspectRatio()
machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
// Get rid of all existing accessory panels.
dismissPanels()
// Attach an options panel if one is available. // Attach an options panel if one is available.
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil) Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
@ -208,11 +219,10 @@ class MachineDocument:
showOptions(self) showOptions(self)
} }
machine.delegate = self // Create and populate an activity display if required.
setupActivityDisplay()
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; machine.delegate = self
// hence the full setup of the best-effort updater prior to setting self as a delegate.
// scanTargetView.delegate = self
scanTargetView.responderDelegate = self scanTargetView.responderDelegate = self
// If this machine has a mouse, enable mouse capture; also indicate whether usurption // If this machine has a mouse, enable mouse capture; also indicate whether usurption
@ -252,7 +262,7 @@ class MachineDocument:
let isStereo = self.machine.isStereo let isStereo = self.machine.isStereo
if selectedSamplingRate > 0 { if selectedSamplingRate > 0 {
// [Re]create the audio queue only if necessary. // [Re]create the audio queue only if necessary.
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate { if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate || self.audioQueue != self.machine.audioQueue {
self.machine.audioQueue = nil self.machine.audioQueue = nil
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo) self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo)
self.audioQueue.delegate = self self.audioQueue.delegate = self
@ -280,8 +290,7 @@ class MachineDocument:
/// Delegate message to receive drag and drop files. /// Delegate message to receive drag and drop files.
final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) { final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) {
let mediaSet = CSMediaSet(fileAt: URL) insertFile(URL)
mediaSet.apply(to: self.machine)
} }
/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process /// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process
@ -292,10 +301,27 @@ class MachineDocument:
openPanel.beginSheetModal(for: self.windowControllers[0].window!) { (response) in openPanel.beginSheetModal(for: self.windowControllers[0].window!) { (response) in
if response == .OK { if response == .OK {
for url in openPanel.urls { for url in openPanel.urls {
let mediaSet = CSMediaSet(fileAt: url) self.insertFile(url)
}
}
}
}
private func insertFile(_ URL: URL) {
// Try to insert media.
let mediaSet = CSMediaSet(fileAt: URL)
if !mediaSet.empty {
mediaSet.apply(to: self.machine) mediaSet.apply(to: self.machine)
return
} }
}
// Failing that see whether a new machine is required.
// TODO.
if let newMachine = CSStaticAnalyser(fileAt: URL) {
machine?.stop()
self.interactionMode = .notStarted
self.scanTargetView.willChangeScanTargetOwner()
configureAs(newMachine)
} }
} }

View File

@ -114,6 +114,8 @@ typedef int Kilobytes;
- (instancetype)initWithFileAtURL:(NSURL *)url; - (instancetype)initWithFileAtURL:(NSURL *)url;
- (void)applyToMachine:(CSMachine *)machine; - (void)applyToMachine:(CSMachine *)machine;
@property(nonatomic, readonly) BOOL empty;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@ -309,4 +309,8 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
[machine applyMedia:_media]; [machine applyMedia:_media];
} }
- (BOOL)empty {
return _media.empty();
}
@end @end

View File

@ -22,4 +22,6 @@
- (nonnull NSBitmapImageRep *)imageRepresentation; - (nonnull NSBitmapImageRep *)imageRepresentation;
- (void)willChangeOwner;
@end @end

View File

@ -1143,6 +1143,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
return &_scanTarget; return &_scanTarget;
} }
- (void)willChangeOwner {
self.scanTarget->will_change_owner();
}
- (NSBitmapImageRep *)imageRepresentation { - (NSBitmapImageRep *)imageRepresentation {
// Create an NSBitmapRep as somewhere to copy pixel data to. // Create an NSBitmapRep as somewhere to copy pixel data to.
NSBitmapImageRep *const result = NSBitmapImageRep *const result =

View File

@ -162,4 +162,10 @@
*/ */
@property(nonatomic, readonly, nonnull) CSScanTarget *scanTarget; @property(nonatomic, readonly, nonnull) CSScanTarget *scanTarget;
/*!
Indicates that the enclosed scan target is about to be handed off to a new owner;
exactly identical to calling scanTarget.will_change_owner().
*/
- (void)willChangeScanTargetOwner;
@end @end

View File

@ -129,6 +129,10 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
return _scanTarget; return _scanTarget;
} }
- (void)willChangeScanTargetOwner {
[_scanTarget willChangeOwner];
}
- (void)updateBacking { - (void)updateBacking {
[_scanTarget updateFrameBuffer]; [_scanTarget updateFrameBuffer];
} }

View File

@ -470,13 +470,56 @@ class DynamicWindowTitler {
update_window_title(); update_window_title();
} }
void set_file_name(const std::string &name) {
file_name_ = name;
update_window_title();
}
private: private:
void update_window_title() { void update_window_title() {
SDL_SetWindowTitle(window_, window_title().c_str()); SDL_SetWindowTitle(window_, window_title().c_str());
} }
bool mouse_is_captured_ = false; bool mouse_is_captured_ = false;
SDL_Window *window_ = nullptr; SDL_Window *window_ = nullptr;
const std::string file_name_; std::string file_name_;
};
/*!
Provides a wrapper for SDL_Joystick pointers that can keep track
of historic hat values.
*/
class SDLJoystick {
public:
SDLJoystick(SDL_Joystick *joystick) : joystick_(joystick) {
hat_values_.resize(SDL_JoystickNumHats(joystick));
}
~SDLJoystick() {
SDL_JoystickClose(joystick_);
}
/// @returns The underlying SDL_Joystick.
SDL_Joystick *get() {
return joystick_;
}
/// @returns A reference to the storage for the previous state of hat @c c.
Uint8 &last_hat_value(int c) {
return hat_values_[c];
}
/// @returns The logic OR of all stored hat states.
Uint8 hat_values() {
Uint8 value = 0;
for(const auto hat_value: hat_values_) {
value |= hat_value;
}
return value;
}
private:
SDL_Joystick *joystick_;
std::vector<Uint8> hat_values_;
}; };
} }
@ -723,7 +766,7 @@ int main(int argc, char *argv[]) {
if(!rom.descriptive_name.empty()) { if(!rom.descriptive_name.empty()) {
std::cerr << rom.descriptive_name << "; "; std::cerr << rom.descriptive_name << "; ";
} }
std::cerr << "accepted crc32s: "; std::cerr << "usual crc32s: ";
bool is_first = true; bool is_first = true;
for(const auto crc32: rom.crc32s) { for(const auto crc32: rom.crc32s) {
if(!is_first) std::cerr << ", "; if(!is_first) std::cerr << ", ";
@ -791,10 +834,6 @@ int main(int argc, char *argv[]) {
SDL_StartTextInput(); SDL_StartTextInput();
} }
// Wire up the best-effort updater, its delegate, and the speaker delegate.
machine_runner.machine = machine.get();
machine_runner.machine_mutex = &machine_mutex;
// Ensure all media is inserted, if this machine accepts it. // Ensure all media is inserted, if this machine accepts it.
{ {
auto media_target = machine->media_target(); auto media_target = machine->media_target();
@ -844,6 +883,15 @@ int main(int argc, char *argv[]) {
// Setup output, assuming a CRT machine for now, and prepare a best-effort updater. // Setup output, assuming a CRT machine for now, and prepare a best-effort updater.
Outputs::Display::OpenGL::ScanTarget scan_target(target_framebuffer); Outputs::Display::OpenGL::ScanTarget scan_target(target_framebuffer);
std::unique_ptr<ActivityObserver> activity_observer;
bool uses_mouse;
std::vector<SDLJoystick> joysticks;
machine_runner.machine_mutex = &machine_mutex;
const auto setup_machine_input_output = [&scan_target, &machine, &speaker_delegate, &activity_observer, &joysticks, &uses_mouse, &machine_runner] {
// Wire up the best-effort updater, its delegate, and the speaker delegate.
machine_runner.machine = machine.get();
machine->scan_producer()->set_scan_target(&scan_target); machine->scan_producer()->set_scan_target(&scan_target);
// For now, lie about audio output intentions. // For now, lie about audio output intentions.
@ -869,65 +917,35 @@ int main(int argc, char *argv[]) {
SDL_PauseAudioDevice(speaker_delegate.audio_device, 0); SDL_PauseAudioDevice(speaker_delegate.audio_device, 0);
} }
int window_width, window_height; /*
SDL_GetWindowSize(window, &window_width, &window_height); If the machine offers anything for activity observation,
create and register an activity observer.
*/
Activity::Source *const activity_source = machine->activity_source();
if(activity_source) {
activity_observer = std::make_unique<ActivityObserver>(activity_source, 4.0f / 3.0f);
} else {
activity_observer = nullptr;
}
// If this is a joystick machine, check for and open attached joysticks. // If this is a joystick machine, check for and open attached joysticks.
/*!
Provides a wrapper for SDL_Joystick pointers that can keep track
of historic hat values.
*/
class SDLJoystick {
public:
SDLJoystick(SDL_Joystick *joystick) : joystick_(joystick) {
hat_values_.resize(SDL_JoystickNumHats(joystick));
}
~SDLJoystick() {
SDL_JoystickClose(joystick_);
}
/// @returns The underlying SDL_Joystick.
SDL_Joystick *get() {
return joystick_;
}
/// @returns A reference to the storage for the previous state of hat @c c.
Uint8 &last_hat_value(int c) {
return hat_values_[c];
}
/// @returns The logic OR of all stored hat states.
Uint8 hat_values() {
Uint8 value = 0;
for(const auto hat_value: hat_values_) {
value |= hat_value;
}
return value;
}
private:
SDL_Joystick *joystick_;
std::vector<Uint8> hat_values_;
};
std::vector<SDLJoystick> joysticks;
const auto joystick_machine = machine->joystick_machine(); const auto joystick_machine = machine->joystick_machine();
if(joystick_machine) { if(joystick_machine) {
SDL_InitSubSystem(SDL_INIT_JOYSTICK); SDL_InitSubSystem(SDL_INIT_JOYSTICK);
for(int c = 0; c < SDL_NumJoysticks(); ++c) { for(int c = 0; c < SDL_NumJoysticks(); ++c) {
joysticks.emplace_back(SDL_JoystickOpen(c)); joysticks.emplace_back(SDL_JoystickOpen(c));
} }
} else {
joysticks.clear();
} }
/* // Keep a record of whether mouse events can be forwarded.
If the machine offers anything for activity observation, uses_mouse = !!machine->mouse_machine();
create and register an activity observer. };
*/ setup_machine_input_output();
std::unique_ptr<ActivityObserver> activity_observer;
Activity::Source *const activity_source = machine->activity_source(); int window_width, window_height;
if(activity_source) { SDL_GetWindowSize(window, &window_width, &window_height);
activity_observer = std::make_unique<ActivityObserver>(activity_source, 4.0f / 3.0f);
}
// SDL 2.x delivers key up/down events and text inputs separately even when they're correlated; // SDL 2.x delivers key up/down events and text inputs separately even when they're correlated;
// this struct and map is used to correlate them by time. // this struct and map is used to correlate them by time.
@ -945,7 +963,6 @@ int main(int argc, char *argv[]) {
std::vector<KeyPress> keypresses; std::vector<KeyPress> keypresses;
// Run the main event loop until the OS tells us to quit. // Run the main event loop until the OS tells us to quit.
const bool uses_mouse = !!machine->mouse_machine();
bool should_quit = false; bool should_quit = false;
Uint32 fullscreen_mode = 0; Uint32 fullscreen_mode = 0;
machine_runner.start(); machine_runner.start();
@ -987,8 +1004,26 @@ int main(int argc, char *argv[]) {
break; break;
case SDL_DROPFILE: { case SDL_DROPFILE: {
Analyser::Static::Media media = Analyser::Static::GetMedia(event.drop.file); const Analyser::Static::Media media = Analyser::Static::GetMedia(event.drop.file);
// If the new file is only media, insert it; if it is a state snapshot then
// tear down the entire machine and replace it.
if(!media.empty()) {
machine->media_target()->insert_media(media); machine->media_target()->insert_media(media);
break;
}
targets = Analyser::Static::GetTargets(event.drop.file);
if(targets.empty()) break;
::Machine::Error error;
std::unique_ptr<::Machine::DynamicMachine> new_machine(::Machine::MachineForTargets(targets, rom_fetcher, error));
if(error != Machine::Error::None) break;
machine = std::move(new_machine);
static_cast<Outputs::Display::ScanTarget *>(&scan_target)->will_change_owner();
setup_machine_input_output();
window_titler.set_file_name(final_path_component(event.drop.file));
} break; } break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT: