1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-10-16 11:23:51 +00:00

Merge pull request #653 from TomHarte/CommandCapture

Allows machines to declare modifiers as 'essential'.
This commit is contained in:
Thomas Harte 2019-09-22 14:04:06 -04:00 committed by GitHub
commit e8bd538182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 173 additions and 30 deletions

View File

@ -10,13 +10,14 @@
using namespace Inputs;
Keyboard::Keyboard() {
Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) {
for(int k = 0; k < int(Key::Help); ++k) {
observed_keys_.insert(Key(k));
}
}
Keyboard::Keyboard(const std::set<Key> &observed_keys) : observed_keys_(observed_keys), is_exclusive_(false) {}
Keyboard::Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers) :
observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {}
void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
std::size_t key_offset = static_cast<std::size_t>(key);
@ -28,6 +29,10 @@ void Keyboard::set_key_pressed(Key key, char value, bool is_pressed) {
if(delegate_) delegate_->keyboard_did_change_key(this, key, is_pressed);
}
const std::set<Inputs::Keyboard::Key> &Keyboard::get_essential_modifiers() {
return essential_modifiers_;
}
void Keyboard::reset_all_keys() {
std::fill(key_states_.begin(), key_states_.end(), false);
if(delegate_) delegate_->reset_all_keys(this);

View File

@ -39,10 +39,10 @@ class Keyboard {
};
/// Constructs a Keyboard that declares itself to observe all keys.
Keyboard();
Keyboard(const std::set<Key> &essential_modifiers = {});
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
Keyboard(const std::set<Key> &observed_keys);
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
// Host interface.
virtual void set_key_pressed(Key key, char value, bool is_pressed);
@ -51,10 +51,18 @@ class Keyboard {
/// @returns a set of all Keys that this keyboard responds to.
virtual const std::set<Key> &observed_keys();
/*
/// @returns the list of modifiers that this keyboard considers 'essential' (i.e. both mapped and highly used).
virtual const std::set<Inputs::Keyboard::Key> &get_essential_modifiers();
/*!
@returns @c true if this keyboard, on its original machine, looked
like a complete keyboard i.e. if a user would expect this keyboard
to be the only thing a real keyboard maps to.
So this would be true of something like the Amstrad CPC, which has a full
keyboard, but it would be false of something like the Sega Master System
which has some buttons that you'd expect an emulator to map to its host
keyboard but which does not offer a full keyboard.
*/
virtual bool is_exclusive();
@ -68,6 +76,7 @@ class Keyboard {
private:
std::set<Key> observed_keys_;
std::set<Key> essential_modifiers_;
std::vector<bool> key_states_;
Delegate *delegate_ = nullptr;
bool is_exclusive_ = true;

View File

@ -291,7 +291,7 @@ class Keyboard {
Provides a mapping from idiomatic PC keys to Macintosh keys.
*/
class KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override;
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) final;
};
}

View File

@ -78,6 +78,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
using Target = Analyser::Static::Macintosh::Target;
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
KeyboardMachine::MappedMachine({
Inputs::Keyboard::Key::LeftShift, Inputs::Keyboard::Key::RightShift,
Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption,
Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta,
}),
mc68000_(*this),
iwm_(CLOCK_RATE),
video_(audio_, drive_speed_accumulator_),

View File

@ -10,7 +10,7 @@
using namespace KeyboardMachine;
MappedMachine::MappedMachine() {
MappedMachine::MappedMachine(const std::set<Inputs::Keyboard::Key> &essential_modifiers) : keyboard_(essential_modifiers) {
keyboard_.set_delegate(this);
}

View File

@ -11,6 +11,7 @@
#include <cstdint>
#include <string>
#include <set>
#include "../Inputs/Keyboard.hpp"
@ -56,7 +57,7 @@ class Machine: public KeyActions {
*/
class MappedMachine: public Inputs::Keyboard::Delegate, public Machine {
public:
MappedMachine();
MappedMachine(const std::set<Inputs::Keyboard::Key> &essential_modifiers = {});
/*!
A keyboard mapper attempts to provide a physical mapping between host keys and emulated keys.

View File

@ -101,7 +101,7 @@ class ConcreteMachine:
audio_queue_,
sn76489_divider),
speaker_(sn76489_),
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}) {
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
// Pick the clock rate based on the region.
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
speaker_.set_input_rate(static_cast<float>(clock_rate / sn76489_divider));

View File

@ -656,6 +656,7 @@
4BCE005D227D30CC000CA200 /* MemoryPacker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005B227D30CC000CA200 /* MemoryPacker.cpp */; };
4BCE0060227D39AB000CA200 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005E227D39AB000CA200 /* Video.cpp */; };
4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; };
4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; };
4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD388882239E198002D14B5 /* 68000Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD388872239E198002D14B5 /* 68000Tests.mm */; };
@ -1095,6 +1096,7 @@
4B90467222C6FA31000E2074 /* TestRunner68000.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TestRunner68000.hpp; sourceTree = "<group>"; };
4B90467322C6FADD000E2074 /* 68000BitwiseTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000BitwiseTests.mm; sourceTree = "<group>"; };
4B90467522C6FD6E000E2074 /* 68000ArithmeticTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = 68000ArithmeticTests.mm; sourceTree = "<group>"; };
4B911A9B2337D8AB00A2BB1D /* CSApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSApplication.h; sourceTree = "<group>"; };
4B92294222B04A3D00A1458F /* MouseMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MouseMachine.hpp; sourceTree = "<group>"; };
4B92294422B04ACB00A1458F /* Mouse.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Mouse.hpp; sourceTree = "<group>"; };
4B92294A22B064FD00A1458F /* QuadratureMouse.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = QuadratureMouse.hpp; sourceTree = "<group>"; };
@ -1478,6 +1480,7 @@
4BCF1FA31DADC3DD0039D2E7 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Oric/Oric.hpp; sourceTree = "<group>"; };
4BD060A51FE49D3C006E14BE /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
4BD0692B22828A2D00D2A54F /* RealTimeClock.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RealTimeClock.hpp; sourceTree = "<group>"; };
4BD0FBC2233706A200148981 /* CSApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CSApplication.m; sourceTree = "<group>"; };
4BD191D9219113B80042E144 /* OpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OpenGL.hpp; sourceTree = "<group>"; };
4BD191F22191180E0042E144 /* ScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTarget.cpp; sourceTree = "<group>"; };
4BD191F32191180E0042E144 /* ScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ScanTarget.hpp; sourceTree = "<group>"; };
@ -3005,22 +3008,24 @@
4BB73EA01B587A5100552FC2 /* Clock Signal */ = {
isa = PBXGroup;
children = (
4BBFE83B21015D9C00BF1C40 /* Joystick Manager */,
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */,
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */,
4B911A9B2337D8AB00A2BB1D /* CSApplication.h */,
4BD0FBC2233706A200148981 /* CSApplication.m */,
4BB73EAD1B587A5100552FC2 /* Info.plist */,
4BB73EA11B587A5100552FC2 /* AppDelegate.swift */,
4BB73EA81B587A5100552FC2 /* Assets.xcassets */,
4B2A538F1D117D36003C6002 /* Audio */,
4B643F3D1D77B88000D431D6 /* Document Controller */,
4B55CE551C3B7D360093A61B /* Documents */,
4BBFE83B21015D9C00BF1C40 /* Joystick Manager */,
4B2A53921D117D36003C6002 /* Machine */,
4B55DD7F20DF06680043F2E5 /* MachinePicker */,
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4BE5F85A1C3E1C2500C43F01 /* Resources */,
4BDA00DB22E60EE900AC3CD0 /* ROMRequester */,
4BD5F1961D1352A000631CD1 /* Updater */,
4B55CE5A1C3B7D6F0093A61B /* Views */,
4BDA00DB22E60EE900AC3CD0 /* ROMRequester */,
);
path = "Clock Signal";
sourceTree = "<group>";
@ -4273,6 +4278,7 @@
4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */,
4BDACBEC22FFA5D20045EF7E /* ncr5380.cpp in Sources */,
4B54C0C21F8D91CD0050900F /* Keyboard.cpp in Sources */,
4BD0FBC3233706A200148981 /* CSApplication.m in Sources */,
4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */,
4B89449520194CB3007DE474 /* MachineForTarget.cpp in Sources */,
4B4A76301DB1A3FA007AAE2E /* AY38910.cpp in Sources */,

View File

@ -67,7 +67,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
@ -76,8 +76,6 @@
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
stopOnEveryThreadSanitizerIssue = "YES"
stopOnEveryUBSanitizerIssue = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "NO">

View File

@ -13,16 +13,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application.
if NSDocumentController.shared.documents.count == 0 {
NSDocumentController.shared.openDocument(self)
}
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application.
}
private var hasShownOpenDocument = false
func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
// Decline to show the 'New...' selector by default; the 'Open...'
// dialogue has already been shown if this application was started
@ -31,6 +28,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Obiter: I dislike it when other applications do this for me, but it
// seems to be the new norm, and I've had user feedback that showing
// nothing is confusing. So here it is.
if !hasShownOpenDocument {
NSDocumentController.shared.openDocument(self)
hasShownOpenDocument = true
}
return false
}
}

View File

@ -0,0 +1,31 @@
//
// CSApplication.h
// Clock Signal
//
// Created by Thomas Harte on 22/09/2019.
// Copyright © 2019 Thomas Harte. All rights reserved.
//
#ifndef CSApplication_h
#define CSApplication_h
#import <Cocoa/Cocoa.h>
@protocol CSApplicationKeyboardEventDelegate
- (void)sendEvent:(nonnull NSEvent *)event;
@end
/*!
CSApplication differs from NSApplication in only one regard: it supports a keyboardEventDelegate.
If a keyboardEventDelegate is installed, all keyboard events  @c NSEventTypeKeyUp,
@c NSEventTypeKeyDown and @c NSEventTypeFlagsChanged will be diverted to it
rather than passed through the usual processing. As a result keyboard shortcuts and assistive
dialogue navigations won't work.
*/
@interface CSApplication: NSApplication
@property(nonatomic, weak, nullable) id<CSApplicationKeyboardEventDelegate> keyboardEventDelegate;
@end
#endif /* CSApplication_h */

View File

@ -0,0 +1,37 @@
//
// Application.m
// Clock Signal
//
// Created by Thomas Harte on 21/09/2019.
// Copyright © 2019 Thomas Harte. All rights reserved.
//
#import "CSApplication.h"
@implementation CSApplication
- (void)sendEvent:(NSEvent *)event {
// The only reason to capture events here rather than at the window or view
// is to divert key combinations such as command+w or command+q in the few
// occasions when the user would expect those to affect a running machine
// rather than to affect application state.
//
// Most obviously: when emulating a Macintosh, you'd expect command+q to
// quit the application inside the Macintosh, not to quit the emulator.
switch(event.type) {
case NSEventTypeKeyUp:
case NSEventTypeKeyDown:
case NSEventTypeFlagsChanged:
if(self.keyboardEventDelegate) {
[self.keyboardEventDelegate sendEvent:event];
return;
}
default:
[super sendEvent:event];
}
}
@end

View File

@ -209,8 +209,10 @@ class MachineDocument:
openGLView.delegate = self
openGLView.responderDelegate = self
// If this machine has a mouse, enable mouse capture.
// If this machine has a mouse, enable mouse capture; also indicate whether usurption
// of the command key is desired.
openGLView.shouldCaptureMouse = machine.hasMouse
openGLView.shouldUsurpCommand = machine.shouldUsurpCommand
setupAudioQueueClockRate()

View File

@ -554,6 +554,6 @@
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<string>CSApplication</string>
</dict>
</plist>

View File

@ -94,6 +94,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
// Input control.
@property (nonatomic, readonly) BOOL hasExclusiveKeyboard;
@property (nonatomic, readonly) BOOL shouldUsurpCommand;
@property (nonatomic, readonly) BOOL hasJoystick;
@property (nonatomic, readonly) BOOL hasMouse;
@property (nonatomic, assign) CSMachineKeyboardInputMode inputMode;

View File

@ -671,6 +671,14 @@ struct ActivityObserver: public Activity::Observer {
return !!_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive();
}
- (BOOL)shouldUsurpCommand {
if(!_machine->keyboard_machine()) return NO;
const auto essential_modifiers = _machine->keyboard_machine()->get_keyboard().get_essential_modifiers();
return essential_modifiers.find(Inputs::Keyboard::Key::LeftMeta) != essential_modifiers.end() ||
essential_modifiers.find(Inputs::Keyboard::Key::RightMeta) != essential_modifiers.end();
}
#pragma mark - Activity observation
- (void)addLED:(NSString *)led {

View File

@ -110,8 +110,22 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
/// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then
/// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate
/// are forwarded, unless and until the user releases the mouse using the control+command shortcut.
@property (nonatomic, assign) BOOL shouldCaptureMouse;
/// Determines whether the CSOpenGLViewResponderDelegate of this window expects to use the command
/// key as though it were any other key — i.e. all command combinations should be forwarded to the delegate,
/// not being allowed to trigger regular application shortcuts such as command+q or command+h.
///
/// How the view respects this will depend on other state; if this view is one that captures the mouse then it
/// will usurp command only while the mouse is captured.
///
/// TODO: what's smart behaviour if this view doesn't capture the mouse? Probably
/// force a similar capturing behaviour?
@property (nonatomic, assign) BOOL shouldUsurpCommand;
/*!
Ends the timer tracking time; should be called prior to giving up the last owning reference
to ensure that any retain cycles implied by the timer are resolved.

View File

@ -7,10 +7,11 @@
//
#import "CSOpenGLView.h"
#import "CSApplication.h"
@import CoreVideo;
@import GLKit;
@interface CSOpenGLView () <NSDraggingDestination>
@interface CSOpenGLView () <NSDraggingDestination, CSApplicationKeyboardEventDelegate>
@end
@implementation CSOpenGLView {
@ -139,23 +140,43 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
return YES;
}
- (void)propagateKeyboardEvent:(NSEvent *)event {
switch(event.type) {
default: break;
case kCGEventKeyDown:
[self.responderDelegate keyDown:event];
break;
case kCGEventKeyUp:
[self.responderDelegate keyUp:event];
break;
case kCGEventFlagsChanged:
// Release the mouse upon a control + command.
if(_mouseIsCaptured &&
event.modifierFlags & NSEventModifierFlagControl &&
event.modifierFlags & NSEventModifierFlagCommand) {
[self releaseMouse];
}
[self.responderDelegate flagsChanged:event];
break;
}
}
- (void)keyDown:(NSEvent *)theEvent {
[self.responderDelegate keyDown:theEvent];
[self propagateKeyboardEvent:theEvent];
}
- (void)keyUp:(NSEvent *)theEvent {
[self.responderDelegate keyUp:theEvent];
[self propagateKeyboardEvent:theEvent];
}
- (void)flagsChanged:(NSEvent *)theEvent {
[self.responderDelegate flagsChanged:theEvent];
[self propagateKeyboardEvent:theEvent];
}
// Release the mouse upon a control + command.
if(_mouseIsCaptured &&
theEvent.modifierFlags & NSEventModifierFlagControl &&
theEvent.modifierFlags & NSEventModifierFlagCommand) {
[self releaseMouse];
}
- (void)sendEvent:(NSEvent *)event {
[self propagateKeyboardEvent:event];
}
- (void)paste:(id)sender {
@ -224,6 +245,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
CGAssociateMouseAndMouseCursorPosition(true);
[NSCursor unhide];
[self.delegate openGLViewDidReleaseMouse:self];
((CSApplication *)[NSApplication sharedApplication]).keyboardEventDelegate = nil;
}
}
@ -283,6 +305,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(false);
[self.delegate openGLViewDidCaptureMouse:self];
if(self.shouldUsurpCommand) {
((CSApplication *)[NSApplication sharedApplication]).keyboardEventDelegate = self;
}
// Don't report the first click to the delegate; treat that as merely
// an invitation to capture the cursor.