142 Commits

Author SHA1 Message Date
Tomáš Znamenáček 3ebbb7efde Version bump to 2.1.0. 2015-01-16 11:53:19 +01:00
Tomáš Znamenáček 66fd9b8e41 Merge pull request #57 from shpakovski/legacy-osx-support
Legacy OS X support down to 10.6 included (fixes #56).
2015-01-16 11:38:43 +01:00
Tomáš Znamenáček 1baa2bae9d Removed section about ARC support from README.
There are now two officially supported ways to use MASShortcut: through
including the Xcode project in your app’s workspace and linking against
MASShortcut.framework, and through CocoaPods. Both options work well in both
ARC and MRC projects with no additional settings needed. (I use MASShortcut in
a MRC project myself.)
2015-01-16 09:42:37 +01:00
Tomáš Znamenáček 27eace979e Added test for hotkeys and shortcut monitor. 2015-01-14 12:54:49 +01:00
Tomáš Znamenáček 44542896d4 Updating to current master. 2015-01-14 11:07:22 +01:00
Tomáš Znamenáček bdb64f0177 Use the clear glyph instead of backspace for clearing a shortcut.
The recording control used to display the backspace glyph (U+232B)
on the button that clears the shortcut. That’s a bit confusing, since
the same backspace glyph can also appear inside the control,
representing the recorded shortcut. The clear glyph (U+2715,
diagonal cross) seems like a better fit – it’s already used in
similar context throughout the Apple UIs like search bars.
2015-01-14 10:03:31 +01:00
Tomáš Znamenáček cfc4bd64d0 Updated headerdoc markup for MASShortcutView+Bindings. 2015-01-14 09:59:28 +01:00
Vadim Shpakovski 988fcee208 Fix README. 2015-01-13 20:45:29 +03:00
Vadim Shpakovski ca561ca70c Merge pull request #59 from aral/patch-1
Added explicit instructions for use in Swift
2015-01-13 20:25:58 +03:00
Aral Balkan 8dc86c9b62 Added explicit instructions for use in Swift
The explicit Cocoa import had tripped me up. Would be good to save someone the same hassle.
2015-01-13 17:21:27 +00:00
Tomáš Znamenáček 86eff031bf Merge branch 'master' into legacy-osx-support.
Conflicts:
	CHANGES
2015-01-13 13:26:24 +01:00
Tomáš Znamenáček 62c142a7f5 Added the Travis build status badge to the README. 2015-01-13 13:24:59 +01:00
Vadim Shpakovski 8286403add Merge pull request #58 from zoul/travis
Add support for Travis Continuous Integration server.
2015-01-13 13:56:55 +03:00
Tomáš Znamenáček a045295531 Added shared Xcode schemes to help Travis with the build. 2015-01-13 09:21:58 +01:00
Tomáš Znamenáček 807c5d782b Added a barebones Travis config file. 2015-01-13 09:14:23 +01:00
Tomáš Znamenáček cd0926808c Changed headerdoc markup to work better with CocoaDocs (closes #55). 2015-01-13 08:49:56 +01:00
Tomáš Znamenáček b9b1964b3f Non-exclusive hotkey registration (#56). 2015-01-12 17:10:20 +01:00
Tomáš Znamenáček 5208c981af Updated Deployment Target setting in podspec. 2015-01-12 17:10:20 +01:00
Tomáš Znamenáček 86a3231398 Added support for older OS X releases back to 10.6 included.
Apart from turning off Auto Layout for the Demo project, the only
thing remaining was several __weak qualifiers to prevent retain
cycles in blocks. I have replaced them with __unsafe_unretained
since __weak is not supported on 10.6. There should be no safety
concerns here, since we are certain the pointers will remain valid.
2015-01-12 17:10:20 +01:00
Vadim Shpakovski 0a0619461d Remove Xcode warnings and fix the hard-coded shortcut. 2015-01-09 22:04:10 +03:00
Tomáš Znamenáček 25d97b68c7 Removed an obsolete reference to the XCTest framework. 2015-01-09 12:58:06 +01:00
Tomáš Znamenáček 179dcd6cc2 Trivial podspec whitespace fixes. 2015-01-09 11:53:46 +01:00
Tomáš Znamenáček 2bbdbbfff9 Fixed Podspec (silly me). 2015-01-09 11:37:00 +01:00
Tomáš Znamenáček aaa7c1cac8 Updated CHANGES before 2.0.0. 2015-01-09 11:31:51 +01:00
Tomáš Znamenáček 425cdd7074 Added myself to Podspec “authors” field. 2015-01-09 11:29:38 +01:00
Tomáš Znamenáček f8ce69e9d0 Markup fixes in README. 2015-01-09 11:26:34 +01:00
Tomáš Znamenáček 6165e66859 Added a note about Shortcut Recorder compatibility (#54). 2015-01-09 11:24:53 +01:00
Tomáš Znamenáček 1707e06678 Added CocoaPods installation instructions (#54). 2015-01-09 11:15:32 +01:00
Tomáš Znamenáček f63c21e11c Expanded demo functionality (preparing for 2.0.0, see #54).
Mostly back to the previous version with enable/disable checkboxes &
one settable and one hard-coded shortcut. I have tried to come up with
a nicer pattern to watch the enabled/disabled checkboxes, but without
external dependencies like Facebook’s KVOController I didn’t come up
with anything better than plain old KVO.
2015-01-09 10:53:45 +01:00
Tomáš Znamenáček fc43b570c7 Simplified and updated the demo project. 2015-01-08 14:16:26 +01:00
Tomáš Znamenáček 45921ff6f8 MASShortcutMonitor will silently skip nil shortcuts passed to unregisterShortcut:. 2015-01-08 12:58:56 +01:00
Tomáš Znamenáček 9b919cba51 Merge pull request #53 from zoul/2.0-candidate
Thank you very much!
2015-01-08 12:00:53 +01:00
Tomáš Znamenáček ea69d59395 Ignore disabled system shortcuts.
This was implemented before in 0633545a, but lost during the rebase.
2015-01-08 10:50:08 +01:00
Tomáš Znamenáček d2a371c92e Documented the recording control behaviour and implemented Cmd-W/Q. 2015-01-08 10:47:25 +01:00
Tomáš Znamenáček 4a32eca2e6 Updated copyright & versioning info in the framework’s Info.plist. 2015-01-07 17:41:51 +01:00
Tomáš Znamenáček 252ae40886 Added a CHANGES file to keep users posted about important changes. 2015-01-07 16:44:55 +01:00
Tomáš Znamenáček 595d3c55bc Fixed the alert that pops up when the shortcut is already used.
The longer message text was used as the alert title.
2015-01-07 16:26:49 +01:00
Tomáš Znamenáček 9a223134d0 Fixed MASShortcutView initialization errors after rebase. 2015-01-07 16:26:49 +01:00
Tomáš Znamenáček d4ba2e7a36 Accept Backspace and Delete keys with modifiers as valid shortcuts.
When recording a new shortcut using the MASShortcutView control,
the user may press Delete or Backspace to clear the current shortcut.
In the previous versions the corresponding code branch didn’t test
the modifier flags, making it impossible to record shortcuts such
as Cmd-Alt-Delete. Now the control should behave as expected, only
using “naked” Delete and Backspace keys to clear the shortcut.
2015-01-07 16:26:49 +01:00
Tomáš Znamenáček 29cf3be3ad Treat build warnings as errors. 2015-01-07 16:26:49 +01:00
Tomáš Znamenáček 0d95079a43 Updated podspec.
We’re not packaging the code as a framework for CocoaPods – I’m not
sure how the podspec should look like for that case and if it’s even
supported, see this Stack Overflow question:

http://stackoverflow.com/questions/25258395

Instead we simply pick the naked source files as usual with CocoaPods.
2015-01-07 15:46:30 +01:00
Tomáš Znamenáček 2fd007895d Fixed wrong archiver used for MASShortcutView bindings. 2015-01-07 15:43:26 +01:00
Tomáš Znamenáček 1094fd9a61 Added header documentation. 2015-01-07 15:43:26 +01:00
Tomáš Znamenáček 4df3b54b38 Introduced a MASDictionaryTransformerName constant.
This makes it easier to set view bindings from within the Interface Builder.
2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 6128e529ea Whitespace fixes. 2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 80808eb6d7 Updated header settings. 2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 7685db2133 Updated documentation. 2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 46aa323115 Added a simplified binding API for MASShortcutView.
This returns the associatedUserDefaultsKey property used in previous code
versions, only the implementation uses less magic.
2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 5f5f6cb9d0 Redesigned demo app layout & feedback.
I have replaced the modal alert with a beep, it’s faster to test
without having to close the alert.
2015-01-07 15:42:22 +01:00
Tomáš Znamenáček 118abba104 Added a shared binder instance singleton.
This adds a really simple API to set up some bindings without having
to keep a binder instance around by hand. If somebody wants to, it’s
not a problem to allocate a separate instance and have precise control
over its lifetime.
2015-01-07 15:42:22 +01:00
Tomáš Znamenáček bbf2f69da4 Better treatment of nil values when storing shortcuts as dictionaries.
This makes it possible to make a difference between “shortcut not
set, use default” and “shortcut set to none”.
2015-01-07 15:42:22 +01:00
Tomáš Znamenáček d687545083 Added a convenience call to register default shortcuts. 2015-01-07 15:42:21 +01:00
Tomáš Znamenáček f03fad7b62 Added Shortcut.h to the tests prefix header to simplify tests. 2015-01-07 15:42:21 +01:00
Tomáš Znamenáček 942bbe849e Added a custom transformer to store shortcuts as dictionaries.
The MASDictionaryTransformer class is used to save shortcuts to
user defaults (and load them back) using a simple dictionary. The
value stored in the user defaults looks like this:

$ defaults read com.shpakovski.mac.Demo
{
    MASDemoShortcut =     {
        keyCode = 15;
        modifierFlags = 1048576;
    };
    …
}

This storage format has got the distinct advantage of being compatible
with the format used by Shortcut Recorder. In order to use it, you
have to set proper binding options for MASShortcutBinder and the
recorder control (MASShortcutView).
2015-01-07 15:42:21 +01:00
Tomáš Znamenáček 756601488f Binding options can now be customized for MASShortcutBinder.
This makes it possible to customize the way the shortcuts are stored
in user defaults. The default options call for the keyed archiver
transformer, deserializing the shortcuts from NSData.
2015-01-07 15:42:21 +01:00
Tomáš Znamenáček be9358bf32 Turned MASShortcutMonitor into a singleton.
There can only be one Carbon event handler, so it doesn’t make sense
to create multiple instances of the shortcut monitor.
2015-01-07 15:42:21 +01:00
Tomáš Znamenáček f9f48f5ca8 Removed explicit @synthesize and renamed “appearance” to “style”.
The “appearance” property didn’t play nice with auto-synthesizing,
not really sure why.
2015-01-07 15:42:21 +01:00
Tomáš Znamenáček 444d1bccb9 Refactored the shortcut dispatcher and bindings to user defaults.
This is a big change that was hard to split into smaller commits. There’s now
a new class to bind shortcuts to actions, a new class to bind user defaults’
keys to actions, and a new way to associate user defaults with the recorder
control (MASShortcutView). I have also updated the demo app to go with the
changes.

The new class to associate shortcuts with actions is called MASShortcutMonitor.
It wraps the Carbon hotkey magic and offers a simple interface to add a
shortcut along with a block that should be run when the shortcut is pressed.
It’s the lowest-level interface.

Since the usual requirement is to store the shortcuts into user defaults,
there’s also a higher-level interface offered by the MASShortcutBinder class.
That takes a defaults key and associates it with a block. When the shortcut
stored under the defaults key changes, the binder automatically switches to the
new shortcut. The class is a wrapper built atop of the previous one, the
MASShortcutMonitor – it simply adds, updates and removes shortcuts as the
user defaults change.

I have removed the special user defaults integration code from the recorder
control (MASShortcutView) and replaced it with a small Cocoa Bindings shim.
This means that in order to keep the recorder control in sync with the defaults
you just have to call the usual bind:toObject:withKeyPath:options: method,
like this:

[_shortcutView bind:MASShortcutBinding
    toObject:[NSUserDefaultsController sharedUserDefaultsController]
    withKeyPath[@"values.ExampleDefaultsKey"
    options:@{NSValueTransformerNameBindingOption:NSKeyedUnarchiveFromDataTransformerName}];

That’s more verbose than the previous solution, but it’s much cleaner and can
be swept under a convenience call if needed. I might also add a dictionaryValue
property later that would make it possible to bind the value to user defaults
directly, without a transformer, and would enable backward compatibility with
Shortcut Recorder.
2015-01-07 15:39:39 +01:00
Tomáš Znamenáček d8efb0755a Removed one forgotten modifierFlags setter.
It’s not needed now the class is effectively immutable.
2015-01-07 15:33:46 +01:00
Tomáš Znamenáček e30504eb2a Implemented equality for shortcut objects.
Shortcuts are equal if and only if their key codes and modifier flags are.
2015-01-07 15:33:46 +01:00
Tomáš Znamenáček 1bdadb166d Made the keyCode and modifierFlags properties read-only.
A shortcut is a good value type, it makes good sense to represent it
using an immutable object like NSNumber or NSString.
2015-01-07 15:33:45 +01:00
Tomáš Znamenáček 4223f9b6b8 Implemented NSCopying for MASShortcut.
This makes it possible to use shortcuts as collection keys.
2015-01-07 15:33:45 +01:00
Tomáš Znamenáček 90cf1f856c Organized classes into Xcode folders. 2015-01-07 15:27:51 +01:00
Tomáš Znamenáček 1cebcd3acf Removed -data and shortcutWithData: from MASShortcut.
Using NSKeyedUnarchiver and NSKeyedArchiver directly is almost the
same amount of typing and it’s much clearer what goes on.
2015-01-07 15:27:51 +01:00
Tomáš Znamenáček fdc9e250fd Trivial refactoring.
Forgot one `instancetype`, added `static` to NSString constants,
removed unneccessary @synthesize directives.
2015-01-07 15:27:51 +01:00
Tomáš Znamenáček 344d22988f Removed [MASShortcut shouldBypass].
It was never used and the recording can already be cancelled by Esc.
2015-01-07 15:27:50 +01:00
Tomáš Znamenáček 5d06f00706 Introduced instancetype return type where appropriate. 2015-01-07 15:27:50 +01:00
Tomáš Znamenáček 6383b05419 Introduced a standalone MASShortcutValidator class to validate shortcuts.
It’s a natural simplification of the MASShortcut class. All MASShortcutView
objects use a shared validator by default, but can be reconfigured to use a
different validator if needed through the shortcutValidator property.
2015-01-07 15:27:50 +01:00
Tomáš Znamenáček 1c801726d3 Converted keycode macros to plain functions.
Plain functions are less prone to bugs, the compiler understands
them better and can offer better error messages, and plain functions
can be refactored more easily.
2015-01-07 15:05:12 +01:00
Tomáš Znamenáček 88392d1a69 Introduced a separate header file for keycode definitions. 2015-01-07 15:05:12 +01:00
Tomáš Znamenáček 42daeb6f12 Added a unit testing target. 2015-01-07 15:05:11 +01:00
Tomáš Znamenáček 0eb5c2e873 Updated documentation to point to the Demo target. 2015-01-07 15:05:11 +01:00
Tomáš Znamenáček df1ae3ede0 Deleted an unneccessary prefix header for the framework target. 2015-01-07 15:05:11 +01:00
Tomáš Znamenáček 0b08de6415 Created an umbrella header.
Now you can just `#import <MASShortcut/Shortcut.h>`.
2015-01-07 15:05:11 +01:00
Tomáš Znamenáček 377b44220f Repackaged the code as a framework and included the demo.
Packaging the code as a framework is mostly just a formality. It doesn’t
really change much, it just turns the code into a regular component.
What it does change is that the code now has its own Xcode settings,
which could make compatibility easier in the long run.

Including the demo in the main repository makes it easier to hack on
the library, since you can try the changes immediately. It also shows
how to bundle the framework into an app that uses it.
2015-01-07 15:05:11 +01:00
Vadim Shpakovski a3a459b4e4 Merge pull request #52 from starkos/flat-button-style
Flat button style
2014-11-24 17:33:12 +03:00
Jason Perkins 443e2db73d Fix indentation on previous commits: tabs to four spaces 2014-11-24 08:15:24 -05:00
Jason Perkins 5c7dd53380 Add new "flat" button style
Hides the button border and background; good for placing several shortcut buttons into a table view.
2014-11-24 08:07:41 -05:00
Vadim Shpakovski 72904d4dde Merge pull request #51 from starkos/userdefaults-semicolon-fix
Fix "Parse issue: Semicolon before method body is ignored"
2014-11-21 16:21:47 +03:00
Jason Perkins aa6c9fbab9 Fix "Parse issue: Semicolon before method body is ignored" 2014-11-21 07:54:39 -05:00
Vadim Shpakovski 7308a0927d Fixes possible memory leak. 2014-11-03 15:46:37 +03:00
Vadim Shpakovski d95c03289c Final touches to Podspec. 2014-09-22 14:06:03 +02:00
Vadim Shpakovski 14a9a4c4a6 Fixed Podspec. 2014-09-22 14:02:19 +02:00
Vadim Shpakovski 9266e38517 Polished Pod spec. 2014-09-22 13:59:45 +02:00
Vadim Shpakovski ddab66d2c0 Updated podspec. 2014-09-22 13:47:40 +02:00
Vadim Shpakovski 8e196a14a0 Merge pull request #45 from pfandrade/master
Implemented initWithCoder: to fix #44
2014-09-17 19:00:06 +03:00
Paulo F. Andrade e53eed24bc Implemented -initWithCoder: in MASShortcutView 2014-09-17 16:50:37 +01:00
Paulo F. Andrade 724376092d Updated MASShortcut 2014-09-17 16:47:37 +01:00
Paulo F. Andrade a5c2e15935 Added support for secure coding 2014-09-17 16:39:48 +01:00
Vadim Shpakovski 0633545a46 Ignoring disabled system shortcuts. 2014-09-02 09:43:53 +03:00
Vadim Shpakovski b30a0b02c4 Merge pull request #43 from darwin/master
allow [MODIFIER]+ESC shortcuts
2014-08-27 12:39:43 +03:00
Antonin Hildebrand 549b3ef29e allow [MODIFIER]+ESC shortcuts
only naked ESC without modifiers should cancel editing

http://discuss.binaryage.com/t/cant-change-activation-key-to-control-esc

tested in TotalTerminal codebase
2014-08-26 13:20:37 +02:00
Vadim Shpakovski f3a8a9a95a Merge pull request #40 from kainjow/master
Fixed compiling with projects that don't use a precompiled header.
2014-07-22 21:14:49 +03:00
Vadim Shpakovski 6e161f95fa Merge pull request #39 from xhacker/podspec
Create MASShortcut.podspec for CocoaPods
2014-07-22 21:12:37 +03:00
Kevin Wojniak 42dfc38ef6 Fixed compiling with projects that don't use a precompiled header. 2014-07-20 15:50:15 -07:00
LIU Dongyuan / 柳东原 dc088a3a77 Create MASShortcut.podspec 2014-07-18 22:45:18 -07:00
Vadim Shpakovski a21ac331ad Merge pull request #35 from ShadowLightz/master
Replace deprecated NSRunCriticalAlertPanel with NSAlert
2014-06-14 15:44:37 +03:00
ShadowLightz 7d3604820e Replace depr. NSRunCriticalAlertPanel with NSAlert
- Now using modern API
- The dialog is now shown as a modal sheet, which suits better
- The alert style is changed to NSWarningAlertStyle,
- NSCriticalAlertStyle was not required
2014-06-14 14:08:03 +02:00
Vadim Shpakovski ea5a30fe6d Merge pull request #32 from iluuu1994/master
Fixed string literal warning
2014-04-06 21:04:36 +03:00
Paulo F. Andrade da27736330 Replaced call to deprecated NSRunCriticalAlertPanel API 2014-04-03 11:25:28 +01:00
Ilija Tovilo 48a311eb90 Fixed string literal warning 2014-03-31 15:27:35 +02:00
Paulo F. Andrade 9c0b4a327c Added support for secure coding 2014-02-17 15:09:41 +00:00
Vadim Shpakovski 28c656d654 Merge pull request #29 from ShadowLightz/master
Silenced compiler warnings, updated README
2014-01-06 06:30:57 -08:00
Jonathan Rahn ce5760d61c update README 2014-01-06 14:08:32 +01:00
Jonathan Rahn f1228d6594 Silence compiler warning "No previous prototype for function..." and added an explicit typecast, update README with a hint to the view's height 2014-01-06 13:59:26 +01:00
Vadim Shpakovski 9edbf670f5 Adding new API for custom drawing. 2013-12-25 18:01:37 +02:00
Vadim Shpakovski b32a19ff7a Adding support for custom drawing. 2013-12-25 18:00:14 +02:00
Vadim Shpakovski fb4ac110a0 Fixing errors in MASShortcutDemo. 2013-07-31 14:26:49 +03:00
Vadim Shpakovski abda36331f Improving README. 2013-06-16 21:20:28 +03:00
Vadim Shpakovski 86443bc53c Adding syntax highlighting to the usage section. 2013-04-16 14:59:09 +03:00
Vadim Shpakovski 278c5fa2a5 Switching to NSUserDefaultsController.
NSUserDefaults does not support KVO officially.
2013-04-16 14:57:23 +03:00
Vadim Shpakovski b3e2e54830 Merge pull request #25 from corybohon/master
Adding a Notification section to the documentation.
2013-04-16 04:45:55 -07:00
Cory Bohon 7321b16163 Update README.md 2013-04-15 23:16:53 -03:00
Cory Bohon 9890a61538 Update README.md 2013-04-15 23:16:20 -03:00
Cory Bohon 75c763f697 Update README.md
Updated the documentation to cover NSUserDefaults observing.
2013-04-15 23:15:32 -03:00
Vadim Shpakovski e5b32d7d47 Fixing the latest changes. 2013-04-15 23:34:46 +03:00
Vadim Shpakovski 8ede004687 Fixing a problem with user defaults. 2013-04-15 23:24:46 +03:00
Vadim Shpakovski fe33039c18 Fixing the possible memory leak. 2013-03-24 11:21:40 +03:00
Vadim Shpakovski ffafb30498 Fixing a bug with resetting user defaults. 2013-03-06 13:53:45 +03:00
Vadim Shpakovski c6131623a7 Adding support for setting default user shortcut in preferences. 2013-03-06 13:27:57 +03:00
Vadim Shpakovski caf0c1e95e Merge pull request #19 from uasi/disambiguate-license
Disambiguate "the BSD license"
2013-02-07 00:42:46 -08:00
Tomoki Aonuma 7f0769adf4 State clearly that the license is the 2-clause BSD
LICENSE is taken from http://en.wikipedia.org/wiki/BSD_licenses
2013-02-07 12:01:35 +09:00
Vadim Shpakovski 20d323b59c Merge pull request #16 from OldFriend/master
Fixed mistypes (double 'used' x2)
2013-02-06 11:44:03 -08:00
Pedro Vieria dfdcd5655e Fixed mistypes (double 'used' x2) 2013-02-06 19:21:57 +00:00
Vadim Shpakovski 42be5135fb Merge pull request #15 from OldFriend/master
Fixed 'shortcut' mistype
2013-02-06 11:03:47 -08:00
Pedro Vieria 0266a4cee2 fixed mistype 2013-02-06 18:56:31 +00:00
Vadim Shpakovski 7300a064ec Update README.md
Adds one more non-ARC repository reference.
2012-12-13 16:45:45 +03:00
Vadim Shpakovski 0f89f3b962 Merge pull request #11 from radex/master
Make F17, F18 and F19 work
2012-12-03 14:24:31 -08:00
Radex 9239e1c98d Make F17-F19 work 2012-12-03 22:53:39 +01:00
Vadim Shpakovski 2c688abec9 Merge branch 'master' of github.com:shpakovski/MASShortcut 2012-11-23 23:46:21 +03:00
Vadim Shpakovski a89afec679 Enables support for exlusive Option modifier. 2012-11-23 23:46:04 +03:00
Vadim Shpakovski c4bc7c135f Updates README
Now there is a link to the non-ARC compatible fork.
2012-11-13 01:05:33 +03:00
Vadim Shpakovski bc56cdc907 Now the component properly handles your trying to set another handler for an existing shortcut.
Only the first assignable handler will be working, the second and other handlers all will be ignored. Also, previous workaround with calling performSelector:withObject:afterDelay: is removed, it seems to be working with an apprpriate use of objc_setAssociatedObject.
2012-11-10 12:34:05 +03:00
Vadim Shpakovski 3d4235f879 Fixes problem with view controllers.
When view controller is loaded from NIB, it posts a notification about changes in user defaults. This causes user defaults watcher to reload the hotkey. Some shortcut objects perform cleanup in -[dealloc], but this method may be called later then -[init] in new shortcut objects. It resulted in freeing just created keyboard shortcut referring to the same user defaults key. The problem has been fixed by moving creation of the replacement hotkey in the next run loop when -[dealloc] is guaranteed to be called in previous watcher.
2012-10-08 13:34:34 +03:00
Vadim Shpakovski 4099b62587 Isolates monitoring code from data. 2012-09-19 12:43:39 +03:00
Vadim Shpakovski 243aee3038 Adds support for direct monitoring shortcuts. 2012-09-19 12:36:53 +03:00
Vadim Shpakovski 723eeb4346 Improves API. 2012-09-19 08:57:26 +03:00
Vadim Shpakovski b1f90fa096 Merge. 2012-09-19 01:50:15 +03:00
Vadim Shpakovski 08717ff81e Merge remote-tracking branch 'origin/master' 2012-09-19 01:49:24 +03:00
Vadim Shpakovski da2e1af574 Adds a new appearance and fixes bugs related to key equivalents. 2012-09-19 01:44:49 +03:00
Vadim Shpakovski dddb527bfb Fixes the issue with setting associated User Defaults key twice. 2012-09-19 00:56:51 +03:00
Vadim Shpakovski e01e9075e5 Improves textured view appearance. 2012-09-03 16:23:21 +03:00
49 changed files with 3313 additions and 690 deletions
+3
View File
@@ -0,0 +1,3 @@
language: objective-c
xcode_project: MASShortcut.xcodeproj
xcode_scheme: MASShortcut
+12
View File
@@ -0,0 +1,12 @@
2.1.0 2015/1/16
- Added support for older OS X versions down to 10.6 included.
- Headerdoc markup that plays better with CocoaDocs.
2.0.1 2015/1/9
- Trivial Podspec fix.
2.0.0 2015/1/9
- First version with a changes file :)
- Major, backwards incompatible refactoring to simplify long-term maintenance.
- Added a simple spec describing the recording behaviour.
- Adds compatibility mode with Shortcut Recorder.
+5
View File
@@ -0,0 +1,5 @@
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, assign) IBOutlet NSWindow *window;
@end
+103
View File
@@ -0,0 +1,103 @@
#import "AppDelegate.h"
static NSString *const MASCustomShortcutKey = @"customShortcut";
static NSString *const MASCustomShortcutEnabledKey = @"customShortcutEnabled";
static NSString *const MASHardcodedShortcutEnabledKey = @"hardcodedShortcutEnabled";
static void *MASObservingContext = &MASObservingContext;
@interface AppDelegate ()
@property(strong) IBOutlet MASShortcutView *customShortcutView;
@property(strong) IBOutlet NSTextField *feedbackTextField;
@end
@implementation AppDelegate
- (void) awakeFromNib
{
[super awakeFromNib];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Register default values to be used for the first app start
[defaults registerDefaults:@{
MASHardcodedShortcutEnabledKey : @YES,
MASCustomShortcutEnabledKey : @YES,
}];
// Bind the shortcut recorder views value to user defaults.
// Run “defaults read com.shpakovski.mac.Demo” to see whats stored
// in user defaults.
[_customShortcutView setAssociatedUserDefaultsKey:MASCustomShortcutKey];
// Enable or disable the recorder view according to the first checkbox state
[_customShortcutView bind:@"enabled" toObject:defaults
withKeyPath:MASCustomShortcutEnabledKey options:nil];
// Watch user defaults for changes in the checkbox states
[defaults addObserver:self forKeyPath:MASCustomShortcutEnabledKey
options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew
context:MASObservingContext];
[defaults addObserver:self forKeyPath:MASHardcodedShortcutEnabledKey
options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew
context:MASObservingContext];
}
- (void)playShortcutFeedback
{
[[NSSound soundNamed:@"Ping"] play];
[_feedbackTextField setStringValue:@"Shortcut pressed!"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_feedbackTextField setStringValue:@""];
});
}
// Handle changes in user defaults. We have to check keyPath here to see which of the
// two checkboxes was changed. This is not very elegant, in practice you could use something
// like https://github.com/facebook/KVOController with a nicer API.
- (void) observeValueForKeyPath: (NSString*) keyPath ofObject: (id) object change: (NSDictionary*) change context: (void*) context
{
if (context != MASObservingContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
if ([keyPath isEqualToString:MASCustomShortcutEnabledKey]) {
[self setCustomShortcutEnabled:newValue];
} else if ([keyPath isEqualToString:MASHardcodedShortcutEnabledKey]) {
[self setHardcodedShortcutEnabled:newValue];
}
}
- (void) setCustomShortcutEnabled: (BOOL) enabled
{
if (enabled) {
[[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey:MASCustomShortcutKey toAction:^{
[self playShortcutFeedback];
}];
} else {
[[MASShortcutBinder sharedBinder] breakBindingWithDefaultsKey:MASCustomShortcutKey];
}
}
- (void) setHardcodedShortcutEnabled: (BOOL) enabled
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:kVK_F2 modifierFlags:NSCommandKeyMask];
if (enabled) {
[[MASShortcutMonitor sharedMonitor] registerShortcut:shortcut withAction:^{
[self playShortcutFeedback];
}];
} else {
[[MASShortcutMonitor sharedMonitor] unregisterShortcut:shortcut];
}
}
#pragma mark NSApplicationDelegate
- (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication*) sender
{
return YES;
}
@end
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 20122015 Vadim Shpakovski. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
+734
View File
@@ -0,0 +1,734 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6254" systemVersion="14B25" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6254"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="AMainMenu" systemMenu="main" id="29">
<items>
<menuItem title="Demo" id="56">
<menu key="submenu" title="Demo" systemMenu="apple" id="57">
<items>
<menuItem title="About Demo" id="58">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="236">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Preferences…" keyEquivalent="," id="129"/>
<menuItem isSeparatorItem="YES" id="143">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Services" id="131">
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="144">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Hide Demo" keyEquivalent="h" id="134">
<connections>
<action selector="hide:" target="-1" id="367"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="145">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="368"/>
</connections>
</menuItem>
<menuItem title="Show All" id="150">
<connections>
<action selector="unhideAllApplications:" target="-1" id="370"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="149">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Quit Demo" keyEquivalent="q" id="136">
<connections>
<action selector="terminate:" target="-3" id="449"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="83">
<menu key="submenu" title="File" id="81">
<items>
<menuItem title="New" keyEquivalent="n" id="82">
<connections>
<action selector="newDocument:" target="-1" id="373"/>
</connections>
</menuItem>
<menuItem title="Open…" keyEquivalent="o" id="72">
<connections>
<action selector="openDocument:" target="-1" id="374"/>
</connections>
</menuItem>
<menuItem title="Open Recent" id="124">
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="125">
<items>
<menuItem title="Clear Menu" id="126">
<connections>
<action selector="clearRecentDocuments:" target="-1" id="127"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="79">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Close" keyEquivalent="w" id="73">
<connections>
<action selector="performClose:" target="-1" id="193"/>
</connections>
</menuItem>
<menuItem title="Save…" keyEquivalent="s" id="75">
<connections>
<action selector="saveDocument:" target="-1" id="362"/>
</connections>
</menuItem>
<menuItem title="Revert to Saved" id="112">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="revertDocumentToSaved:" target="-1" id="364"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="74">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Page Setup..." keyEquivalent="P" id="77">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="runPageLayout:" target="-1" id="87"/>
</connections>
</menuItem>
<menuItem title="Print…" keyEquivalent="p" id="78">
<connections>
<action selector="print:" target="-1" id="86"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="217">
<menu key="submenu" title="Edit" id="205">
<items>
<menuItem title="Undo" keyEquivalent="z" id="207">
<connections>
<action selector="undo:" target="-1" id="223"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="215">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="redo:" target="-1" id="231"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="206">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Cut" keyEquivalent="x" id="199">
<connections>
<action selector="cut:" target="-1" id="228"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="197">
<connections>
<action selector="copy:" target="-1" id="224"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="203">
<connections>
<action selector="paste:" target="-1" id="226"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="485">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="486"/>
</connections>
</menuItem>
<menuItem title="Delete" id="202">
<connections>
<action selector="delete:" target="-1" id="235"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="198">
<connections>
<action selector="selectAll:" target="-1" id="232"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="214">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Find" id="218">
<menu key="submenu" title="Find" id="220">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="209">
<connections>
<action selector="performFindPanelAction:" target="-1" id="241"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="534">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="535"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="208">
<connections>
<action selector="performFindPanelAction:" target="-1" id="487"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="488"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221">
<connections>
<action selector="performFindPanelAction:" target="-1" id="489"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="210">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="245"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="216">
<menu key="submenu" title="Spelling and Grammar" id="200">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="204">
<connections>
<action selector="showGuessPanel:" target="-1" id="230"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="201">
<connections>
<action selector="checkSpelling:" target="-1" id="225"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="453"/>
<menuItem title="Check Spelling While Typing" id="219">
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="222"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="346">
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="347"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="454">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="456"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="348">
<menu key="submenu" title="Substitutions" id="349">
<items>
<menuItem title="Show Substitutions" id="457">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="458"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="459"/>
<menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350">
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="355"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351">
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="356"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="460">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="461"/>
</connections>
</menuItem>
<menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="357"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="462">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="463"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="450">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="451">
<items>
<menuItem title="Make Upper Case" id="452">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="464"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="465">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="468"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="466">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="467"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="211">
<menu key="submenu" title="Speech" id="212">
<items>
<menuItem title="Start Speaking" id="196">
<connections>
<action selector="startSpeaking:" target="-1" id="233"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="195">
<connections>
<action selector="stopSpeaking:" target="-1" id="227"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Format" id="375">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Format" id="376">
<items>
<menuItem title="Font" id="377">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Font" systemMenu="font" id="388">
<items>
<menuItem title="Show Fonts" keyEquivalent="t" id="389">
<connections>
<action selector="orderFrontFontPanel:" target="420" id="424"/>
</connections>
</menuItem>
<menuItem title="Bold" tag="2" keyEquivalent="b" id="390">
<connections>
<action selector="addFontTrait:" target="420" id="421"/>
</connections>
</menuItem>
<menuItem title="Italic" tag="1" keyEquivalent="i" id="391">
<connections>
<action selector="addFontTrait:" target="420" id="422"/>
</connections>
</menuItem>
<menuItem title="Underline" keyEquivalent="u" id="392">
<connections>
<action selector="underline:" target="-1" id="432"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="393"/>
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="394">
<connections>
<action selector="modifyFont:" target="420" id="425"/>
</connections>
</menuItem>
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="395">
<connections>
<action selector="modifyFont:" target="420" id="423"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="396"/>
<menuItem title="Kern" id="397">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Kern" id="415">
<items>
<menuItem title="Use Default" id="416">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardKerning:" target="-1" id="438"/>
</connections>
</menuItem>
<menuItem title="Use None" id="417">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffKerning:" target="-1" id="441"/>
</connections>
</menuItem>
<menuItem title="Tighten" id="418">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="tightenKerning:" target="-1" id="431"/>
</connections>
</menuItem>
<menuItem title="Loosen" id="419">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="loosenKerning:" target="-1" id="435"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Ligatures" id="398">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Ligatures" id="411">
<items>
<menuItem title="Use Default" id="412">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useStandardLigatures:" target="-1" id="439"/>
</connections>
</menuItem>
<menuItem title="Use None" id="413">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="turnOffLigatures:" target="-1" id="440"/>
</connections>
</menuItem>
<menuItem title="Use All" id="414">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="useAllLigatures:" target="-1" id="434"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Baseline" id="399">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Baseline" id="405">
<items>
<menuItem title="Use Default" id="406">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unscript:" target="-1" id="437"/>
</connections>
</menuItem>
<menuItem title="Superscript" id="407">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="superscript:" target="-1" id="430"/>
</connections>
</menuItem>
<menuItem title="Subscript" id="408">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="subscript:" target="-1" id="429"/>
</connections>
</menuItem>
<menuItem title="Raise" id="409">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="raiseBaseline:" target="-1" id="426"/>
</connections>
</menuItem>
<menuItem title="Lower" id="410">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowerBaseline:" target="-1" id="427"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="400"/>
<menuItem title="Show Colors" keyEquivalent="C" id="401">
<connections>
<action selector="orderFrontColorPanel:" target="-1" id="433"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="402"/>
<menuItem title="Copy Style" keyEquivalent="c" id="403">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="copyFont:" target="-1" id="428"/>
</connections>
</menuItem>
<menuItem title="Paste Style" keyEquivalent="v" id="404">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteFont:" target="-1" id="436"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Text" id="496">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Text" id="497">
<items>
<menuItem title="Align Left" keyEquivalent="{" id="498">
<connections>
<action selector="alignLeft:" target="-1" id="524"/>
</connections>
</menuItem>
<menuItem title="Center" keyEquivalent="|" id="499">
<connections>
<action selector="alignCenter:" target="-1" id="518"/>
</connections>
</menuItem>
<menuItem title="Justify" id="500">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="alignJustified:" target="-1" id="523"/>
</connections>
</menuItem>
<menuItem title="Align Right" keyEquivalent="}" id="501">
<connections>
<action selector="alignRight:" target="-1" id="521"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="502"/>
<menuItem title="Writing Direction" id="503">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Writing Direction" id="508">
<items>
<menuItem title="Paragraph" enabled="NO" id="509">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="510">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="525"/>
</connections>
</menuItem>
<menuItem id="511">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="526"/>
</connections>
</menuItem>
<menuItem id="512">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="527"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="513"/>
<menuItem title="Selection" enabled="NO" id="514">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem id="515">
<string key="title"> Default</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionNatural:" target="-1" id="528"/>
</connections>
</menuItem>
<menuItem id="516">
<string key="title"> Left to Right</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="529"/>
</connections>
</menuItem>
<menuItem id="517">
<string key="title"> Right to Left</string>
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="530"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="504"/>
<menuItem title="Show Ruler" id="505">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleRuler:" target="-1" id="520"/>
</connections>
</menuItem>
<menuItem title="Copy Ruler" keyEquivalent="c" id="506">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="copyRuler:" target="-1" id="522"/>
</connections>
</menuItem>
<menuItem title="Paste Ruler" keyEquivalent="v" id="507">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="pasteRuler:" target="-1" id="519"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="295">
<menu key="submenu" title="View" id="296">
<items>
<menuItem title="Show Toolbar" keyEquivalent="t" id="297">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="toggleToolbarShown:" target="-1" id="366"/>
</connections>
</menuItem>
<menuItem title="Customize Toolbar…" id="298">
<connections>
<action selector="runToolbarCustomizationPalette:" target="-1" id="365"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="19">
<menu key="submenu" title="Window" systemMenu="window" id="24">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="23">
<connections>
<action selector="performMiniaturize:" target="-1" id="37"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="239">
<connections>
<action selector="performZoom:" target="-1" id="240"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="92">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Bring All to Front" id="5">
<connections>
<action selector="arrangeInFront:" target="-1" id="39"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="490">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="491">
<items>
<menuItem title="Demo Help" keyEquivalent="?" id="492">
<connections>
<action selector="showHelp:" target="-1" id="493"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<window title="Demo" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="DemoWindow" animationBehavior="default" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="335" y="390" width="393" height="129"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="393" height="129"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView id="536" customClass="MASShortcutView">
<rect key="frame" x="142" y="90" width="158" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</customView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="PG0-jh-Onh">
<rect key="frame" x="18" y="92" width="111" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Custom shortcut:" id="85u-1A-n7H">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button id="zCi-ki-Uuh">
<rect key="frame" x="140" y="63" width="169" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Enable custom shortcut" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="Y47-p3-sDa">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="rCO-Ve-DT5" name="value" keyPath="values.customShortcutEnabled" id="VjS-3V-VdA"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="KnS-ut-phz">
<rect key="frame" x="18" y="65" width="111" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Options:" id="cUE-gA-heG">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button id="F4r-KM-wn9">
<rect key="frame" x="140" y="43" width="235" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Enable hard-coded shortcut (⌘F2)" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="7gv-jN-44g">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="rCO-Ve-DT5" name="value" keyPath="values.hardcodedShortcutEnabled" id="dlZ-si-HeN"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="9fB-XS-8pK">
<rect key="frame" x="18" y="20" width="111" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Feedback:" id="Zbz-mV-NDc">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="Aso-dH-W8I">
<rect key="frame" x="140" y="20" width="211" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" placeholderString="Press a shortcut to see feedback" id="Ckx-v7-e6x">
<font key="font" metaFont="system"/>
<color key="textColor" red="0.37647062540054321" green="0.85098046064376831" blue="0.17647059261798859" alpha="1" colorSpace="deviceRGB"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<point key="canvasLocation" x="352.5" y="367.5"/>
</window>
<customObject id="494" customClass="AppDelegate">
<connections>
<outlet property="customShortcutView" destination="536" id="aO6-hh-1vm"/>
<outlet property="feedbackTextField" destination="Aso-dH-W8I" id="hk8-xL-ieC"/>
<outlet property="window" destination="371" id="532"/>
</connections>
</customObject>
<customObject id="420" customClass="NSFontManager"/>
<userDefaultsController representsSharedInstance="YES" id="rCO-Ve-DT5"/>
</objects>
</document>
+2
View File
@@ -0,0 +1,2 @@
#import <Cocoa/Cocoa.h>
#import <MASShortcut/Shortcut.h>
+4
View File
@@ -0,0 +1,4 @@
int main(int argc, char *argv[])
{
return NSApplicationMain(argc, (const char **)argv);
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<key>CFBundleVersion</key>
<string>2.1.0</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 20142015 Vadim Shpakovski. All rights reserved.</string>
</dict>
</plist>
+19
View File
@@ -0,0 +1,19 @@
extern NSString *const MASDictionaryTransformerName;
/**
Converts shortcuts for storage in user defaults.
User defaults cant stored custom types directly, they have to
be serialized to `NSData` or some other supported type like an
`NSDictionary`. In Cocoa Bindings, the conversion can be done
using value transformers like this one.
Theres a built-in transformer (`NSKeyedUnarchiveFromDataTransformerName`)
that converts any `NSCoding` types to `NSData`, but with shortcuts
it makes sense to use a dictionary instead the defaults look better
when inspected with the `defaults` command-line utility and the
format is compatible with an older sortcut library called Shortcut
Recorder.
*/
@interface MASDictionaryTransformer : NSValueTransformer
@end
+51
View File
@@ -0,0 +1,51 @@
#import "MASDictionaryTransformer.h"
#import "MASShortcut.h"
NSString *const MASDictionaryTransformerName = @"MASDictionaryTransformer";
static NSString *const MASKeyCodeKey = @"keyCode";
static NSString *const MASModifierFlagsKey = @"modifierFlags";
@implementation MASDictionaryTransformer
+ (BOOL) allowsReverseTransformation
{
return YES;
}
// Storing nil values as an empty dictionary lets us differ between
// “not available, use default value” and “explicitly set to none”.
// See http://stackoverflow.com/questions/5540760 for details.
- (NSDictionary*) reverseTransformedValue: (MASShortcut*) shortcut
{
if (shortcut == nil) {
return [NSDictionary dictionary];
} else {
return @{
MASKeyCodeKey: @([shortcut keyCode]),
MASModifierFlagsKey: @([shortcut modifierFlags])
};
}
}
- (MASShortcut*) transformedValue: (NSDictionary*) dictionary
{
// We have to be defensive here as the value may come from user defaults.
if (![dictionary isKindOfClass:[NSDictionary class]]) {
return nil;
}
id keyCodeBox = [dictionary objectForKey:MASKeyCodeKey];
id modifierFlagsBox = [dictionary objectForKey:MASModifierFlagsKey];
SEL integerValue = @selector(integerValue);
if (![keyCodeBox respondsToSelector:integerValue] || ![modifierFlagsBox respondsToSelector:integerValue]) {
return nil;
}
return [MASShortcut
shortcutWithKeyCode:[keyCodeBox integerValue]
modifierFlags:[modifierFlagsBox integerValue]];
}
@end
+32
View File
@@ -0,0 +1,32 @@
@interface MASDictionaryTransformerTests : XCTestCase
@end
@implementation MASDictionaryTransformerTests
- (void) testErrorHandling
{
MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
XCTAssertNil([transformer transformedValue:nil],
@"Decoding a shortcut from a nil dictionary returns nil.");
XCTAssertNil([transformer transformedValue:(id)@"foo"],
@"Decoding a shortcut from a invalid-type dictionary returns nil.");
XCTAssertNil([transformer transformedValue:@{}],
@"Decoding a shortcut from an empty dictionary returns nil.");
XCTAssertNil([transformer transformedValue:@{@"keyCode":@"foo"}],
@"Decoding a shortcut from a wrong-typed dictionary returns nil.");
XCTAssertNil([transformer transformedValue:@{@"keyCode":@1}],
@"Decoding a shortcut from an incomplete dictionary returns nil.");
XCTAssertNil([transformer transformedValue:@{@"modifierFlags":@1}],
@"Decoding a shortcut from an incomplete dictionary returns nil.");
}
- (void) testNilRepresentation
{
MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
XCTAssertEqualObjects([transformer reverseTransformedValue:nil], [NSDictionary dictionary],
@"Store nil values as an empty dictionary.");
XCTAssertNil([transformer transformedValue:[NSDictionary dictionary]],
@"Load empty dictionary as nil.");
}
@end
+12
View File
@@ -0,0 +1,12 @@
#import "MASShortcut.h"
extern FourCharCode const MASHotKeySignature;
@interface MASHotKey : NSObject
@property(readonly) UInt32 carbonID;
@property(copy) dispatch_block_t action;
+ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut;
@end
+44
View File
@@ -0,0 +1,44 @@
#import "MASHotKey.h"
FourCharCode const MASHotKeySignature = 'MASS';
@interface MASHotKey ()
@property(assign) EventHotKeyRef hotKeyRef;
@property(assign) UInt32 carbonID;
@end
@implementation MASHotKey
- (instancetype) initWithShortcut: (MASShortcut*) shortcut
{
self = [super init];
static UInt32 CarbonHotKeyID = 0;
_carbonID = ++CarbonHotKeyID;
EventHotKeyID hotKeyID = { .signature = MASHotKeySignature, .id = _carbonID };
OSStatus status = RegisterEventHotKey([shortcut carbonKeyCode], [shortcut carbonFlags],
hotKeyID, GetEventDispatcherTarget(), 0, &_hotKeyRef);
if (status != noErr) {
return nil;
}
return self;
}
+ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut
{
return [[self alloc] initWithShortcut:shortcut];
}
- (void) dealloc
{
if (_hotKeyRef) {
UnregisterEventHotKey(_hotKeyRef);
_hotKeyRef = NULL;
}
}
@end
+15
View File
@@ -0,0 +1,15 @@
#import "MASHotKey.h"
@interface MASHotKeyTests : XCTestCase
@end
@implementation MASHotKeyTests
- (void) testBasicFunctionality
{
MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:
[MASShortcut shortcutWithKeyCode:kVK_ANSI_H modifierFlags:NSCommandKeyMask|NSAlternateKeyMask]];
XCTAssertNotNil(hotKey, @"Register a simple Cmd-Alt-H hotkey.");
}
@end
+42
View File
@@ -0,0 +1,42 @@
#import <Carbon/Carbon.h>
// These glyphs are missed in Carbon.h
enum {
kMASShortcutGlyphEject = 0x23CF,
kMASShortcutGlyphClear = 0x2715,
kMASShortcutGlyphDeleteLeft = 0x232B,
kMASShortcutGlyphDeleteRight = 0x2326,
kMASShortcutGlyphLeftArrow = 0x2190,
kMASShortcutGlyphRightArrow = 0x2192,
kMASShortcutGlyphUpArrow = 0x2191,
kMASShortcutGlyphDownArrow = 0x2193,
kMASShortcutGlyphEscape = 0x238B,
kMASShortcutGlyphHelp = 0x003F,
kMASShortcutGlyphPageDown = 0x21DF,
kMASShortcutGlyphPageUp = 0x21DE,
kMASShortcutGlyphTabRight = 0x21E5,
kMASShortcutGlyphReturn = 0x2305,
kMASShortcutGlyphReturnR2L = 0x21A9,
kMASShortcutGlyphPadClear = 0x2327,
kMASShortcutGlyphNorthwestArrow = 0x2196,
kMASShortcutGlyphSoutheastArrow = 0x2198,
} MASShortcutGlyph;
NS_INLINE NSString* NSStringFromMASKeyCode(unsigned short ch)
{
return [NSString stringWithFormat:@"%C", ch];
}
NS_INLINE NSUInteger MASPickCocoaModifiers(NSUInteger flags)
{
return (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask));
}
NS_INLINE UInt32 MASCarbonModifiersFromCocoaModifiers(NSUInteger cocoaFlags)
{
return
(cocoaFlags & NSCommandKeyMask ? cmdKey : 0)
| (cocoaFlags & NSAlternateKeyMask ? optionKey : 0)
| (cocoaFlags & NSControlKeyMask ? controlKey : 0)
| (cocoaFlags & NSShiftKeyMask ? shiftKey : 0);
}
+70
View File
@@ -0,0 +1,70 @@
#import "MASKeyCodes.h"
/**
A model class to hold a key combination.
This class just represents a combination of keys. It does not care if
the combination is valid or can be used as a hotkey, it doesnt watch
the input system for the shortcut appearance, nor it does access user
defaults.
*/
@interface MASShortcut : NSObject <NSSecureCoding, NSCopying>
/**
The virtual key code for the keyboard key.
Hardware independent, same as in `NSEvent`. See `Events.h` in the HIToolbox
framework for a complete list, or Command-click this symbol: `kVK_ANSI_A`.
*/
@property (nonatomic, readonly) NSUInteger keyCode;
/**
Cocoa keyboard modifier flags.
Same as in `NSEvent`: `NSCommandKeyMask`, `NSAlternateKeyMask`, etc.
*/
@property (nonatomic, readonly) NSUInteger modifierFlags;
/**
Same as `keyCode`, just a different type.
*/
@property (nonatomic, readonly) UInt32 carbonKeyCode;
/**
Carbon modifier flags.
A bit sum of `cmdKey`, `optionKey`, etc.
*/
@property (nonatomic, readonly) UInt32 carbonFlags;
/**
A string representing the “key” part of a shortcut, like the `5` in `⌘5`.
*/
@property (nonatomic, readonly) NSString *keyCodeString;
/**
A key-code string used in key equivalent matching.
For precise meaning of “key equivalents” see the `keyEquivalent`
property of `NSMenuItem`. Here the string is used to support shortcut
validation (“is the shortcut already taken in this menu?”) and
for display in `NSMenu`.
*/
@property (nonatomic, readonly) NSString *keyCodeStringForKeyEquivalent;
/**
A string representing the shortcut modifiers, like the `⌘` in `⌘5`.
*/
@property (nonatomic, readonly) NSString *modifierFlagsString;
- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
/**
Creates a new shortcut from an `NSEvent` object.
This is just a convenience initializer that reads the key code and modifiers from an `NSEvent`.
*/
+ (instancetype)shortcutWithEvent:(NSEvent *)anEvent;
@end
+241
View File
@@ -0,0 +1,241 @@
#import "MASShortcut.h"
static NSString *const MASShortcutKeyCode = @"KeyCode";
static NSString *const MASShortcutModifierFlags = @"ModifierFlags";
@implementation MASShortcut
#pragma mark Initialization
- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
self = [super init];
if (self) {
_keyCode = code;
_modifierFlags = MASPickCocoaModifiers(flags);
}
return self;
}
+ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
return [[self alloc] initWithKeyCode:code modifierFlags:flags];
}
+ (instancetype)shortcutWithEvent:(NSEvent *)event
{
return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
}
#pragma mark Shortcut Accessors
- (UInt32)carbonKeyCode
{
return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
}
- (UInt32)carbonFlags
{
return MASCarbonModifiersFromCocoaModifiers(self.modifierFlags);
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
}
- (NSString *)keyCodeStringForKeyEquivalent
{
NSString *keyCodeString = self.keyCodeString;
if (keyCodeString.length > 1) {
switch (self.keyCode) {
case kVK_F1: return NSStringFromMASKeyCode(0xF704);
case kVK_F2: return NSStringFromMASKeyCode(0xF705);
case kVK_F3: return NSStringFromMASKeyCode(0xF706);
case kVK_F4: return NSStringFromMASKeyCode(0xF707);
case kVK_F5: return NSStringFromMASKeyCode(0xF708);
case kVK_F6: return NSStringFromMASKeyCode(0xF709);
case kVK_F7: return NSStringFromMASKeyCode(0xF70a);
case kVK_F8: return NSStringFromMASKeyCode(0xF70b);
case kVK_F9: return NSStringFromMASKeyCode(0xF70c);
case kVK_F10: return NSStringFromMASKeyCode(0xF70d);
case kVK_F11: return NSStringFromMASKeyCode(0xF70e);
case kVK_F12: return NSStringFromMASKeyCode(0xF70f);
// From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
case kVK_F13: return NSStringFromMASKeyCode(0xF710);
case kVK_F14: return NSStringFromMASKeyCode(0xF711);
case kVK_F15: return NSStringFromMASKeyCode(0xF712);
case kVK_F16: return NSStringFromMASKeyCode(0xF713);
case kVK_F17: return NSStringFromMASKeyCode(0xF714);
case kVK_F18: return NSStringFromMASKeyCode(0xF715);
case kVK_F19: return NSStringFromMASKeyCode(0xF716);
case kVK_Space: return NSStringFromMASKeyCode(0x20);
default: return @"";
}
}
return keyCodeString.lowercaseString;
}
- (NSString *)keyCodeString
{
// Some key codes don't have an equivalent
switch (self.keyCode) {
case NSNotFound: return @"";
case kVK_F1: return @"F1";
case kVK_F2: return @"F2";
case kVK_F3: return @"F3";
case kVK_F4: return @"F4";
case kVK_F5: return @"F5";
case kVK_F6: return @"F6";
case kVK_F7: return @"F7";
case kVK_F8: return @"F8";
case kVK_F9: return @"F9";
case kVK_F10: return @"F10";
case kVK_F11: return @"F11";
case kVK_F12: return @"F12";
case kVK_F13: return @"F13";
case kVK_F14: return @"F14";
case kVK_F15: return @"F15";
case kVK_F16: return @"F16";
case kVK_F17: return @"F17";
case kVK_F18: return @"F18";
case kVK_F19: return @"F19";
case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
case kVK_Escape: return NSStringFromMASKeyCode(kMASShortcutGlyphEscape);
case kVK_Delete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteLeft);
case kVK_ForwardDelete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteRight);
case kVK_LeftArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphLeftArrow);
case kVK_RightArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphRightArrow);
case kVK_UpArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphUpArrow);
case kVK_DownArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphDownArrow);
case kVK_Help: return NSStringFromMASKeyCode(kMASShortcutGlyphHelp);
case kVK_PageUp: return NSStringFromMASKeyCode(kMASShortcutGlyphPageUp);
case kVK_PageDown: return NSStringFromMASKeyCode(kMASShortcutGlyphPageDown);
case kVK_Tab: return NSStringFromMASKeyCode(kMASShortcutGlyphTabRight);
case kVK_Return: return NSStringFromMASKeyCode(kMASShortcutGlyphReturnR2L);
// Keypad
case kVK_ANSI_Keypad0: return @"0";
case kVK_ANSI_Keypad1: return @"1";
case kVK_ANSI_Keypad2: return @"2";
case kVK_ANSI_Keypad3: return @"3";
case kVK_ANSI_Keypad4: return @"4";
case kVK_ANSI_Keypad5: return @"5";
case kVK_ANSI_Keypad6: return @"6";
case kVK_ANSI_Keypad7: return @"7";
case kVK_ANSI_Keypad8: return @"8";
case kVK_ANSI_Keypad9: return @"9";
case kVK_ANSI_KeypadDecimal: return @".";
case kVK_ANSI_KeypadMultiply: return @"*";
case kVK_ANSI_KeypadPlus: return @"+";
case kVK_ANSI_KeypadClear: return NSStringFromMASKeyCode(kMASShortcutGlyphPadClear);
case kVK_ANSI_KeypadDivide: return @"/";
case kVK_ANSI_KeypadEnter: return NSStringFromMASKeyCode(kMASShortcutGlyphReturn);
case kVK_ANSI_KeypadMinus: return @"";
case kVK_ANSI_KeypadEquals: return @"=";
// Hardcode
case 119: return NSStringFromMASKeyCode(kMASShortcutGlyphSoutheastArrow);
case 115: return NSStringFromMASKeyCode(kMASShortcutGlyphNorthwestArrow);
}
// Everything else should be printable so look it up in the current keyboard
OSStatus error = noErr;
NSString *keystroke = nil;
TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
if (inputSource) {
CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
if (layoutDataRef) {
UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
UniCharCount length = 0;
UniChar chars[256] = { 0 };
UInt32 deadKeyState = 0;
error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
sizeof(chars) / sizeof(UniChar), &length, chars);
keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
}
CFRelease(inputSource);
}
// Validate keystroke
if (keystroke.length) {
static NSMutableCharacterSet *validChars = nil;
if (validChars == nil) {
validChars = [[NSMutableCharacterSet alloc] init];
[validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
[validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
[validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
}
for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
keystroke = @"";
break;
}
}
}
// Finally, we've got a shortcut!
return keystroke.uppercaseString;
}
- (NSString *)modifierFlagsString
{
unichar chars[4];
NSUInteger count = 0;
// These are in the same order as the menu manager shows them
if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode;
if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode;
if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode;
if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode;
return (count ? [NSString stringWithCharacters:chars length:count] : @"");
}
#pragma mark NSObject
- (BOOL) isEqual: (MASShortcut*) object
{
return [object isKindOfClass:[self class]]
&& (object.keyCode == self.keyCode)
&& (object.modifierFlags == self.modifierFlags);
}
- (NSUInteger) hash
{
return self.keyCode + self.modifierFlags;
}
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
[coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
_keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
_modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
}
return self;
}
#pragma mark NSSecureCoding
+ (BOOL)supportsSecureCoding
{
return YES;
}
#pragma mark NSCopying
- (instancetype) copyWithZone:(NSZone *)zone
{
return [[self class] shortcutWithKeyCode:_keyCode modifierFlags:_modifierFlags];
}
@end
+67
View File
@@ -0,0 +1,67 @@
#import "MASShortcutMonitor.h"
/**
Binds actions to user defaults keys.
If you store shortcuts in user defaults (for example by binding
a `MASShortcutView` to user defaults), you can use this class to
connect an action directly to a user defaults key. If the shortcut
stored under the key changes, the action will get automatically
updated to the new one.
This class is mostly a wrapper around a `MASShortcutMonitor`. It
watches the changes in user defaults and updates the shortcut monitor
accordingly with the new shortcuts.
*/
@interface MASShortcutBinder : NSObject
/**
A convenience shared instance.
You may use it so that you dont have to manage an instance by hand,
but its perfectly fine to allocate and use a separate instance instead.
*/
+ (instancetype) sharedBinder;
/**
The underlying shortcut monitor.
*/
@property(strong) MASShortcutMonitor *shortcutMonitor;
/**
Binding options customizing the access to user defaults.
As an example, you can use `NSValueTransformerNameBindingOption` to customize
the storage format used for the shortcuts. By default the shortcuts are converted
from `NSData` (`NSKeyedUnarchiveFromDataTransformerName`). Note that if the
binder is to work with `MASShortcutView`, both object have to use the same storage
format.
*/
@property(copy) NSDictionary *bindingOptions;
/**
Binds given action to a shortcut stored under the given defaults key.
In other words, no matter what shortcut you store under the given key,
pressing it will always trigger the given action.
*/
- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action;
/**
Disconnect the binding between user defaults and action.
In other words, the shortcut stored under the given key will no longer trigger an action.
*/
- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName;
/**
Register default shortcuts in user defaults.
This is a convenience frontent to `[NSUserDefaults registerDefaults]`.
The dictionary should contain a map of user defaults keys to appropriate
keyboard shortcuts. The shortcuts will be transformed according to
`bindingOptions` and registered using `registerDefaults`.
*/
- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts;
@end
+114
View File
@@ -0,0 +1,114 @@
#import "MASShortcutBinder.h"
#import "MASShortcut.h"
@interface MASShortcutBinder ()
@property(strong) NSMutableDictionary *actions;
@property(strong) NSMutableDictionary *shortcuts;
@end
@implementation MASShortcutBinder
#pragma mark Initialization
- (id) init
{
self = [super init];
[self setActions:[NSMutableDictionary dictionary]];
[self setShortcuts:[NSMutableDictionary dictionary]];
[self setShortcutMonitor:[MASShortcutMonitor sharedMonitor]];
[self setBindingOptions:@{NSValueTransformerNameBindingOption: NSKeyedUnarchiveFromDataTransformerName}];
return self;
}
- (void) dealloc
{
for (NSString *bindingName in [_actions allKeys]) {
[self unbind:bindingName];
}
}
+ (instancetype) sharedBinder
{
static dispatch_once_t once;
static MASShortcutBinder *sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
#pragma mark Registration
- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action
{
[_actions setObject:[action copy] forKey:defaultsKeyName];
[self bind:defaultsKeyName toObject:[NSUserDefaultsController sharedUserDefaultsController]
withKeyPath:[@"values." stringByAppendingString:defaultsKeyName] options:_bindingOptions];
}
- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName
{
[_shortcutMonitor unregisterShortcut:[_shortcuts objectForKey:defaultsKeyName]];
[_shortcuts removeObjectForKey:defaultsKeyName];
[_actions removeObjectForKey:defaultsKeyName];
[self unbind:defaultsKeyName];
}
- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts
{
NSValueTransformer *transformer = [_bindingOptions valueForKey:NSValueTransformerBindingOption];
if (transformer == nil) {
NSString *transformerName = [_bindingOptions valueForKey:NSValueTransformerNameBindingOption];
if (transformerName) {
transformer = [NSValueTransformer valueTransformerForName:transformerName];
}
}
NSAssert(transformer != nil, @"Cant register default shortcuts without a transformer.");
[defaultShortcuts enumerateKeysAndObjectsUsingBlock:^(NSString *defaultsKey, MASShortcut *shortcut, BOOL *stop) {
id value = [transformer reverseTransformedValue:shortcut];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{defaultsKey:value}];
}];
}
#pragma mark Bindings
- (BOOL) isRegisteredAction: (NSString*) name
{
return !![_actions objectForKey:name];
}
- (id) valueForUndefinedKey: (NSString*) key
{
return [self isRegisteredAction:key] ?
[_shortcuts objectForKey:key] :
[super valueForUndefinedKey:key];
}
- (void) setValue: (id) value forUndefinedKey: (NSString*) key
{
if (![self isRegisteredAction:key]) {
[super setValue:value forUndefinedKey:key];
return;
}
MASShortcut *newShortcut = value;
MASShortcut *currentShortcut = [_shortcuts objectForKey:key];
// Unbind previous shortcut if any
if (currentShortcut != nil) {
[_shortcutMonitor unregisterShortcut:currentShortcut];
}
// Just deleting the old shortcut
if (newShortcut == nil) {
return;
}
// Bind new shortcut
[_shortcuts setObject:newShortcut forKey:key];
[_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]];
}
@end
+98
View File
@@ -0,0 +1,98 @@
static NSString *const SampleDefaultsKey = @"sampleShortcut";
@interface MASShortcutBinderTests : XCTestCase
@property(strong) MASShortcutBinder *binder;
@property(strong) MASShortcutMonitor *monitor;
@property(strong) NSUserDefaults *defaults;
@end
@implementation MASShortcutBinderTests
- (void) setUp
{
[super setUp];
[self setBinder:[[MASShortcutBinder alloc] init]];
[self setMonitor:[_binder shortcutMonitor]];
[self setDefaults:[[NSUserDefaults alloc] init]];
[_defaults removeObjectForKey:SampleDefaultsKey];
}
- (void) tearDown
{
[_monitor unregisterAllShortcuts];
[self setMonitor:nil];
[self setDefaults:nil];
[self setBinder:nil];
[super tearDown];
}
- (void) testInitialValueReading
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
[_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
@"Pass the initial shortcut from defaults to shortcut monitor.");
}
- (void) testValueChangeReading
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
[_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
@"Pass the shortcut from defaults to shortcut monitor after defaults change.");
}
- (void) testValueClearing
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
[_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
[_defaults removeObjectForKey:SampleDefaultsKey];
XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
@"Unregister shortcut from monitor after value is cleared from defaults.");
}
- (void) testBindingRemoval
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
[_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
[_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
@"Unregister shortcut from monitor after binding was removed.");
}
- (void) testRebinding
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
[_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
[_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
@"Bind after unbinding.");
}
- (void) testTransformerDeserialization
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
NSDictionary *storedShortcut = @{@"keyCode": @5, @"modifierFlags": @1048576};
[_defaults setObject:storedShortcut forKey:SampleDefaultsKey];
[_binder setBindingOptions:@{NSValueTransformerBindingOption:[MASDictionaryTransformer new]}];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
@"Deserialize shortcut from user defaults using a custom transformer.");
}
- (void) testDefaultShortcuts
{
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
[_binder registerDefaultShortcuts:@{SampleDefaultsKey: shortcut}];
[_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
@"Bind shortcut using a default value.");
}
@end
+27
View File
@@ -0,0 +1,27 @@
#import "MASShortcut.h"
/**
Executes action when a shortcut is pressed.
There can only be one instance of this class, otherwise things
will probably not work. (Theres a Carbon event handler inside
and there can only be one Carbon event handler of a given type.)
*/
@interface MASShortcutMonitor : NSObject
- (instancetype) init __unavailable;
+ (instancetype) sharedMonitor;
/**
Register a shortcut along with an action.
Attempting to insert an already registered shortcut probably wont work.
It may burn your house or cut your fingers. You have been warned.
*/
- (BOOL) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action;
- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut;
- (void) unregisterShortcut: (MASShortcut*) shortcut;
- (void) unregisterAllShortcuts;
@end
+108
View File
@@ -0,0 +1,108 @@
#import "MASShortcutMonitor.h"
#import "MASHotKey.h"
@interface MASShortcutMonitor ()
@property(assign) EventHandlerRef eventHandlerRef;
@property(strong) NSMutableDictionary *hotKeys;
@end
static OSStatus MASCarbonEventCallback(EventHandlerCallRef, EventRef, void*);
@implementation MASShortcutMonitor
#pragma mark Initialization
- (instancetype) init
{
self = [super init];
[self setHotKeys:[NSMutableDictionary dictionary]];
EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), MASCarbonEventCallback,
1, &hotKeyPressedSpec, (__bridge void*)self, &_eventHandlerRef);
if (status != noErr) {
return nil;
}
return self;
}
- (void) dealloc
{
if (_eventHandlerRef) {
RemoveEventHandler(_eventHandlerRef);
_eventHandlerRef = NULL;
}
}
+ (instancetype) sharedMonitor
{
static dispatch_once_t once;
static MASShortcutMonitor *sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
#pragma mark Registration
- (BOOL) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action
{
MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:shortcut];
if (hotKey) {
[hotKey setAction:action];
[_hotKeys setObject:hotKey forKey:shortcut];
return YES;
} else {
return NO;
}
}
- (void) unregisterShortcut: (MASShortcut*) shortcut
{
if (shortcut) {
[_hotKeys removeObjectForKey:shortcut];
}
}
- (void) unregisterAllShortcuts
{
[_hotKeys removeAllObjects];
}
- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut
{
return !![_hotKeys objectForKey:shortcut];
}
#pragma mark Event Handling
- (void) handleEvent: (EventRef) event
{
if (GetEventClass(event) != kEventClassKeyboard) {
return;
}
EventHotKeyID hotKeyID;
OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
if (status != noErr || hotKeyID.signature != MASHotKeySignature) {
return;
}
[_hotKeys enumerateKeysAndObjectsUsingBlock:^(MASShortcut *shortcut, MASHotKey *hotKey, BOOL *stop) {
if (hotKeyID.id == [hotKey carbonID]) {
if ([hotKey action]) {
dispatch_async(dispatch_get_main_queue(), [hotKey action]);
}
*stop = YES;
}
}];
}
@end
static OSStatus MASCarbonEventCallback(EventHandlerCallRef _, EventRef event, void *context)
{
MASShortcutMonitor *dispatcher = (__bridge id)context;
[dispatcher handleEvent:event];
return noErr;
}
+23
View File
@@ -0,0 +1,23 @@
#import "MASShortcutMonitor.h"
@interface MASShortcutMonitorTests : XCTestCase
@end
@implementation MASShortcutMonitorTests
- (void) testMonitorCreation
{
XCTAssertNotNil([MASShortcutMonitor sharedMonitor], @"Create a shared shortcut monitor.");
}
- (void) testShortcutRegistration
{
MASShortcutMonitor *monitor = [MASShortcutMonitor sharedMonitor];
MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:kVK_ANSI_H modifierFlags:NSCommandKeyMask|NSAlternateKeyMask];
XCTAssertTrue([monitor registerShortcut:shortcut withAction:NULL], @"Register a shortcut.");
XCTAssertTrue([monitor isShortcutRegistered:shortcut], @"Remember a previously registered shortcut.");
[monitor unregisterShortcut:shortcut];
XCTAssertFalse([monitor isShortcutRegistered:shortcut], @"Forget shortcut after unregistering.");
}
@end
+26
View File
@@ -0,0 +1,26 @@
@interface MASShortcutTests : XCTestCase
@end
@implementation MASShortcutTests
- (void) testEquality
{
MASShortcut *keyA = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
MASShortcut *keyB = [MASShortcut shortcutWithKeyCode:2 modifierFlags:NSControlKeyMask];
MASShortcut *keyC = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSAlternateKeyMask];
MASShortcut *keyD = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
XCTAssertTrue([keyA isEqual:keyA], @"Shortcut is equal to itself.");
XCTAssertTrue([keyA isEqual:[keyA copy]], @"Shortcut is equal to its copy.");
XCTAssertFalse([keyA isEqual:keyB], @"Shortcuts not equal when key codes differ.");
XCTAssertFalse([keyA isEqual:keyC], @"Shortcuts not equal when modifier flags differ.");
XCTAssertTrue([keyA isEqual:keyD], @"Shortcuts are equal when key codes and modifiers are.");
XCTAssertFalse([keyA isEqual:@"foo"], @"Shortcut not equal to an object of a different class.");
}
- (void) testShortcutRecorderCompatibility
{
MASShortcut *key = [MASShortcut shortcutWithKeyCode:87 modifierFlags:1048576];
XCTAssertEqualObjects([key description], @"⌘5", @"Basic compatibility with the keycode & modifier combination used by Shortcut Recorder.");
}
@end
+15
View File
@@ -0,0 +1,15 @@
#import "MASShortcut.h"
@interface MASShortcutValidator : NSObject
// The following API enable hotkeys with the Option key as the only modifier
// For example, Option-G will not generate © and Option-R will not paste ®
@property(assign) BOOL allowAnyShortcutWithOptionModifier;
+ (instancetype) sharedValidator;
- (BOOL) isShortcutValid: (MASShortcut*) shortcut;
- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation;
- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation;
@end
+111
View File
@@ -0,0 +1,111 @@
#import "MASShortcutValidator.h"
@implementation MASShortcutValidator
+ (instancetype) sharedValidator
{
static dispatch_once_t once;
static MASShortcutValidator *sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (BOOL) isShortcutValid: (MASShortcut*) shortcut
{
NSUInteger keyCode = [shortcut keyCode];
NSUInteger modifiers = [shortcut modifierFlags];
// Allow any function key with any combination of modifiers
BOOL includesFunctionKey = ((keyCode == kVK_F1) || (keyCode == kVK_F2) || (keyCode == kVK_F3) || (keyCode == kVK_F4) ||
(keyCode == kVK_F5) || (keyCode == kVK_F6) || (keyCode == kVK_F7) || (keyCode == kVK_F8) ||
(keyCode == kVK_F9) || (keyCode == kVK_F10) || (keyCode == kVK_F11) || (keyCode == kVK_F12) ||
(keyCode == kVK_F13) || (keyCode == kVK_F14) || (keyCode == kVK_F15) || (keyCode == kVK_F16) ||
(keyCode == kVK_F17) || (keyCode == kVK_F18) || (keyCode == kVK_F19) || (keyCode == kVK_F20));
if (includesFunctionKey) return YES;
// Do not allow any other key without modifiers
BOOL hasModifierFlags = (modifiers > 0);
if (!hasModifierFlags) return NO;
// Allow any hotkey containing Control or Command modifier
BOOL includesCommand = ((modifiers & NSCommandKeyMask) > 0);
BOOL includesControl = ((modifiers & NSControlKeyMask) > 0);
if (includesCommand || includesControl) return YES;
// Allow Option key only in selected cases
BOOL includesOption = ((modifiers & NSAlternateKeyMask) > 0);
if (includesOption) {
// Always allow Option-Space and Option-Escape because they do not have any bind system commands
if ((keyCode == kVK_Space) || (keyCode == kVK_Escape)) return YES;
// Allow Option modifier with any key even if it will break the system binding
if (_allowAnyShortcutWithOptionModifier) return YES;
}
// The hotkey does not have any modifiers or violates system bindings
return NO;
}
- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation
{
NSString *keyEquivalent = [shortcut keyCodeStringForKeyEquivalent];
NSUInteger flags = [shortcut modifierFlags];
for (NSMenuItem *menuItem in menu.itemArray) {
if (menuItem.hasSubmenu && [self isShortcut:shortcut alreadyTakenInMenu:[menuItem submenu] explanation:explanation]) return YES;
BOOL equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask) == flags);
BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent];
// Check if the cases are different, we know ours is lower and that shift is included in our modifiers
// If theirs is capitol, we need to add shift to their modifiers
if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) {
equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
}
if (equalFlags && equalHotkeyLowercase) {
if (explanation) {
*explanation = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item %@.",
@"Message for alert when shortcut is already used");
*explanation = [NSString stringWithFormat:*explanation, menuItem.title];
}
return YES;
}
}
return NO;
}
- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation
{
CFArrayRef globalHotKeys;
if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {
// Enumerate all global hotkeys and check if any of them matches current shortcut
for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);
if (([(__bridge NSNumber *)code unsignedIntegerValue] == [shortcut keyCode]) &&
([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags]) &&
([(__bridge NSNumber *)enabled boolValue])) {
if (explanation) {
*explanation = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide "
@"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
@"can be changed in the Keyboard & Mouse panel in System Preferences.",
@"Message for alert when shortcut is already used by the system");
}
return YES;
}
}
CFRelease(globalHotKeys);
}
return [self isShortcut:shortcut alreadyTakenInMenu:[NSApp mainMenu] explanation:explanation];
}
@end
+25
View File
@@ -0,0 +1,25 @@
#import "MASShortcutView.h"
/**
A simplified interface to bind the recorder value to user defaults.
You can bind the `shortcutValue` to user defaults using the standard
`bind:toObject:withKeyPath:options:` call, but since thats a lot to type
and read, heres a simpler option.
Setting the `associatedUserDefaultsKey` binds the views shortcut value
to the given user defaults key. You can supply a value transformer to convert
values between user defaults and `MASShortcut`. If you dont supply
a transformer, the `NSUnarchiveFromDataTransformerName` will be used
automatically.
Set `associatedUserDefaultsKey` to `nil` to disconnect the binding.
*/
@interface MASShortcutView (Bindings)
@property(copy) NSString *associatedUserDefaultsKey;
- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer;
- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName;
@end
+50
View File
@@ -0,0 +1,50 @@
#import "MASShortcutView+Bindings.h"
@implementation MASShortcutView (Bindings)
- (NSString*) associatedUserDefaultsKey
{
NSDictionary* bindingInfo = [self infoForBinding:MASShortcutBinding];
if (bindingInfo != nil) {
NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
NSString *key = [keyPath stringByReplacingOccurrencesOfString:@"values." withString:@""];
return key;
} else {
return nil;
}
}
- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer
{
// Break previous binding if any
NSString *currentKey = [self associatedUserDefaultsKey];
if (currentKey != nil) {
[self unbind:currentKey];
}
// Stop if the new binding is nil
if (newKey == nil) {
return;
}
NSDictionary *options = transformer ?
@{NSValueTransformerBindingOption:transformer} :
nil;
[self bind:MASShortcutBinding
toObject:[NSUserDefaultsController sharedUserDefaultsController]
withKeyPath:[@"values." stringByAppendingString:newKey]
options:options];
}
- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName
{
[self setAssociatedUserDefaultsKey:newKey withTransformer:[NSValueTransformer valueTransformerForName:transformerName]];
}
- (void) setAssociatedUserDefaultsKey: (NSString*) newKey
{
[self setAssociatedUserDefaultsKey:newKey withTransformerName:NSKeyedUnarchiveFromDataTransformerName];
}
@end
+24
View File
@@ -0,0 +1,24 @@
@class MASShortcut, MASShortcutValidator;
extern NSString *const MASShortcutBinding;
typedef enum {
MASShortcutViewStyleDefault = 0, // Height = 19 px
MASShortcutViewStyleTexturedRect, // Height = 25 px
MASShortcutViewStyleRounded, // Height = 43 px
MASShortcutViewStyleFlat
} MASShortcutViewStyle;
@interface MASShortcutView : NSView
@property (nonatomic, strong) MASShortcut *shortcutValue;
@property (nonatomic, strong) MASShortcutValidator *shortcutValidator;
@property (nonatomic, getter = isRecording) BOOL recording;
@property (nonatomic, getter = isEnabled) BOOL enabled;
@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
@property (nonatomic, assign) MASShortcutViewStyle style;
/// Returns custom class for drawing control.
+ (Class)shortcutCellClass;
@end
+137 -40
View File
@@ -1,5 +1,7 @@
#import "MASShortcutView.h"
#import "MASShortcut.h"
#import "MASShortcutValidator.h"
NSString *const MASShortcutBinding = @"shortcutValue";
#define HINT_BUTTON_WIDTH 23.0
#define BUTTON_FONT_SIZE 11.0
@@ -23,28 +25,41 @@
NSTrackingArea *_hintArea;
}
@synthesize enabled = _enabled;
@synthesize hinting = _hinting;
@synthesize shortcutValue = _shortcutValue;
@synthesize shortcutPlaceholder = _shortcutPlaceholder;
@synthesize shortcutValueChange = _shortcutValueChange;
@synthesize recording = _recording;
#pragma mark -
+ (Class)shortcutCellClass
{
return [NSButtonCell class];
}
- (id)initWithFrame:(CGRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self) {
_shortcutCell = [[NSButtonCell alloc] init];
_shortcutCell.buttonType = NSPushOnPushOffButton;
_shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
_enabled = YES;
[self resetShortcutCellStyle];
[self commonInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
_shortcutCell = [[[self.class shortcutCellClass] alloc] init];
_shortcutCell.buttonType = NSPushOnPushOffButton;
_shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
_shortcutValidator = [MASShortcutValidator sharedValidator];
_enabled = YES;
[self resetShortcutCellStyle];
}
- (void)dealloc
{
[self activateEventMonitoring:NO];
@@ -63,10 +78,10 @@
}
}
- (void)setAppearance:(MASShortcutViewAppearance)appearance
- (void)setStyle:(MASShortcutViewStyle)newStyle
{
if (_appearance != appearance) {
_appearance = appearance;
if (_style != newStyle) {
_style = newStyle;
[self resetShortcutCellStyle];
[self setNeedsDisplay:YES];
}
@@ -74,15 +89,25 @@
- (void)resetShortcutCellStyle
{
switch (_appearance) {
case MASShortcutViewAppearanceDefault: {
switch (_style) {
case MASShortcutViewStyleDefault: {
_shortcutCell.bezelStyle = NSRoundRectBezelStyle;
break;
}
case MASShortcutViewAppearanceTexturedRect: {
case MASShortcutViewStyleTexturedRect: {
_shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
break;
}
case MASShortcutViewStyleRounded: {
_shortcutCell.bezelStyle = NSRoundedBezelStyle;
break;
}
case MASShortcutViewStyleFlat: {
self.wantsLayer = YES;
_shortcutCell.backgroundColor = [NSColor clearColor];
_shortcutCell.bordered = NO;
break;
}
}
}
@@ -113,6 +138,7 @@
_shortcutValue = shortcutValue;
[self resetToolTips];
[self setNeedsDisplay:YES];
[self propagateValue:shortcutValue forBinding:@"shortcutValue"];
if (self.shortcutValueChange) {
self.shortcutValueChange(self);
@@ -139,29 +165,37 @@
_shortcutCell.state = state;
_shortcutCell.enabled = self.enabled;
switch (_appearance) {
case MASShortcutViewAppearanceDefault: {
switch (_style) {
case MASShortcutViewStyleDefault: {
[_shortcutCell drawWithFrame:frame inView:self];
break;
}
case MASShortcutViewAppearanceTexturedRect: {
case MASShortcutViewStyleTexturedRect: {
[_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
break;
}
case MASShortcutViewStyleRounded: {
[_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
break;
}
case MASShortcutViewStyleFlat: {
[_shortcutCell drawWithFrame:frame inView:self];
break;
}
}
}
- (void)drawRect:(CGRect)dirtyRect
{
if (self.shortcutValue) {
[self drawInRect:self.bounds withTitle:MASShortcutChar(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft)
[self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphClear)
alignment:NSRightTextAlignment state:NSOffState];
CGRect shortcutRect;
[self getShortcutRect:&shortcutRect hintRect:NULL];
NSString *title = (self.recording
? (_hinting
? NSLocalizedString(@"Use Old Shortuct", @"Cancel action button for non-empty shortcut in recording state")
? NSLocalizedString(@"Use Old Shortcut", @"Cancel action button for non-empty shortcut in recording state")
: (self.shortcutPlaceholder.length > 0
? self.shortcutPlaceholder
: NSLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state")))
@@ -171,7 +205,7 @@
else {
if (self.recording)
{
[self drawInRect:self.bounds withTitle:MASShortcutChar(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState];
[self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState];
CGRect shortcutRect;
[self getShortcutRect:&shortcutRect hintRect:NULL];
@@ -195,7 +229,14 @@
- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef
{
CGRect shortcutRect, hintRect;
CGRectDivide(self.bounds, &hintRect, &shortcutRect, HINT_BUTTON_WIDTH, CGRectMaxXEdge);
CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
switch (self.style) {
case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break;
case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break;
case MASShortcutViewStyleFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
default: break;
}
CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
if (shortcutRectRef) *shortcutRectRef = shortcutRect;
if (hintRectRef) *hintRectRef = hintRect;
}
@@ -328,41 +369,50 @@ void *kUserDataHint = &kUserDataHint;
static id eventMonitor = nil;
if (shouldActivate) {
__weak MASShortcutView *weakSelf = self;
__unsafe_unretained MASShortcutView *weakSelf = self;
NSEventMask eventMask = (NSKeyDownMask | NSFlagsChangedMask);
eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
// Create a shortcut from the event
MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) {
// Delete shortcut
// If the shortcut is a plain Delete or Backspace, clear the current shortcut and cancel recording
if (!shortcut.modifierFlags && ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete))) {
weakSelf.shortcutValue = nil;
weakSelf.recording = NO;
event = nil;
}
else if (shortcut.keyCode == kVK_Escape) {
// Cancel recording
// If the shortcut is a plain Esc, cancel recording
else if (!shortcut.modifierFlags && shortcut.keyCode == kVK_Escape) {
weakSelf.recording = NO;
event = nil;
}
else if (shortcut.shouldBypass) {
// Command + W, Command + Q, ESC should deactivate recorder
// If the shortcut is Cmd-W or Cmd-Q, cancel recording and pass the event through
else if ((shortcut.modifierFlags == NSCommandKeyMask) && (shortcut.keyCode == kVK_ANSI_W || shortcut.keyCode == kVK_ANSI_Q)) {
weakSelf.recording = NO;
}
else {
// Verify possible shortcut
if (shortcut.keyCodeString.length > 0) {
if (shortcut.valid) {
if ([_shortcutValidator isShortcutValid:shortcut]) {
// Verify that shortcut is not used
NSError *error = nil;
if ([shortcut isTakenError:&error]) {
NSString *explanation = nil;
if ([_shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) {
// Prevent cancel of recording when Alert window is key
[weakSelf activateResignObserver:NO];
[weakSelf activateEventMonitoring:NO];
NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
@"Title for alert when shortcut is already used");
NSRunCriticalAlertPanel([NSString stringWithFormat:format, shortcut], error.localizedDescription,
NSLocalizedString(@"OK", @"Alert button when shortcut is already used"),
nil, nil);
NSAlert* alert = [[NSAlert alloc]init];
alert.alertStyle = NSCriticalAlertStyle;
alert.informativeText = explanation;
alert.messageText = [NSString stringWithFormat:format, shortcut];
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
[alert runModal];
weakSelf.shortcutPlaceholder = nil;
[weakSelf activateResignObserver:YES];
[weakSelf activateEventMonitoring:YES];
@@ -400,7 +450,7 @@ void *kUserDataHint = &kUserDataHint;
static id observer = nil;
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
if (shouldActivate) {
__weak MASShortcutView *weakSelf = self;
__unsafe_unretained MASShortcutView *weakSelf = self;
observer = [notificationCenter addObserverForName:NSWindowDidResignKeyNotification object:self.window
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
weakSelf.recording = NO;
@@ -411,4 +461,51 @@ void *kUserDataHint = &kUserDataHint;
}
}
#pragma mark Bindings
// http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/
-(void) propagateValue:(id)value forBinding:(NSString*)binding;
{
NSParameterAssert(binding != nil);
//WARNING: bindingInfo contains NSNull, so it must be accounted for
NSDictionary* bindingInfo = [self infoForBinding:binding];
if(!bindingInfo)
return; //there is no binding
//apply the value transformer, if one has been set
NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey];
if(bindingOptions){
NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption];
if(!transformer || (id)transformer == [NSNull null]){
NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption];
if(transformerName && (id)transformerName != [NSNull null]){
transformer = [NSValueTransformer valueTransformerForName:transformerName];
}
}
if(transformer && (id)transformer != [NSNull null]){
if([[transformer class] allowsReverseTransformation]){
value = [transformer reverseTransformedValue:value];
} else {
NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__);
}
}
}
id boundObject = [bindingInfo objectForKey:NSObservedObjectKey];
if(!boundObject || boundObject == [NSNull null]){
NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
return;
}
NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){
NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
return;
}
[boundObject setValue:value forKeyPath:boundKeyPath];
}
@end
+2
View File
@@ -0,0 +1,2 @@
#import <AppKit/AppKit.h>
#import <Carbon/Carbon.h>
+7
View File
@@ -0,0 +1,7 @@
#import "MASShortcut.h"
#import "MASShortcutValidator.h"
#import "MASShortcutMonitor.h"
#import "MASShortcutBinder.h"
#import "MASDictionaryTransformer.h"
#import "MASShortcutView.h"
#import "MASShortcutView+Bindings.h"
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2012-2013, Vadim Shpakovski
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-9
View File
@@ -1,9 +0,0 @@
#import "MASShortcut.h"
@interface MASShortcut (UserDefaults)
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey;
@end
-158
View File
@@ -1,158 +0,0 @@
#import "MASShortcut+UserDefaults.h"
@interface MASShortcutHotKey : NSObject
@property (nonatomic, readonly) NSString *userDefaultsKey;
@property (nonatomic, readonly, copy) void (^handler)();
@property (nonatomic, readonly) EventHotKeyRef carbonHotKey;
@property (nonatomic, readonly) UInt32 carbonHotKeyID;
- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
+ (void)uninstallEventHandler;
@end
#pragma mark -
@implementation MASShortcut (UserDefaults)
+ (NSMutableDictionary *)registeredHotKeys
{
static NSMutableDictionary *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [NSMutableDictionary dictionary];
});
return shared;
}
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
{
MASShortcutHotKey *hotKey = [[MASShortcutHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler];
[[self registeredHotKeys] setObject:hotKey forKey:userDefaultsKey];
}
+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey
{
NSMutableDictionary *registeredHotKeys = [self registeredHotKeys];
[registeredHotKeys removeObjectForKey:userDefaultsKey];
if (registeredHotKeys.count == 0) {
[MASShortcutHotKey uninstallEventHandler];
}
}
@end
#pragma mark -
@implementation MASShortcutHotKey
@synthesize carbonHotKeyID = _carbonHotKeyID;
@synthesize handler = _handler;
@synthesize userDefaultsKey = _userDefaultsKey;
@synthesize carbonHotKey = _carbonHotKey;
#pragma mark -
- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
{
self = [super init];
if (self) {
_userDefaultsKey = userDefaultsKey.copy;
_handler = [handler copy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:)
name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
[self installHotKeyFromUserDefaults];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
[self uninstallExisitingHotKey];
}
#pragma mark -
- (void)userDefaultsDidChange:(NSNotification *)note
{
[self uninstallExisitingHotKey];
[self installHotKeyFromUserDefaults];
}
#pragma mark - Carbon events
static EventHandlerRef sEventHandler = NULL;
+ (BOOL)installCommonEventHandler
{
if (sEventHandler == NULL) {
EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), CarbonCallback, 1, &hotKeyPressedSpec, NULL, &sEventHandler);
if (status != noErr) {
sEventHandler = NULL;
return NO;
}
}
return YES;
}
+ (void)uninstallEventHandler
{
if (sEventHandler) {
RemoveEventHandler(sEventHandler);
sEventHandler = NULL;
}
}
#pragma mark -
- (void)uninstallExisitingHotKey
{
if (_carbonHotKey) {
UnregisterEventHotKey(_carbonHotKey);
_carbonHotKey = NULL;
}
}
FourCharCode const kMASShortcutSignature = 'MASS';
- (void)installHotKeyFromUserDefaults
{
NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:_userDefaultsKey];
MASShortcut *shortcut = [MASShortcut shortcutWithData:data];
if ((shortcut == nil) || ![[self class] installCommonEventHandler]) return;
static UInt32 sCarbonHotKeyID = 0;
_carbonHotKeyID = ++ sCarbonHotKeyID;
EventHotKeyID hotKeyID = { .signature = kMASShortcutSignature, .id = _carbonHotKeyID };
if (RegisterEventHotKey(shortcut.carbonKeyCode, shortcut.carbonFlags, hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &_carbonHotKey) != noErr) {
_carbonHotKey = NULL;
}
}
static OSStatus CarbonCallback(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData)
{
if (GetEventClass(inEvent) != kEventClassKeyboard) return noErr;
EventHotKeyID hotKeyID;
OSStatus status = GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
if (status != noErr) return status;
if (hotKeyID.signature != kMASShortcutSignature) return noErr;
[[MASShortcut registeredHotKeys] enumerateKeysAndObjectsUsingBlock:^(id key, MASShortcutHotKey *hotKey, BOOL *stop) {
if (hotKeyID.id == hotKey.carbonHotKeyID) {
if (hotKey.handler) {
hotKey.handler();
}
*stop = YES;
}
}];
return noErr;
}
@end
-53
View File
@@ -1,53 +0,0 @@
#import <Carbon/Carbon.h>
#define MASShortcutChar(char) [NSString stringWithFormat:@"%C", (unsigned short)(char)]
#define MASShortcutClear(flags) (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
#define MASShortcutCarbonFlags(cocoaFlags) (\
(cocoaFlags & NSCommandKeyMask ? cmdKey : 0) | \
(cocoaFlags & NSAlternateKeyMask ? optionKey : 0) | \
(cocoaFlags & NSControlKeyMask ? controlKey : 0) | \
(cocoaFlags & NSShiftKeyMask ? shiftKey : 0))
// These glyphs are missed in Carbon.h
enum {
kMASShortcutGlyphEject = 0x23CF,
kMASShortcutGlyphClear = 0x2715,
kMASShortcutGlyphDeleteLeft = 0x232B,
kMASShortcutGlyphDeleteRight = 0x2326,
kMASShortcutGlyphLeftArrow = 0x2190,
kMASShortcutGlyphRightArrow = 0x2192,
kMASShortcutGlyphUpArrow = 0x2191,
kMASShortcutGlyphDownArrow = 0x2193,
kMASShortcutGlyphEscape = 0x238B,
kMASShortcutGlyphHelp = 0x003F,
kMASShortcutGlyphPageDown = 0x21DF,
kMASShortcutGlyphPageUp = 0x21DE,
kMASShortcutGlyphTabRight = 0x21E5,
kMASShortcutGlyphReturn = 0x2305,
kMASShortcutGlyphReturnR2L = 0x21A9,
kMASShortcutGlyphPadClear = 0x2327,
kMASShortcutGlyphNorthwestArrow = 0x2196,
kMASShortcutGlyphSoutheastArrow = 0x2198,
} MASShortcutGlyph;
@interface MASShortcut : NSObject <NSCoding>
@property (nonatomic) NSUInteger keyCode;
@property (nonatomic) NSUInteger modifierFlags;
@property (nonatomic, readonly) UInt32 carbonKeyCode;
@property (nonatomic, readonly) UInt32 carbonFlags;
@property (nonatomic, readonly) NSString *keyCodeString;
@property (nonatomic, readonly) NSString *modifierFlagsString;
@property (nonatomic, readonly) NSData *data;
@property (nonatomic, readonly) BOOL shouldBypass;
@property (nonatomic, readonly, getter = isValid) BOOL valid;
- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+ (MASShortcut *)shortcutWithEvent:(NSEvent *)anEvent;
+ (MASShortcut *)shortcutWithData:(NSData *)aData;
- (BOOL)isTakenError:(NSError **)error;
@end
-271
View File
@@ -1,271 +0,0 @@
#import "MASShortcut.h"
NSString *const kMASShortcutKeyCode = @"KeyCode";
NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
@implementation MASShortcut {
NSUInteger _keyCode; // NSNotFound if empty
NSUInteger _modifierFlags; // 0 if empty
}
@synthesize modifierFlags = _modifierFlags;
@synthesize keyCode = _keyCode;
#pragma mark -
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:kMASShortcutKeyCode];
[coder encodeInteger:(NSInteger)self.modifierFlags forKey:kMASShortcutModifierFlags];
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
NSInteger code = [decoder decodeIntegerForKey:kMASShortcutKeyCode];
self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
self.modifierFlags = [decoder decodeIntegerForKey:kMASShortcutModifierFlags];
}
return self;
}
- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
self = [super init];
if (self) {
_keyCode = code;
_modifierFlags = MASShortcutClear(flags);
}
return self;
}
+ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
{
return [[self alloc] initWithKeyCode:code modifierFlags:flags];
}
+ (MASShortcut *)shortcutWithEvent:(NSEvent *)event
{
return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
}
+ (MASShortcut *)shortcutWithData:(NSData *)data
{
return (data ? (MASShortcut *)[NSKeyedUnarchiver unarchiveObjectWithData:data] : nil);
}
#pragma mark - Shortcut accessors
- (NSData *)data
{
return [NSKeyedArchiver archivedDataWithRootObject:self];
}
- (void)setModifierFlags:(NSUInteger)value
{
_modifierFlags = MASShortcutClear(value);
}
- (UInt32)carbonKeyCode
{
return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
}
- (UInt32)carbonFlags
{
return MASShortcutCarbonFlags(self.modifierFlags);
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
}
- (NSString *)keyCodeString
{
// Some key codes don't have an equivalent
switch (self.keyCode) {
case NSNotFound: return @"";
case kVK_F1: return @"F1";
case kVK_F2: return @"F2";
case kVK_F3: return @"F3";
case kVK_F4: return @"F4";
case kVK_F5: return @"F5";
case kVK_F6: return @"F6";
case kVK_F7: return @"F7";
case kVK_F8: return @"F8";
case kVK_F9: return @"F9";
case kVK_F10: return @"F10";
case kVK_F11: return @"F11";
case kVK_F12: return @"F12";
case kVK_F13: return @"F13";
case kVK_F14: return @"F14";
case kVK_F15: return @"F15";
case kVK_F16: return @"F16";
case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
case kVK_Escape: return MASShortcutChar(kMASShortcutGlyphEscape);
case kVK_Delete: return MASShortcutChar(kMASShortcutGlyphDeleteLeft);
case kVK_ForwardDelete: return MASShortcutChar(kMASShortcutGlyphDeleteRight);
case kVK_LeftArrow: return MASShortcutChar(kMASShortcutGlyphLeftArrow);
case kVK_RightArrow: return MASShortcutChar(kMASShortcutGlyphRightArrow);
case kVK_UpArrow: return MASShortcutChar(kMASShortcutGlyphUpArrow);
case kVK_DownArrow: return MASShortcutChar(kMASShortcutGlyphDownArrow);
case kVK_Help: return MASShortcutChar(kMASShortcutGlyphHelp);
case kVK_PageUp: return MASShortcutChar(kMASShortcutGlyphPageUp);
case kVK_PageDown: return MASShortcutChar(kMASShortcutGlyphPageDown);
case kVK_Tab: return MASShortcutChar(kMASShortcutGlyphTabRight);
case kVK_Return: return MASShortcutChar(kMASShortcutGlyphReturnR2L);
// Keypad
case kVK_ANSI_Keypad0: return @"0";
case kVK_ANSI_Keypad1: return @"1";
case kVK_ANSI_Keypad2: return @"2";
case kVK_ANSI_Keypad3: return @"3";
case kVK_ANSI_Keypad4: return @"4";
case kVK_ANSI_Keypad5: return @"5";
case kVK_ANSI_Keypad6: return @"6";
case kVK_ANSI_Keypad7: return @"7";
case kVK_ANSI_Keypad8: return @"8";
case kVK_ANSI_Keypad9: return @"9";
case kVK_ANSI_KeypadDecimal: return @".";
case kVK_ANSI_KeypadMultiply: return @"*";
case kVK_ANSI_KeypadPlus: return @"+";
case kVK_ANSI_KeypadClear: return MASShortcutChar(kMASShortcutGlyphPadClear);
case kVK_ANSI_KeypadDivide: return @"/";
case kVK_ANSI_KeypadEnter: return MASShortcutChar(kMASShortcutGlyphReturn);
case kVK_ANSI_KeypadMinus: return @"";
case kVK_ANSI_KeypadEquals: return @"=";
// Hardcode
case 119: return MASShortcutChar(kMASShortcutGlyphSoutheastArrow);
case 115: return MASShortcutChar(kMASShortcutGlyphNorthwestArrow);
}
// Everything else should be printable so look it up in the current keyboard
OSStatus error = noErr;
NSString *keystroke = nil;
TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
if (inputSource) {
CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
if (layoutDataRef) {
UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
UniCharCount length = 0;
UniChar chars[256] = { 0 };
UInt32 deadKeyState = 0;
error = UCKeyTranslate(layoutData, self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
sizeof(chars) / sizeof(UniChar), &length, chars);
keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
}
CFRelease(inputSource);
}
// Validate keystroke
if (keystroke.length) {
static NSMutableCharacterSet *validChars = nil;
if (validChars == nil) {
validChars = [[NSMutableCharacterSet alloc] init];
[validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
[validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
[validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
}
for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
keystroke = @"";
break;
}
}
}
// Finally, we've got a shortcut!
return keystroke.uppercaseString;
}
- (NSString *)modifierFlagsString
{
unichar chars[4];
NSUInteger count = 0;
// These are in the same order as the menu manager shows them
if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode;
if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode;
if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode;
if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode;
return (count ? [NSString stringWithCharacters:chars length:count] : @"");
}
#pragma mark - Validation logic
- (BOOL)shouldBypass
{
NSString *codeString = self.keyCodeString;
return (self.modifierFlags == NSCommandKeyMask) && ([codeString isEqualToString:@"W"] || [codeString isEqualToString:@"Q"]);
}
- (BOOL)isValid
{
BOOL hasFlags = (_modifierFlags > 0);
BOOL hasCommand = ((_modifierFlags & NSCommandKeyMask) > 0);
BOOL hasControl = ((_modifierFlags & NSControlKeyMask) > 0);
BOOL hasOption = ((_modifierFlags & NSAlternateKeyMask) > 0);
BOOL isFunction = ((_keyCode == kVK_F1) || (_keyCode == kVK_F2) || (_keyCode == kVK_F3) || (_keyCode == kVK_F4) ||
(_keyCode == kVK_F5) || (_keyCode == kVK_F6) || (_keyCode == kVK_F7) || (_keyCode == kVK_F8) ||
(_keyCode == kVK_F9) || (_keyCode == kVK_F10) || (_keyCode == kVK_F11) || (_keyCode == kVK_F12) ||
(_keyCode == kVK_F13) || (_keyCode == kVK_F14) || (_keyCode == kVK_F15) || (_keyCode == kVK_F16) ||
(_keyCode == kVK_F17) || (_keyCode == kVK_F18) || (_keyCode == kVK_F19) || (_keyCode == kVK_F20));
BOOL isSpecial = ((_keyCode == kVK_Space) || (_keyCode == kVK_Escape) || (_keyCode == kVK_Return));
return ((hasFlags && (hasCommand || hasControl || (hasOption && isSpecial))) || isFunction);
}
- (BOOL)isKeyEquivalent:(NSString *)keyEquivalent flags:(NSUInteger)flags takenInMenu:(NSMenu *)menu error:(NSError **)outError
{
for (NSMenuItem *menuItem in menu.itemArray) {
if (menuItem.hasSubmenu && [self isKeyEquivalent:keyEquivalent flags:flags takenInMenu:menuItem.submenu error:outError]) return YES;
BOOL equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask) == flags);
BOOL equalHotkey = [menuItem.keyEquivalent.uppercaseString isEqualToString:keyEquivalent];
if (equalFlags && equalHotkey) {
if (outError) {
NSString *format = NSLocalizedString(@"This shortcut cannot be used used because it is already used by the menu item %@.",
@"Message for alert when shortcut is already used");
NSDictionary *info = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:format, menuItem.title]
forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
}
return YES;
}
}
return NO;
}
- (BOOL)isTakenError:(NSError **)outError
{
CFArrayRef globalHotKeys;
if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {
// Enumerate all global hotkeys and check if any of them matches current shortcut
for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
if (([(__bridge NSNumber *)code unsignedIntegerValue] == self.keyCode) &&
([(__bridge NSNumber *)flags unsignedIntegerValue] == self.carbonFlags)) {
if (outError) {
NSString *description = NSLocalizedString(@"This combination cannot be used used because it is already used by a system-wide "
@"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
@"can be changed in the Keyboard & Mouse panel in System Preferences.",
@"Message for alert when shortcut is already used by the system");
NSDictionary *info = [NSDictionary dictionaryWithObject:description forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
}
return YES;
}
}
CFRelease(globalHotKeys);
}
return [self isKeyEquivalent:self.keyCodeString flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError];
}
@end
+17
View File
@@ -0,0 +1,17 @@
Pod::Spec.new do |s|
s.name = 'MASShortcut'
s.version = '2.1.0'
s.summary = 'Modern framework for managing global keyboard shortcuts compatible with Mac App Store'
s.homepage = 'https://github.com/shpakovski/MASShortcut'
s.license = 'BSD 2-clause'
s.authors = { 'Vadim Shpakovski' => 'vadim@shpakovski.com',
'Tomáš Znamenáček' => 'tomas.znamenacek@gmail.com' }
s.platform = :osx
s.osx.deployment_target = "10.6"
s.source = { :git => 'https://github.com/shpakovski/MASShortcut.git', :tag => '2.1.0' }
s.source_files = 'Framework/*.{h,m}'
s.exclude_files = 'Framework/*Tests.m'
s.osx.frameworks = 'Carbon', 'AppKit'
s.requires_arc = true
end
+662
View File
@@ -0,0 +1,662 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
0D39DCA21A668A4400639145 /* MASHotKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D39DCA11A668A4400639145 /* MASHotKeyTests.m */; };
0D39DCA41A668E5500639145 /* MASShortcutMonitorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */; };
0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; };
0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D1B1990D55E0010B8EF /* MASShortcut.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D1C1990D55E0010B8EF /* MASShortcut.m */; };
0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D211990D55E0010B8EF /* MASShortcutView.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D221990D55E0010B8EF /* MASShortcutView.m */; };
0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; };
0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6A1990D6110010B8EF /* AppDelegate.m */; };
0D827D711990D6110010B8EF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6D1990D6110010B8EF /* main.m */; };
0D827D721990D6110010B8EF /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0D827D6E1990D6110010B8EF /* MainMenu.xib */; };
0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; };
0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
0D827D771990F81E0010B8EF /* Shortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D761990F81E0010B8EF /* Shortcut.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9319910B740010B8EF /* MASShortcutTests.m */; };
0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; };
0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9619910FF70010B8EF /* MASKeyCodes.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827D99199110F60010B8EF /* Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D98199110F60010B8EF /* Prefix.pch */; };
0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */; };
0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; };
0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DAB199132840010B8EF /* MASShortcutBinder.h */; settings = {ATTRIBUTES = (Public, ); }; };
0DC2F17619922798003A0131 /* MASHotKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F17419922798003A0131 /* MASHotKey.h */; };
0DC2F17719922798003A0131 /* MASHotKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F17519922798003A0131 /* MASHotKey.m */; };
0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */; };
0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DAC199132840010B8EF /* MASShortcutBinder.m */; };
0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */; };
0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; };
0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */; };
0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */; };
0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */; settings = {ATTRIBUTES = (Public, ); }; };
0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0D827CCA1990D4420010B8EF /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0D827CD21990D4420010B8EF;
remoteInfo = MASShortcut;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
0D827D741990D6980010B8EF /* Copy Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */,
);
name = "Copy Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0D39DCA11A668A4400639145 /* MASHotKeyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASHotKeyTests.m; path = Framework/MASHotKeyTests.m; sourceTree = "<group>"; };
0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutMonitorTests.m; path = Framework/MASShortcutMonitorTests.m; sourceTree = "<group>"; };
0D827CD31990D4420010B8EF /* MASShortcut.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MASShortcut.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0D827CD61990D4420010B8EF /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
0D827CD91990D4420010B8EF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
0D827CDA1990D4420010B8EF /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
0D827CDB1990D4420010B8EF /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
0D827D1B1990D55E0010B8EF /* MASShortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcut.h; path = Framework/MASShortcut.h; sourceTree = "<group>"; };
0D827D1C1990D55E0010B8EF /* MASShortcut.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcut.m; path = Framework/MASShortcut.m; sourceTree = "<group>"; };
0D827D211990D55E0010B8EF /* MASShortcutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutView.h; path = Framework/MASShortcutView.h; sourceTree = "<group>"; };
0D827D221990D55E0010B8EF /* MASShortcutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutView.m; path = Framework/MASShortcutView.m; sourceTree = "<group>"; };
0D827D2F1990D5640010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Framework/Info.plist; sourceTree = "<group>"; };
0D827D371990D5E70010B8EF /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
0D827D691990D6110010B8EF /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
0D827D6A1990D6110010B8EF /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
0D827D6B1990D6110010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0D827D6C1990D6110010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = "<group>"; };
0D827D6D1990D6110010B8EF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
0D827D6E1990D6110010B8EF /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
0D827D761990F81E0010B8EF /* Shortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Shortcut.h; path = Framework/Shortcut.h; sourceTree = "<group>"; };
0D827D8319910AFF0010B8EF /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0D827D8719910AFF0010B8EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0D827D8D19910AFF0010B8EF /* Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = "<group>"; };
0D827D9319910B740010B8EF /* MASShortcutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutTests.m; path = Framework/MASShortcutTests.m; sourceTree = "<group>"; };
0D827D9619910FF70010B8EF /* MASKeyCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASKeyCodes.h; path = Framework/MASKeyCodes.h; sourceTree = "<group>"; };
0D827D98199110F60010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Prefix.pch; path = Framework/Prefix.pch; sourceTree = "<group>"; };
0D827D9C19911A190010B8EF /* MASShortcutValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutValidator.h; path = Framework/MASShortcutValidator.h; sourceTree = "<group>"; };
0D827D9D19911A190010B8EF /* MASShortcutValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutValidator.m; path = Framework/MASShortcutValidator.m; sourceTree = "<group>"; };
0D827DA319912D240010B8EF /* MASShortcutMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutMonitor.h; path = Framework/MASShortcutMonitor.h; sourceTree = "<group>"; };
0D827DA419912D240010B8EF /* MASShortcutMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutMonitor.m; path = Framework/MASShortcutMonitor.m; sourceTree = "<group>"; };
0D827DAB199132840010B8EF /* MASShortcutBinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutBinder.h; path = Framework/MASShortcutBinder.h; sourceTree = "<group>"; };
0D827DAC199132840010B8EF /* MASShortcutBinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutBinder.m; path = Framework/MASShortcutBinder.m; sourceTree = "<group>"; };
0DC2F17419922798003A0131 /* MASHotKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASHotKey.h; path = Framework/MASHotKey.h; sourceTree = "<group>"; };
0DC2F17519922798003A0131 /* MASHotKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASHotKey.m; path = Framework/MASHotKey.m; sourceTree = "<group>"; };
0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutBinderTests.m; path = Framework/MASShortcutBinderTests.m; sourceTree = "<group>"; };
0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASDictionaryTransformer.h; path = Framework/MASDictionaryTransformer.h; sourceTree = "<group>"; };
0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASDictionaryTransformer.m; path = Framework/MASDictionaryTransformer.m; sourceTree = "<group>"; };
0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASDictionaryTransformerTests.m; path = Framework/MASDictionaryTransformerTests.m; sourceTree = "<group>"; };
0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "MASShortcutView+Bindings.h"; path = "Framework/MASShortcutView+Bindings.h"; sourceTree = "<group>"; };
0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "MASShortcutView+Bindings.m"; path = "Framework/MASShortcutView+Bindings.m"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0D827CCF1990D4420010B8EF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D341990D5E70010B8EF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */,
0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D8019910AFF0010B8EF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0D827CC91990D4420010B8EF = {
isa = PBXGroup;
children = (
0D827D151990D4D70010B8EF /* Framework */,
0D827D8519910AFF0010B8EF /* Test Support */,
0D827D681990D6110010B8EF /* Demo */,
0D827CD51990D4420010B8EF /* Frameworks */,
0D827CD41990D4420010B8EF /* Products */,
);
sourceTree = "<group>";
};
0D827CD41990D4420010B8EF /* Products */ = {
isa = PBXGroup;
children = (
0D827CD31990D4420010B8EF /* MASShortcut.framework */,
0D827D371990D5E70010B8EF /* Demo.app */,
0D827D8319910AFF0010B8EF /* Tests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
0D827CD51990D4420010B8EF /* Frameworks */ = {
isa = PBXGroup;
children = (
0D827CD61990D4420010B8EF /* Cocoa.framework */,
0D827CD91990D4420010B8EF /* Foundation.framework */,
0D827CDA1990D4420010B8EF /* CoreData.framework */,
0D827CDB1990D4420010B8EF /* AppKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
0D827D151990D4D70010B8EF /* Framework */ = {
isa = PBXGroup;
children = (
0D827DA019912A660010B8EF /* Model */,
0D827DA219912A870010B8EF /* Monitoring */,
0DC2F18A19937060003A0131 /* User Defaults Storage */,
0D827DA119912A6D0010B8EF /* UI */,
0D827D2F1990D5640010B8EF /* Info.plist */,
0D827D98199110F60010B8EF /* Prefix.pch */,
0D827D761990F81E0010B8EF /* Shortcut.h */,
);
name = Framework;
sourceTree = "<group>";
};
0D827D681990D6110010B8EF /* Demo */ = {
isa = PBXGroup;
children = (
0D827D691990D6110010B8EF /* AppDelegate.h */,
0D827D6A1990D6110010B8EF /* AppDelegate.m */,
0D827D6E1990D6110010B8EF /* MainMenu.xib */,
0D827D6B1990D6110010B8EF /* Info.plist */,
0D827D6C1990D6110010B8EF /* Prefix.pch */,
0D827D6D1990D6110010B8EF /* main.m */,
);
path = Demo;
sourceTree = "<group>";
};
0D827D8519910AFF0010B8EF /* Test Support */ = {
isa = PBXGroup;
children = (
0D827D8719910AFF0010B8EF /* Info.plist */,
0D827D8D19910AFF0010B8EF /* Prefix.pch */,
);
name = "Test Support";
path = Tests;
sourceTree = "<group>";
};
0D827DA019912A660010B8EF /* Model */ = {
isa = PBXGroup;
children = (
0D827D9619910FF70010B8EF /* MASKeyCodes.h */,
0D827D1B1990D55E0010B8EF /* MASShortcut.h */,
0D827D1C1990D55E0010B8EF /* MASShortcut.m */,
0D827D9319910B740010B8EF /* MASShortcutTests.m */,
0D827D9C19911A190010B8EF /* MASShortcutValidator.h */,
0D827D9D19911A190010B8EF /* MASShortcutValidator.m */,
);
name = Model;
sourceTree = "<group>";
};
0D827DA119912A6D0010B8EF /* UI */ = {
isa = PBXGroup;
children = (
0D827D211990D55E0010B8EF /* MASShortcutView.h */,
0D827D221990D55E0010B8EF /* MASShortcutView.m */,
0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */,
0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */,
);
name = UI;
sourceTree = "<group>";
};
0D827DA219912A870010B8EF /* Monitoring */ = {
isa = PBXGroup;
children = (
0DC2F17419922798003A0131 /* MASHotKey.h */,
0DC2F17519922798003A0131 /* MASHotKey.m */,
0D39DCA11A668A4400639145 /* MASHotKeyTests.m */,
0D827DA319912D240010B8EF /* MASShortcutMonitor.h */,
0D827DA419912D240010B8EF /* MASShortcutMonitor.m */,
0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */,
);
name = Monitoring;
sourceTree = "<group>";
};
0DC2F18A19937060003A0131 /* User Defaults Storage */ = {
isa = PBXGroup;
children = (
0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */,
0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */,
0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */,
0D827DAB199132840010B8EF /* MASShortcutBinder.h */,
0D827DAC199132840010B8EF /* MASShortcutBinder.m */,
0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */,
);
name = "User Defaults Storage";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
0D827CD01990D4420010B8EF /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */,
0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */,
0D827D99199110F60010B8EF /* Prefix.pch in Headers */,
0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */,
0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */,
0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */,
0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */,
0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */,
0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */,
0DC2F17619922798003A0131 /* MASHotKey.h in Headers */,
0D827D771990F81E0010B8EF /* Shortcut.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
0D827CD21990D4420010B8EF /* MASShortcut */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */;
buildPhases = (
0D827CCE1990D4420010B8EF /* Sources */,
0D827CCF1990D4420010B8EF /* Frameworks */,
0D827CD01990D4420010B8EF /* Headers */,
0D827CD11990D4420010B8EF /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = MASShortcut;
productName = MASShortcut;
productReference = 0D827CD31990D4420010B8EF /* MASShortcut.framework */;
productType = "com.apple.product-type.framework";
};
0D827D361990D5E70010B8EF /* Demo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */;
buildPhases = (
0D827D331990D5E70010B8EF /* Sources */,
0D827D341990D5E70010B8EF /* Frameworks */,
0D827D351990D5E70010B8EF /* Resources */,
0D827D741990D6980010B8EF /* Copy Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Demo;
productName = Demo;
productReference = 0D827D371990D5E70010B8EF /* Demo.app */;
productType = "com.apple.product-type.application";
};
0D827D8219910AFF0010B8EF /* Tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "Tests" */;
buildPhases = (
0D827D7F19910AFF0010B8EF /* Sources */,
0D827D8019910AFF0010B8EF /* Frameworks */,
0D827D8119910AFF0010B8EF /* Resources */,
);
buildRules = (
);
dependencies = (
0D827D8F19910AFF0010B8EF /* PBXTargetDependency */,
);
name = Tests;
productName = Tests;
productReference = 0D827D8319910AFF0010B8EF /* Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
0D827CCA1990D4420010B8EF /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0610;
ORGANIZATIONNAME = "Vadim Shpakovski";
TargetAttributes = {
0D827D8219910AFF0010B8EF = {
TestTargetID = 0D827CD21990D4420010B8EF;
};
};
};
buildConfigurationList = 0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 0D827CC91990D4420010B8EF;
productRefGroup = 0D827CD41990D4420010B8EF /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
0D827CD21990D4420010B8EF /* MASShortcut */,
0D827D361990D5E70010B8EF /* Demo */,
0D827D8219910AFF0010B8EF /* Tests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0D827CD11990D4420010B8EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D351990D5E70010B8EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0D827D721990D6110010B8EF /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D8119910AFF0010B8EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
0D827CCE1990D4420010B8EF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0DC2F17719922798003A0131 /* MASHotKey.m in Sources */,
0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */,
0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */,
0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */,
0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */,
0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */,
0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */,
0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D331990D5E70010B8EF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0D827D711990D6110010B8EF /* main.m in Sources */,
0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D7F19910AFF0010B8EF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */,
0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */,
0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */,
0D39DCA21A668A4400639145 /* MASHotKeyTests.m in Sources */,
0D39DCA41A668E5500639145 /* MASShortcutMonitorTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0D827D8F19910AFF0010B8EF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0D827CD21990D4420010B8EF /* MASShortcut */;
targetProxy = 0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
0D827CF91990D4420010B8EF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.9;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
};
name = Debug;
};
0D827CFA1990D4420010B8EF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_ENABLE_OBJC_EXCEPTIONS = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.9;
SDKROOT = macosx;
};
name = Release;
};
0D827CFC1990D4420010B8EF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Framework/Prefix.pch;
INFOPLIST_FILE = Framework/Info.plist;
INSTALL_PATH = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.6;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
WRAPPER_EXTENSION = framework;
};
name = Debug;
};
0D827CFD1990D4420010B8EF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Framework/Prefix.pch;
INFOPLIST_FILE = Framework/Info.plist;
INSTALL_PATH = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.6;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
WRAPPER_EXTENSION = framework;
};
name = Release;
};
0D827D621990D5E70010B8EF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Demo/Prefix.pch;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = Demo/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 10.6;
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = app;
};
name = Debug;
};
0D827D631990D5E70010B8EF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Demo/Prefix.pch;
INFOPLIST_FILE = Demo/Info.plist;
MACOSX_DEPLOYMENT_TARGET = 10.6;
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = app;
};
name = Release;
};
0D827D9019910AFF0010B8EF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(DEVELOPER_FRAMEWORKS_DIR)",
"$(inherited)",
);
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Tests/Prefix.pch;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = xctest;
};
name = Debug;
};
0D827D9119910AFF0010B8EF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(DEVELOPER_FRAMEWORKS_DIR)",
"$(inherited)",
);
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Tests/Prefix.pch;
INFOPLIST_FILE = Tests/Info.plist;
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = xctest;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0D827CF91990D4420010B8EF /* Debug */,
0D827CFA1990D4420010B8EF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0D827CFC1990D4420010B8EF /* Debug */,
0D827CFD1990D4420010B8EF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0D827D621990D5E70010B8EF /* Debug */,
0D827D631990D5E70010B8EF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "Tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0D827D9019910AFF0010B8EF /* Debug */,
0D827D9119910AFF0010B8EF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 0D827CCA1990D4420010B8EF /* Project object */;
}
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D8219910AFF0010B8EF"
BuildableName = "Tests.xctest"
BlueprintName = "Tests"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D8219910AFF0010B8EF"
BuildableName = "Tests.xctest"
BlueprintName = "Tests"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-7
View File
@@ -1,7 +0,0 @@
#import "MASShortcutView.h"
@interface MASShortcutView (UserDefaults)
@property (nonatomic, copy) NSString *associatedUserDefaultsKey;
@end
-120
View File
@@ -1,120 +0,0 @@
#import "MASShortcutView+UserDefaults.h"
#import "MASShortcut.h"
#import <objc/runtime.h>
@interface MASShortcutDefaultsObserver : NSObject
@property (nonatomic, readonly) NSString *userDefaultsKey;
@property (nonatomic, readonly, weak) MASShortcutView *shortcutView;
- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey;
@end
#pragma mark -
@implementation MASShortcutView (UserDefaults)
void *kDefaultsObserver = &kDefaultsObserver;
- (NSString *)associatedUserDefaultsKey
{
MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, kDefaultsObserver);
return defaultsObserver.userDefaultsKey;
}
- (void)setAssociatedUserDefaultsKey:(NSString *)associatedUserDefaultsKey
{
MASShortcutDefaultsObserver *defaultsObserver = [[MASShortcutDefaultsObserver alloc] initWithShortcutView:self userDefaultsKey:associatedUserDefaultsKey];
objc_setAssociatedObject(self, kDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
#pragma mark -
@implementation MASShortcutDefaultsObserver {
MASShortcut *_originalShortcut;
BOOL _internalPreferenceChange;
BOOL _internalShortcutChange;
}
@synthesize userDefaultsKey = _userDefaultsKey;
@synthesize shortcutView = _shortcutView;
#pragma mark -
- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey
{
self = [super init];
if (self) {
_originalShortcut = shortcutView.shortcutValue;
_shortcutView = shortcutView;
_userDefaultsKey = userDefaultsKey.copy;
[self startObservingShortcutView];
}
return self;
}
- (void)dealloc
{
// __weak _shortcutView is not yet deallocated because it refers MASShortcutDefaultsObserver
[self stopObservingShortcutView];
}
#pragma mark -
void *kShortcutValueObserver = &kShortcutValueObserver;
- (void)startObservingShortcutView
{
// Read initial shortcut value from user preferences
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults dataForKey:_userDefaultsKey];
_shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
// Observe user preferences to update shortcut value when it changed
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:defaults];
// Observe the keyboard shortcut that user inputs by hand
[_shortcutView addObserver:self forKeyPath:@"shortcutValue" options:0 context:kShortcutValueObserver];
}
- (void)userDefaultsDidChange:(NSNotification *)note
{
// Ignore notifications posted from -[self observeValueForKeyPath:]
if (_internalPreferenceChange) return;
_internalShortcutChange = YES;
NSData *data = [note.object dataForKey:_userDefaultsKey];
_shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
_internalShortcutChange = NO;
}
- (void)stopObservingShortcutView
{
// Stop observing keyboard hotkeys entered by user in the shortcut view
[_shortcutView removeObserver:self forKeyPath:@"shortcutValue" context:kShortcutValueObserver];
// Stop observing user preferences
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
// Restore original hotkey in the shortcut view
_shortcutView.shortcutValue = _originalShortcut;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kShortcutValueObserver) {
if (_internalShortcutChange) return;
MASShortcut *shortcut = [object valueForKey:keyPath];
_internalPreferenceChange = YES;
[[NSUserDefaults standardUserDefaults] setObject:shortcut.data forKey:_userDefaultsKey];
_internalPreferenceChange = NO;
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
-16
View File
@@ -1,16 +0,0 @@
@class MASShortcut;
typedef enum {
MASShortcutViewAppearanceDefault = 0,
MASShortcutViewAppearanceTexturedRect
} MASShortcutViewAppearance;
@interface MASShortcutView : NSView
@property (nonatomic, strong) MASShortcut *shortcutValue;
@property (nonatomic, getter = isRecording) BOOL recording;
@property (nonatomic, getter = isEnabled) BOOL enabled;
@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
@property (nonatomic) MASShortcutViewAppearance appearance;
@end
+97 -16
View File
@@ -1,30 +1,111 @@
[![Build Status](https://travis-ci.org/shpakovski/MASShortcut.svg?branch=master)](https://travis-ci.org/shpakovski/MASShortcut)
# Intro
Some time ago Cocoa developers used a brilliant framework [ShortcutRecorder](http://wafflesoftware.net/shortcut/) for managing keyboard shortcuts in application preferences. However, it became incompatible with a new plugin architecture of Xcode 4.
Some time ago Cocoa developers used a brilliant framework [ShortcutRecorder](http://wafflesoftware.net/shortcut/) for managing keyboard shortcuts in application preferences. However, it became incompatible with the new plugin architecture of Xcode 4.
The project MASShortcut introduces modern API and user interface for recording, storing and using global keyboard shortcuts. All code is compatible with Xcode 4.3, Mac OS X 10.7 and the sandboxed environment.
The MASShortcut project introduces a modern API and user interface for recording, storing and using system-wide keyboard shortcuts. All code is compatible with recent Xcode & OS X versions and the sandboxed environment.
# Installation
You can use [CocoaPods](http://cocoapods.org/), adding the following line to your Podfile:
pod 'MASShortcut'
If you want to stick to the 1.x branch, you can use the version smart match operator:
pod 'MASShortcut', '~> 1'
# Usage
I hope, it is really easy:
// Drop a custom view into XIB and set its class to MASShortcutView
@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
// Think up a preference key to store a global shortcut between launches
NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
```objective-c
#import <MASShortcut/Shortcut.h>
// Assign the preference key and the shortcut view will take care of persistence
self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
// Drop a custom view into XIB, set its class to MASShortcutView
// and its height to 19. If you select another appearance style,
// look up the correct height values in MASShortcutView.h.
@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
// Execute your block of code automatically when user triggers a shortcut from preferences
[MASShortcut registerGlobalShortcutWithUserDefaultsKey:kPreferenceGlobalShortcut handler:^{
// Let me know if you find a better or more convenient API.
}];
// Pick a preference key to store the shortcut between launches
static NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
To set an example, I made a demo project: [MASShortcutDemo](https://github.com/shpakovski/MASShortcutDemo). Enjoy!
// Associate the shortcut view with user defaults
self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
// Associate the preference key with an action
[[MASShortcutBinder sharedBinder]
bindShortcutWithDefaultsKey:kPreferenceGlobalShortcut
toAction:^{
// Let me know if you find a better or a more convenient API.
}];
```
You can see a real usage example in the Demo target. Enjoy!
# Shortcut Recorder Compatibility
By default, MASShortcut uses a different User Defaults storage format incompatible with Shortcut Recorder. But its easily possible to change that, so that you can replace Shortcut Recorder with MASShortcut without having to migrate the shortcuts previously stored by your apps. There are two parts of the story:
If you bind the recorder control (`MASShortcutView`) to User defaults, set the Value Transformer field in the Interface Builder to `MASDictionaryTransformer`. This makes sure the shortcuts are written in the Shortcut Recorder format.
If you use `MASShortcutBinder` to automatically load shortcuts from User Defaults, set the `bindingOptions` accordingly:
```objective-c
[[MASShortcutBinder sharedBinder] setBindingOptions:@{NSValueTransformerNameBindingOption:MASDictionaryTransformerName}];
```
This makes sure that the shortcuts in the Shortcut Recorder format are loaded correctly.
# Notifications
By registering for KVO notifications from `NSUserDefaultsController`, you can get a callback whenever a user changes the shortcut, allowing you to perform any UI updates, or other code handling tasks.
This is just as easy to implement:
```objective-c
// Declare an ivar for key path in the user defaults controller
NSString *_observableKeyPath;
// Make a global context reference
void *kGlobalShortcutContext = &kGlobalShortcutContext;
// Implement when loading view
_observableKeyPath = [@"values." stringByAppendingString:kPreferenceGlobalShortcut];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath
options:NSKeyValueObservingOptionInitial
context:kGlobalShortcutContext];
// Capture the KVO change and do something
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
change:(NSDictionary *)change context:(void *)ctx
{
if (ctx == kGlobalShortcutContext) {
NSLog(@"Shortcut has changed");
}
else {
[super observeValueForKeyPath:keyPath ofObject:obj change:change context:ctx];
}
}
// Do not forget to remove the observer
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self
forKeyPath:_observableKeyPath
context:kGlobalShortcutContext];
```
# Using in Swift projects
1. Install as a Pod using the latest CocoaPods with Swift support.
2. Create a bridging header file [using the instructions here](http://swiftalicio.us/2014/11/using-cocoapods-from-swift/)
3. Your bridging header file should contain the following [two](https://github.com/shpakovski/MASShortcut/issues/36) imports:
```objective-c
#import <Cocoa/Cocoa.h>
#import <MASShortcut/Shortcut.h>
```
# Copyright
MASShortcut is licensed under the BSD license.
MASShortcut is licensed under the 2-clause BSD license.
+15
View File
@@ -0,0 +1,15 @@
This is an attempt to specify some of the parts of the library so that its easier to spot bugs and regressions.
The specification is expected to grow incrementally, as the developers update various parts of the code. If you hack on a part of the library that would benefit from a precise specification and is not documented here yet, please consider adding to the specification.
Please stay high-level when writing the spec, do not document particular classes or other implementation details. The spec should be usable as a testing scenario you should be able to walk through the spec and verify correct code behaviour on the library demo app.
# Recording Shortcuts
* If a shortcut has no modifiers and is not a function key (F1F20), it must be rejected. (Examples: `A`, Shift-A.)
* If the shortcut is plain Esc without modifiers, it must be rejected and cancels the recording.
* If the shortcut is plain Backspace or plain Delete, it must be rejected, clears the recorded shortcut and cancels the recording.
* If the shortcut is Cmd-W or Cmd-Q, the recording must be cancelled and the keypress passed through to the system, closing the window or quitting the app.
* If a shortcut is already taken by system and is enabled, it must be rejected. (Examples: Cmd-S, Cmd-N. TBD: What exactly does it mean that the shortcut is “enabled”?)
* TBD: Option-key handling.
* All other shortcuts must be accepted. (Examples: Ctrl-Esc, Cmd-Delete, F16.)
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
+5
View File
@@ -0,0 +1,5 @@
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <XCTest/XCTest.h>
#import "Shortcut.h"
#endif