222 Commits

Author SHA1 Message Date
Tomáš Znamenáček 409699c3c0 Version bump to 2.3.4 2016-08-12 11:49:17 +02:00
Vadim Shpakovski 928bbe25b4 Upgrade Projects to Xcode 7 2016-08-08 18:24:19 +03:00
Vadim Shpakovski 658a4e1e78 Merge pull request #94 from sfsam/master
Added Makefile to build from the command line
2016-07-15 09:45:59 +03:00
Sanjay Madan bd3cf9bf9c Added Makefile to build from the command line 2016-07-14 22:12:06 -07:00
Vadim Shpakovski a4dd585497 Merge pull request #93 from pointum/master
ru localization update
2016-07-13 17:35:07 +03:00
Maxim Ananov 843cb81697 ru localization update 2016-07-13 17:26:02 +03:00
Tomáš Znamenáček 9471ddbc7c Update CHANGES 2016-04-14 10:08:23 +02:00
Vadim Shpakovski 46bd84f57d Merge pull request #89 from radex/add-translations
Add Korean, Dutch, Polish, Russian & update Spanish translations
2016-04-14 11:04:37 +03:00
radex 6233bfac56 Add ko, nl, pl, ru, es translations 2016-04-14 09:55:34 +02:00
Tomáš Znamenáček 03e3ed398a Use correct quote signs for German translation.
The quotes should be like „this“, not ‘this’.
2016-02-14 09:44:45 +01:00
Tomáš Znamenáček 00b5041867 Shortened the new German translation strings to fit the UI.
Closes #85.
2016-02-14 09:41:52 +01:00
Tomáš Znamenáček 69b0a06656 Added basic German localization to the demo project.
This makes it possible to test the German MASShortcut localization
on the demo project.
2016-02-10 19:13:53 +01:00
Tomáš Znamenáček abb1ac2799 Updated CHANGES. 2016-02-05 10:07:59 +01:00
Vadim Shpakovski 9bbb5b3e6a Merge pull request #84 from floschliep/master
Improve German Localization
2016-02-05 09:30:17 +01:00
Florian Schliep 00dd8616f1 Improved German Localization
I translated the missing strings and improved the existing ones.
2016-02-04 18:56:50 +01:00
Tomáš Znamenáček d359752bef Updated CHANGES. 2016-01-22 10:29:17 +01:00
Tomáš Znamenáček 6b80ea95b5 Merge pull request #83 from MichaelRow/master
Simplified and traditional Chinese localizations.
2016-01-22 10:28:06 +01:00
MichaelRow 8e14c20ca4 Simplified and traditional Chinese localizations. 2016-01-22 12:04:51 +08:00
Tomáš Znamenáček d0063130b5 Version bump to 2.3.3. 2016-01-09 15:51:28 +01:00
Tomáš Znamenáček c384d0e94b Updated CHANGES. 2016-01-09 15:47:06 +01:00
Tomáš Znamenáček e267d30bfd Whitespace fixes. 2016-01-09 15:45:45 +01:00
Nikita Ivanchikov d32fc188ca Another approach to try a fix for the CocoaPods localization bundle problem (#74).
Checking if resources bundle was copied inside framework bundle, then falling back to old search methods
2016-01-09 15:43:05 +01:00
Tomáš Znamenáček 2b2fb80556 Updated changelog. 2015-12-06 16:08:46 +01:00
Tomáš Znamenáček 60978895b2 Merge pull request #80 from magiclantern/magiclantern/french-localization
Expanded Frech localization.
2015-12-06 16:07:10 +01:00
Nikita Ourazbaev 6a97ae68e9 Some suggestions for fr
Some suggestions for the gaps in the French localization
2015-12-06 10:04:45 -05:00
Tomáš Znamenáček 29d047c928 Documented that we don’t accept Option-Shift shortcuts by default.
The reason is that Option-Shift shortcuts are often already used
by system to insert some special characters. See `MASShortcutValidator`
for details, including how to enable support for these shortcuts.
Fixes #79.
2015-12-03 15:15:05 +01:00
Tomáš Znamenáček 5a18ccc5e1 Even more elaborate workaround for the CocoaPods localization problem (#74).
Just testing if the CocoaPods bundle is present didn’t work, so now we
even try to load a localized string from the bundle.
2015-11-06 08:59:52 +01:00
Tomáš Znamenáček e83732400b Trying a fix for the CocoaPods localization bundle problem (#74).
This change simply doesn’t try to detect which bundle we should use
and tries both bundles, the CocoaPods one first. The chosen bundle is
then cached to avoid the overhead on subsequent requests.
2015-11-04 15:02:55 +01:00
Tomáš Znamenáček 2730683848 Updated CHANGES. 2015-10-31 10:26:15 +01:00
Vadim Shpakovski 471322a113 Merge pull request #77 from oreshinya/japanese_localization
Japanese localization
2015-10-14 20:16:16 +03:00
shinya takahashi 09b7364130 Japanese localization 2015-10-14 18:44:06 +09:00
Tomáš Znamenáček 82a06382f0 Version bump to 2.3.2. 2015-10-12 09:31:34 +02:00
Tomáš Znamenáček dd2507b7f9 Silenced a potential “tautological compare” warning in MASShortcutView.
Fixes #76. I considered putting the #pragma just around the particular
compare line, but I think having it around the whole block is more readable
and there’s little chance of having some legitimate warnings silenced.
2015-10-12 09:28:19 +02:00
Tomáš Znamenáček a0af086f16 Typo fix. 2015-10-08 11:53:21 +02:00
Tomáš Znamenáček a73a254ce7 Documented the CocoaPods localization fix. 2015-10-08 11:30:24 +02:00
Tomáš Znamenáček ed694bc1a5 Merge pull request #75 from beaufour/master
Fixed localization when used from CocoaPods.
2015-10-08 11:26:27 +02:00
Allan Beaufour ded95506c6 fixes localized strings when using CocoaPods
Puts strings inside a MASShortcut resource bundle, and looks for
locallized strings inside that bundle when used as a CocoaPod
2015-10-07 12:52:59 -04:00
Tomáš Znamenáček b99a1a4dc4 Updated localization and accessibility status in README. 2015-09-10 10:01:40 +02:00
Tomáš Znamenáček dd0a37c785 Release 2.3.1. 2015-09-10 09:39:14 +02:00
Tomáš Znamenáček a06ef8d27c Changed MASLocalization.h to explicit import, trying to appease CocopaPods. 2015-09-10 09:37:38 +02:00
Tomáš Znamenáček 8b669cea8d Release 2.3.0. 2015-09-10 09:29:14 +02:00
Tomáš Znamenáček 3cca4da610 Added basic localization for German, Spanish, French, Italian, and Japanese. 2015-09-10 09:24:41 +02:00
Tomáš Znamenáček 78bd9fbd77 Mentioned Carthage in the installation options. 2015-08-18 17:26:41 +02:00
Tomáš Znamenáček c7fc9a555a Use an absolute URL for the demo screenshot.
An absolute URL works better on the CocoaPods website.
2015-08-18 17:24:58 +02:00
Tomáš Znamenáček 86be6257cb Updated contributing guidelines (backward compatibility, commit messages). 2015-08-18 17:23:09 +02:00
Tomáš Znamenáček 9f5db7ad25 Removed an obsolete Localizable.string file. 2015-08-18 14:01:33 +02:00
Tomáš Znamenáček 814934072a Added Czech localization. 2015-08-18 14:00:09 +02:00
Tomáš Znamenáček 74e3f32438 Updated CHANGES, Info.plist and podspec for the 2.2.0 release. 2015-08-18 11:17:51 +02:00
Vadim Shpakovski 1f3dbbce75 Merge pull request #71 from Stillness-2/master
Some fix
2015-08-10 02:44:50 +03:00
Roman Sokolov c98154e2c3 fix for keyboard navigation
Tab key must pass through. This is important if you want to do keyboard navigation through this control.
2015-08-09 21:55:03 +03:00
Roman Sokolov 5ead539397 Fix
Fix problem: 
Set hotkey [TheSameHOTKEY], then mousedown on delete button, then set hotkey again  [TheSameHOTKEY], then this hotkey do not work.
2015-08-09 21:27:19 +03:00
Tomáš Znamenáček 3f63f8fefc Merge pull request #69 from pfandrade/master
Not declaring MASShortcutGlyph as a variable in the header.

Prevents duplicate symbol errors.
2015-04-10 09:06:12 +02:00
Paulo F. Andrade dcb134242d Not declaring MASShortcutGlyph as a variable in the header 2015-04-09 20:20:36 +01:00
Tomáš Znamenáček 6c4577199c Namespaced the testing build scheme name (Tests → MASShortcutTests). 2015-03-09 14:32:38 +01:00
Tomáš Znamenáček 643e87c199 Updated CHANGES. 2015-03-09 14:15:32 +01:00
Vadim Shpakovski bc2c91fc94 Merge pull request #67 from brow/carthage
Support installation with Carthage
2015-03-09 15:37:05 +03:00
Tom Brow 7586157fe2 add Carthage compatibility flag to README 2015-03-08 15:17:58 -07:00
Tom Brow 4d3c2cad2c missing import? 2015-03-08 15:12:59 -07:00
Tom Brow d845d8cda9 module map 2015-03-08 15:12:27 -07:00
Tom Brow 2efd8d1dca DEFINES_MODULE = YES 2015-03-08 14:49:30 -07:00
Tomáš Znamenáček 7bbaed6227 Removed support for dots and spaces in user defaults keys (#64).
I could find no way to make the feature work, so I have pulled it
from the code. I have also inserted asserts to warn library users
who would attempt to use illegal characters in user defaults keys
in the future. In short, you want your user defaults keys to be
something identifier-like.
2015-03-05 14:32:51 +01:00
Tomáš Znamenáček fdc43c1cd3 Fix a problem with defaults keys with dot symbols (#64). 2015-03-05 10:59:01 +01:00
Tomáš Znamenáček a9e6e5241c Added a test case for a binder problem with dot symbols (#64). 2015-03-04 16:28:25 +01:00
Tomáš Znamenáček 67f4a5477b Trivial refactoring. 2015-03-04 16:15:47 +01:00
Tomáš Znamenáček a1eeb57ad6 Fix accessibility-related crashes on older OS X releases (#47).
The NSAccessibilityPriorityKey symbol was only introduced in 10.9,
so that we have to check for its availability before using it, otherwise
we get a SIGSEGV.
2015-03-04 16:07:46 +01:00
Tomáš Znamenáček 934abde616 Updated README and CHANGES. 2015-03-04 11:13:01 +01:00
Tomáš Znamenáček 00dd421e20 Merge pull request #66 from starkos/issue-47-accessibility
Add basic accessibility support.
2015-03-04 11:08:47 +01:00
Jason Perkins e2b2d5b9e1 Allow first responder support to be turned on and off 2015-02-16 10:22:22 -05:00
Jason Perkins 86d5b1ae49 Merge branch 'master' into issue-47-accessibility 2015-02-16 10:09:48 -05:00
Vadim Shpakovski 3ea350cec1 Merge pull request #65 from oreshinya/add_option_showing_delete_hotkey_button
Added options to show button that delete hot key.
2015-02-12 19:57:02 +03:00
shinya takahashi aeaad2986f Added options to show button that delete hot key. 2015-02-13 01:05:14 +09:00
Tomáš Znamenáček ef399290e4 Version bump to 2.1.2. 2015-01-28 16:11:36 +01:00
Tomáš Znamenáček 38fe428b19 Documented that keyCodeString depends on active keyboard layout. 2015-01-28 15:32:20 +01:00
Tomáš Znamenáček de647cf825 Talk about shortcut “formatting” instead of “rendering”. 2015-01-28 15:19:46 +01:00
Dmitry Obukhov 0db346d02b Better key equivalent handling on non-ASCII keyboard layouts.
See #60 for a discussion. In short, keyCodeStringForKeyEquivalent
should be now correct even with non-ASCII keyboard layouts such as
Russian.
2015-01-28 14:57:17 +01:00
Tomáš Znamenáček 0972882368 Clarified the spec part about rendering shortcuts. 2015-01-28 10:16:15 +01:00
Tomáš Znamenáček 72598e01af Added a note about shortcut rendering in the spec. 2015-01-28 10:13:53 +01:00
Tomáš Znamenáček 345da61b0d Added a short release guide. 2015-01-26 09:43:47 +01:00
Jason Perkins b564f5296a Enable control to become first responder 2015-01-21 07:36:08 -05:00
Jason Perkins 439ec69ab8 Fix "semicolon in method body" warning 2015-01-20 14:04:23 -05:00
Tomáš Znamenáček fd6885abdb Improved screenshot quality. 2015-01-19 15:47:35 +01:00
Tomáš Znamenáček 35e15f4950 Mentioned installation via Git submodules. 2015-01-19 15:37:14 +01:00
Tomáš Znamenáček fc1ec3eb41 Linked to the API documentation. 2015-01-19 15:32:23 +01:00
Tomáš Znamenáček bd0510c74d Smaller screenshot. 2015-01-19 15:28:31 +01:00
Tomáš Znamenáček 15dda682e4 Added a list of features and a screenshot of the demo project. 2015-01-19 15:27:36 +01:00
Tomáš Znamenáček 04987a7bd5 Version bump to 2.1.1. 2015-01-16 12:59:29 +01:00
Tomáš Znamenáček bf3a032c4b Decrease headerdoc indenting to appease appledoc (see #55).
I think I have finally found out the reason for CocoaDocs ignoring
our markup: I have indented the documentation by four spaces, which
was interpreted as “code” by appledoc. Trying now without the indent,
that should finally help.
2015-01-16 12:57:08 +01:00
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
Jason Perkins eda4bdb9c9 Add initial accessibility using 10.10 APIs 2015-01-14 12:23:22 -05: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
75 changed files with 5102 additions and 856 deletions
+3
View File
@@ -0,0 +1,3 @@
language: objective-c
xcode_project: MASShortcut.xcodeproj
xcode_scheme: MASShortcut
+47
View File
@@ -0,0 +1,47 @@
2.3.4 2016/8/12
- Simplified and traditional Chinese localization [MichaelRow]
- Add Korean, Dutch, Polish, Russian & update Spanish localizations [Radek Pietruszewski]
- Improved German localization [Florian Schliep]
Add a Makefile to improve command-line building [sfsam]
2.3.3 2016/1/9
- Improved Japanese localization [oreshinya]
- Improved Frech localization [magiclantern]
- Fixed CocoaPods localization with use_frameworks! [nivanchikov]
2.3.2 2015/10/12
- Fixed localization when building through CocoaPods [Allan Beaufour]
2.3.1 2015/9/10
- Trying to work around a strange build error in CocoaPods.
2.3.0 2015/9/10
- Basic localization support for Czech, German, Spanish,
Italian, French, and Japanese. Native speaking testers welcome!
2.2.0 2015/8/18
- Basic accessibility support [starkos]
- Added an option to hide the shortcut delete button [oreshinya]
- Advertised support for Carthage [Tom Brown]
- Bugfix for shortcuts not working after set twice [Roman Sokolov]
- Ignore a solo Tab key when recording shortcuts [Roman Sokolov]
2.1.2 2015/1/28
- Better key equivalent handling for non-ASCII layouts.
[Dmitry Obukhov]
2.1.1 2015/1/16
- Another headerdoc fix for CocoaDocs, hopefully the last one.
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.
+29
View File
@@ -0,0 +1,29 @@
# Backward Compatibility
Please note that this framework supports older OS X versions down to 10.6. When changing the code, be careful not to call any API functions not available in 10.6 or call them conditionally, only where supported.
# Commit Messages
Please use descriptive commit message. As an example, _Bug fix_ commit message doesnt say much, while _Fixed a memory-management bug in formatting code_ works much better.
# How to Release a New Version
First, update the version numbers. (MASShortcut uses [Semantic Versioning](http://semver.org/), so please read the docs if youre not sure what the deal is.) The version number is stored in `Framework/Info.plist` and `MASShortcut.podspec` (twice in both files).
Then update the `CHANGES` file. Add information about the new version (see the previous versions for an example) and add the release date.
Now commit the changes:
$ git commit -a -m "Version bump to x.y.z."
And tag the last commit:
$ git tag -a x.y.z
Now push both the commits and tags (`--tags`) to GitHub and push the new podspec to CocoaPods:
$ pod trunk push MASShortcut.podspec
This will run sanity checks on the podspec and fail if the spec does not validate.
Thats it. Go have a beer or a cup of tea to celebrate.
+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:NSLocalizedString(@"Shortcut pressed!", @"Feedback thats displayed when user presses the sample shortcut.")];
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>$(PRODUCT_BUNDLE_IDENTIFIER)</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>
+2
View File
@@ -0,0 +1,2 @@
#import <Cocoa/Cocoa.h>
#import <MASShortcut/Shortcut.h>
+3
View File
@@ -0,0 +1,3 @@
/* Feedback thats displayed when user presses the sample shortcut. */
"Shortcut pressed!" = "Funguje!";
+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="8173.3" systemVersion="14E46" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8173.3"/>
</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="9" y="92" width="120" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Klávesová zkratka:" 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="196" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Zapnout klávesovou zkratku" 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="Předvolby:" 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="237" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Zapnout vestavěnou zkratku (⌘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="Zkratka v akci:" 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="stiskněte klávesovou zkratku" 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="6.5" y="248.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>
+3
View File
@@ -0,0 +1,3 @@
/* Feedback thats displayed when user presses the sample shortcut. */
"Shortcut pressed!" = "Kurzbefehl gedrückt!";
+3
View File
@@ -0,0 +1,3 @@
/* Feedback thats displayed when user presses the sample shortcut. */
"Shortcut pressed!" = "Shortcut pressed!";
+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="8173.3" systemVersion="14E46" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="8173.3"/>
</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>
+3
View File
@@ -0,0 +1,3 @@
/* Feedback thats displayed when user presses the sample shortcut. */
"Shortcut pressed!" = "ショートカットが押されました!";
+4
View File
@@ -0,0 +1,4 @@
int main(int argc, char *argv[])
{
return NSApplicationMain(argc, (const char **)argv);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+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>$(PRODUCT_BUNDLE_IDENTIFIER)</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.3.4</string>
<key>CFBundleVersion</key>
<string>2.3.4</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 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
+43
View File
@@ -0,0 +1,43 @@
#import <Carbon/Carbon.h>
#import <AppKit/AppKit.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,
};
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);
}
+13
View File
@@ -0,0 +1,13 @@
/**
Reads a localized string from the frameworks bundle.
Normally you would use NSLocalizedString to read the localized
strings, but thats just a shortcut for loading the strings from
the main bundle. And once the framework ends up in an app, the
main bundle will be the apps bundle and wont contain our strings.
So we introduced this helper function that makes sure to load the
strings from the frameworks bundle. Please avoid using
NSLocalizedString throughout the framework, it wouldnt work
properly.
*/
NSString *MASLocalizedString(NSString *key, NSString *comment);
+34
View File
@@ -0,0 +1,34 @@
#import "MASLocalization.h"
#import "MASShortcut.h"
static NSString *const MASLocalizationTableName = @"Localizable";
static NSString *const MASPlaceholderLocalizationString = @"XXX";
// The CocoaPods trickery here is needed because when the code
// is built as a part of CocoaPods, it wont make a separate framework
// and the Localized.strings file wont be bundled correctly.
// See https://github.com/shpakovski/MASShortcut/issues/74
NSString *MASLocalizedString(NSString *key, NSString *comment) {
static NSBundle *localizationBundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *frameworkBundle = [NSBundle bundleForClass:[MASShortcut class]];
// first we'll check if resources bundle was copied to MASShortcut framework bundle when !use_frameworks option is active
NSURL *cocoaPodsBundleURL = [frameworkBundle URLForResource:@"MASShortcut" withExtension:@"bundle"];
if (cocoaPodsBundleURL) {
localizationBundle = [NSBundle bundleWithURL: cocoaPodsBundleURL];
} else {
// trying to fetch cocoapods bundle from main bundle
cocoaPodsBundleURL = [[NSBundle mainBundle] URLForResource: @"MASShortcut" withExtension:@"bundle"];
if (cocoaPodsBundleURL) {
localizationBundle = [NSBundle bundleWithURL: cocoaPodsBundleURL];
} else {
// fallback to framework bundle
localizationBundle = frameworkBundle;
}
}
});
return [localizationBundle localizedStringForKey:key
value:MASPlaceholderLocalizationString
table:MASLocalizationTableName];
}
+81
View File
@@ -0,0 +1,81 @@
#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`.
@warning The value may change depending on the active keyboard layout.
For example for the `^2` keyboard shortcut (`kVK_ANSI_2+NSControlKeyMask`
to be precise) the `keyCodeString` is `2` on the US keyboard, but `ě` when
the Czech keyboard layout is active. See the spec for details.
*/
@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`.
The value of this property may differ from `keyCodeString`. For example
the Russian keyboard has a `Г` (Ge) Cyrillic character in place of the
latin `U` key. This means you can create a `^Г` shortcut, but in menus
thats always displayed as `^U`. So the `keyCodeString` returns `Г`
and `keyCodeStringForKeyEquivalent` returns `U`.
*/
@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
+242
View File
@@ -0,0 +1,242 @@
#import "MASShortcut.h"
#import "MASLocalization.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 MASLocalizedString(@"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 ASCII capable keyboard layout
OSStatus error = noErr;
NSString *keystroke = nil;
TISInputSourceRef inputSource = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
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
+6
View File
@@ -0,0 +1,6 @@
framework module MASShortcut {
umbrella header "Shortcut.h"
export *
module * { export * }
}
+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
+121
View File
@@ -0,0 +1,121 @@
#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
{
NSAssert([defaultsKeyName rangeOfString:@"."].location == NSNotFound,
@"Illegal character in binding name (“.”), please see http://git.io/x5YS.");
NSAssert([defaultsKeyName rangeOfString:@" "].location == NSNotFound,
@"Illegal character in binding name (“ ”), please see http://git.io/x5YS.");
[_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) {
[_shortcuts removeObjectForKey:key];
return;
}
// Bind new shortcut
[_shortcuts setObject:newShortcut forKey:key];
[_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]];
}
@end
+107
View File
@@ -0,0 +1,107 @@
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.");
}
// See issue #64 <http://git.io/x5YS> for rationale and discussion.
- (void) testIllegalSymbolsInBindingNames
{
XCTAssertThrows([_binder bindShortcutWithDefaultsKey:@"foo.bar" toAction:^{}],
@"Throw for illegal binding symbols: a dot (“.”).");
XCTAssertThrows([_binder bindShortcutWithDefaultsKey:@"foo bar" toAction:^{}],
@"Throw for illegal binding symbols: a space (“ ”).");
}
@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
+29
View File
@@ -0,0 +1,29 @@
#import "MASShortcut.h"
/**
This class is used by the recording control to tell which shortcuts are acceptable.
There are two kinds of shortcuts that are not considered acceptable: shortcuts that
are too simple (like single letter keys) and shortcuts that are already used by the
operating system.
*/
@interface MASShortcutValidator : NSObject
/**
Set to `YES` if you want to accept Option-something shortcuts.
`NO` by default, since Option-something shortcuts are often used by system,
for example Option-G will type the © sign. This also applies to Option-Shift
shortcuts in other words, shortcut recorder will not accept shortcuts like
Option-Shift-K by default. (Again, since Option-Shift-K inserts the Apple
logo sign by default.)
*/
@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
+112
View File
@@ -0,0 +1,112 @@
#import "MASShortcutValidator.h"
#import "MASLocalization.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 = MASLocalizedString(@"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 = MASLocalizedString(@"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
+26
View File
@@ -0,0 +1,26 @@
@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;
- (void)setAcceptsFirstResponder:(BOOL)value;
@end
+252 -65
View File
@@ -1,9 +1,11 @@
#import "MASShortcutView.h"
#import "MASShortcut.h"
#import "MASShortcutValidator.h"
#import "MASLocalization.h"
#define HINT_BUTTON_WIDTH 23.0
#define BUTTON_FONT_SIZE 11.0
#define SEGMENT_CHROME_WIDTH 6.0
NSString *const MASShortcutBinding = @"shortcutValue";
static const CGFloat MASHintButtonWidth = 23;
static const CGFloat MASButtonFontSize = 11;
#pragma mark -
@@ -11,6 +13,7 @@
@property (nonatomic, getter = isHinting) BOOL hinting;
@property (nonatomic, copy) NSString *shortcutPlaceholder;
@property (nonatomic, assign) BOOL showsDeleteButton;
@end
@@ -21,30 +24,46 @@
NSInteger _shortcutToolTipTag;
NSInteger _hintToolTipTag;
NSTrackingArea *_hintArea;
BOOL _acceptsFirstResponder;
}
@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:MASButtonFontSize];
_shortcutValidator = [MASShortcutValidator sharedValidator];
_enabled = YES;
_showsDeleteButton = YES;
_acceptsFirstResponder = NO;
[self resetShortcutCellStyle];
}
- (void)dealloc
{
[self activateEventMonitoring:NO];
@@ -63,10 +82,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,19 +93,25 @@
- (void)resetShortcutCellStyle
{
switch (_appearance) {
case MASShortcutViewAppearanceDefault: {
switch (_style) {
case MASShortcutViewStyleDefault: {
_shortcutCell.bezelStyle = NSRoundRectBezelStyle;
break;
}
case MASShortcutViewAppearanceTexturedRect: {
case MASShortcutViewStyleTexturedRect: {
_shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
break;
}
case MASShortcutViewAppearanceRounded: {
case MASShortcutViewStyleRounded: {
_shortcutCell.bezelStyle = NSRoundedBezelStyle;
break;
}
case MASShortcutViewStyleFlat: {
self.wantsLayer = YES;
_shortcutCell.backgroundColor = [NSColor clearColor];
_shortcutCell.bordered = NO;
break;
}
}
}
@@ -101,15 +126,35 @@
// Only enabled view supports recording
if (flag && !self.enabled) return;
if (_recording != flag) {
_recording = flag;
self.shortcutPlaceholder = nil;
[self resetToolTips];
[self activateEventMonitoring:_recording];
[self activateResignObserver:_recording];
[self setNeedsDisplay:YES];
// Only care about changes in state
if (flag == _recording) return;
_recording = flag;
self.shortcutPlaceholder = nil;
[self resetToolTips];
[self activateEventMonitoring:_recording];
[self activateResignObserver:_recording];
[self setNeedsDisplay:YES];
// Give VoiceOver users feedback on the result. Requires at least 10.9 to run.
// Were silencing the tautological compare warning here so that if someone
// takes the naked source files and compiles them with -Wall, the following
// NSAccessibilityPriorityKey comparison doesnt cause a warning. See:
// https://github.com/shpakovski/MASShortcut/issues/76
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"
if (_recording == NO && (&NSAccessibilityPriorityKey != NULL)) {
NSString* msg = _shortcutValue ?
MASLocalizedString(@"Shortcut set", @"VoiceOver: Shortcut set") :
MASLocalizedString(@"Shortcut cleared", @"VoiceOver: Shortcut cleared");
NSDictionary *announcementInfo = @{
NSAccessibilityAnnouncementKey : msg,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh),
};
NSAccessibilityPostNotificationWithUserInfo(self, NSAccessibilityAnnouncementRequestedNotification, announcementInfo);
}
#pragma clang diagnostic pop
}
- (void)setShortcutValue:(MASShortcut *)shortcutValue
@@ -117,6 +162,7 @@
_shortcutValue = shortcutValue;
[self resetToolTips];
[self setNeedsDisplay:YES];
[self propagateValue:shortcutValue forBinding:MASShortcutBinding];
if (self.shortcutValueChange) {
self.shortcutValueChange(self);
@@ -143,56 +189,66 @@
_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 MASShortcutViewAppearanceRounded: {
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)
alignment:NSRightTextAlignment state:NSOffState];
NSString *buttonTitle;
if (self.recording) {
buttonTitle = NSStringFromMASKeyCode(kMASShortcutGlyphEscape);
} else if (self.showsDeleteButton) {
buttonTitle = NSStringFromMASKeyCode(kMASShortcutGlyphClear);
}
if (buttonTitle != nil) {
[self drawInRect:self.bounds withTitle:buttonTitle 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")
? MASLocalizedString(@"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")))
: MASLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state")))
: _shortcutValue ? _shortcutValue.description : @"");
[self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:self.isRecording ? NSOnState : NSOffState];
}
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];
NSString *title = (_hinting
? NSLocalizedString(@"Cancel", @"Cancel action button in recording state")
? MASLocalizedString(@"Cancel", @"Cancel action button in recording state")
: (self.shortcutPlaceholder.length > 0
? self.shortcutPlaceholder
: NSLocalizedString(@"Type Shortcut", @"Empty shortcut button in recording state")));
: MASLocalizedString(@"Type Shortcut", @"Empty shortcut button in recording state")));
[self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:NSOnState];
}
else
{
[self drawInRect:self.bounds withTitle:NSLocalizedString(@"Record Shortcut", @"Empty shortcut button in normal state")
[self drawInRect:self.bounds withTitle:MASLocalizedString(@"Record Shortcut", @"Empty shortcut button in normal state")
alignment:NSCenterTextAlignment state:NSOffState];
}
}
@@ -203,10 +259,11 @@
- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef
{
CGRect shortcutRect, hintRect;
CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
switch (self.appearance) {
case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
CGFloat hintButtonWidth = MASHintButtonWidth;
switch (self.style) {
case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break;
case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break;
case MASShortcutViewStyleFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - MASButtonFontSize); break;
default: break;
}
CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
@@ -324,10 +381,10 @@ void *kUserDataHint = &kUserDataHint;
- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(CGPoint)point userData:(void *)data
{
if (data == kUserDataShortcut) {
return NSLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button");
return MASLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button");
}
else if (data == kUserDataHint) {
return NSLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut");
return MASLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut");
}
return nil;
}
@@ -342,41 +399,55 @@ 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
// Tab key must pass through.
if (shortcut.keyCode == kVK_Tab){
return event;
}
// 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",
NSString *format = MASLocalizedString(@"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:MASLocalizedString(@"OK", @"Alert button when shortcut is already used")];
[alert runModal];
weakSelf.shortcutPlaceholder = nil;
[weakSelf activateResignObserver:YES];
[weakSelf activateEventMonitoring:YES];
@@ -414,7 +485,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;
@@ -425,4 +496,120 @@ 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];
}
#pragma mark - Accessibility
- (BOOL)accessibilityIsIgnored
{
return NO;
}
- (NSString *)accessibilityHelp
{
return MASLocalizedString(@"To record a new shortcut, click this button, and then type the"
@" new shortcut, or press delete to clear an existing shortcut.",
@"VoiceOver shortcut help");
}
- (NSString *)accessibilityLabel
{
NSString* title = _shortcutValue.description ?: @"Empty";
title = [title stringByAppendingFormat:@" %@", MASLocalizedString(@"keyboard shortcut", @"VoiceOver title")];
return title;
}
- (BOOL)accessibilityPerformPress
{
if (self.isRecording == NO) {
self.recording = YES;
return YES;
}
else {
return NO;
}
}
- (NSString *)accessibilityRole
{
return NSAccessibilityButtonRole;
}
- (BOOL)acceptsFirstResponder
{
return _acceptsFirstResponder;
}
- (void)setAcceptsFirstResponder:(BOOL)value
{
_acceptsFirstResponder = value;
}
- (BOOL)becomeFirstResponder
{
[self setNeedsDisplay:YES];
return [super becomeFirstResponder];
}
- (BOOL)resignFirstResponder
{
[self setNeedsDisplay:YES];
return [super resignFirstResponder];
}
- (void)drawFocusRingMask
{
[_shortcutCell drawFocusRingMaskWithFrame:[self bounds] inView:self];
}
- (NSRect)focusRingMaskBounds
{
return [self bounds];
}
@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.
-8
View File
@@ -1,8 +0,0 @@
#import "MASShortcut.h"
@interface MASShortcut (Monitoring)
+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
+ (void)removeGlobalHotkeyMonitor:(id)monitor;
@end
-158
View File
@@ -1,158 +0,0 @@
#import "MASShortcut+Monitoring.h"
NSMutableDictionary *MASRegisteredHotKeys();
BOOL InstallCommonEventHandler();
void UninstallEventHandler();
void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey);
#pragma mark -
@interface MASShortcutHotKey : NSObject
@property (nonatomic, readonly) MASShortcut *shortcut;
@property (nonatomic, readonly, copy) void (^handler)();
@property (nonatomic, readonly) EventHotKeyRef carbonHotKey;
@property (nonatomic, readonly) UInt32 carbonHotKeyID;
- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
@end
#pragma mark -
@implementation MASShortcut (Monitoring)
+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
{
NSString *monitor = [NSString stringWithFormat:@"%p: %@", shortcut, shortcut.description];
MASShortcutHotKey *hotKey = [[MASShortcutHotKey alloc] initWithShortcut:shortcut handler:handler];
[MASRegisteredHotKeys() setObject:hotKey forKey:monitor];
return monitor;
}
+ (void)removeGlobalHotkeyMonitor:(id)monitor
{
if (monitor == nil) return;
NSMutableDictionary *registeredHotKeys = MASRegisteredHotKeys();
[registeredHotKeys removeObjectForKey:monitor];
if (registeredHotKeys.count == 0) {
UninstallEventHandler();
}
}
@end
#pragma mark -
@implementation MASShortcutHotKey
@synthesize carbonHotKeyID = _carbonHotKeyID;
@synthesize handler = _handler;
@synthesize shortcut = _shortcut;
@synthesize carbonHotKey = _carbonHotKey;
#pragma mark -
- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
{
self = [super init];
if (self) {
_shortcut = shortcut;
_handler = [handler copy];
InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey);
}
return self;
}
- (void)dealloc
{
[self uninstallExisitingHotKey];
}
- (void)uninstallExisitingHotKey
{
if (_carbonHotKey) {
UnregisterEventHotKey(_carbonHotKey);
_carbonHotKey = NULL;
}
}
@end
#pragma mark - Carbon magic
NSMutableDictionary *MASRegisteredHotKeys()
{
static NSMutableDictionary *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [NSMutableDictionary dictionary];
});
return shared;
}
#pragma mark -
FourCharCode const kMASShortcutSignature = 'MASS';
void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey)
{
if ((shortcut == nil) || !InstallCommonEventHandler()) return;
static UInt32 sCarbonHotKeyID = 0;
EventHotKeyID hotKeyID = { .signature = kMASShortcutSignature, .id = ++ sCarbonHotKeyID };
EventHotKeyRef carbonHotKey = NULL;
if (RegisterEventHotKey(shortcut.carbonKeyCode, shortcut.carbonFlags, hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &carbonHotKey) != noErr) {
carbonHotKey = NULL;
}
if (outCarbonHotKeyID) *outCarbonHotKeyID = hotKeyID.id;
if (outCarbonHotKey) *outCarbonHotKey = carbonHotKey;
}
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;
[MASRegisteredHotKeys() enumerateKeysAndObjectsUsingBlock:^(id key, MASShortcutHotKey *hotKey, BOOL *stop) {
if (hotKeyID.id == hotKey.carbonHotKeyID) {
if (hotKey.handler) {
hotKey.handler();
}
*stop = YES;
}
}];
return noErr;
}
#pragma mark -
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;
}
}
-8
View File
@@ -1,8 +0,0 @@
#import "MASShortcut.h"
@interface MASShortcut (UserDefaults)
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey;
@end
-87
View File
@@ -1,87 +0,0 @@
#import "MASShortcut+UserDefaults.h"
#import "MASShortcut+Monitoring.h"
@interface MASShortcutUserDefaultsHotKey : NSObject
@property (nonatomic, readonly) NSString *userDefaultsKey;
@property (nonatomic, copy) void (^handler)();
@property (nonatomic, weak) id monitor;
- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
@end
#pragma mark -
@implementation MASShortcut (UserDefaults)
+ (NSMutableDictionary *)registeredUserDefaultsHotKeys
{
static NSMutableDictionary *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [NSMutableDictionary dictionary];
});
return shared;
}
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
{
MASShortcutUserDefaultsHotKey *hotKey = [[MASShortcutUserDefaultsHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler];
[[self registeredUserDefaultsHotKeys] setObject:hotKey forKey:userDefaultsKey];
}
+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey
{
NSMutableDictionary *registeredHotKeys = [self registeredUserDefaultsHotKeys];
[registeredHotKeys removeObjectForKey:userDefaultsKey];
}
@end
#pragma mark -
@implementation MASShortcutUserDefaultsHotKey
@synthesize monitor = _monitor;
@synthesize handler = _handler;
@synthesize userDefaultsKey = _userDefaultsKey;
#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]];
[MASShortcut removeGlobalHotkeyMonitor:self.monitor];
}
#pragma mark -
- (void)userDefaultsDidChange:(NSNotification *)note
{
[MASShortcut removeGlobalHotkeyMonitor:self.monitor];
[self installHotKeyFromUserDefaults];
}
- (void)installHotKeyFromUserDefaults
{
NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:_userDefaultsKey];
MASShortcut *shortcut = [MASShortcut shortcutWithData:data];
if (shortcut == nil) return;
self.monitor = [MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:self.handler];
}
@end
-54
View File
@@ -1,54 +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 *keyCodeStringForKeyEquivalent;
@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
-308
View File
@@ -1,308 +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
{
id shortcut = (data ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil);
return shortcut;
}
#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 *)keyCodeStringForKeyEquivalent
{
NSString *keyCodeString = self.keyCodeString;
if (keyCodeString.length > 1) {
switch (self.keyCode) {
case kVK_F1: return MASShortcutChar(0xF704);
case kVK_F2: return MASShortcutChar(0xF705);
case kVK_F3: return MASShortcutChar(0xF706);
case kVK_F4: return MASShortcutChar(0xF707);
case kVK_F5: return MASShortcutChar(0xF708);
case kVK_F6: return MASShortcutChar(0xF709);
case kVK_F7: return MASShortcutChar(0xF70a);
case kVK_F8: return MASShortcutChar(0xF70b);
case kVK_F9: return MASShortcutChar(0xF70c);
case kVK_F10: return MASShortcutChar(0xF70d);
case kVK_F11: return MASShortcutChar(0xF70e);
case kVK_F12: return MASShortcutChar(0xF70f);
// From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
case kVK_F13: return MASShortcutChar(0xF710);
case kVK_F14: return MASShortcutChar(0xF711);
case kVK_F15: return MASShortcutChar(0xF712);
case kVK_F16: return MASShortcutChar(0xF713);
case kVK_Space: return MASShortcutChar(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_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 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 = (MASShortcutClear(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
}
if (equalFlags && equalHotkeyLowercase) {
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.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError];
}
@end
+19
View File
@@ -0,0 +1,19 @@
# coding: utf-8
Pod::Spec.new do |s|
s.name = 'MASShortcut'
s.version = '2.3.4'
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.3.4' }
s.source_files = 'Framework/*.{h,m}'
s.exclude_files = 'Framework/*Tests.m'
s.osx.frameworks = 'Carbon', 'AppKit'
s.requires_arc = true
s.osx.resource_bundles = { 'MASShortcut' => ['*.lproj'] }
end
+761
View File
@@ -0,0 +1,761 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
0D2CAB131B8332E5005431FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB151B8332E5005431FC /* Localizable.strings */; };
0D2CAB191B8339F4005431FC /* MASLocalization.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D2CAB171B8339F4005431FC /* MASLocalization.h */; };
0D2CAB1A1B8339F4005431FC /* MASLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D2CAB181B8339F4005431FC /* MASLocalization.m */; };
0D2CAB1B1B83409C005431FC /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB1D1B83409C005431FC /* MainMenu.xib */; };
0D2CAB211B834464005431FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB231B834464005431FC /* Localizable.strings */; };
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 */; };
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 */
0D2CAB141B8332E5005431FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
0D2CAB161B8332EE005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
0D2CAB171B8339F4005431FC /* MASLocalization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASLocalization.h; path = Framework/MASLocalization.h; sourceTree = "<group>"; };
0D2CAB181B8339F4005431FC /* MASLocalization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASLocalization.m; path = Framework/MASLocalization.m; sourceTree = "<group>"; };
0D2CAB1C1B83409C005431FC /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = "<group>"; };
0D2CAB1E1B8340A4005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = cs; path = cs.lproj/MainMenu.xib; sourceTree = "<group>"; };
0D2CAB221B834464005431FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
0D2CAB241B834467005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
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>"; };
0D58DE521BA165FC0023BFBE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
0D58DE531BA166170023BFBE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
0D58DE541BA166270023BFBE /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
0D58DE551BA166390023BFBE /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
0D58DE561BA166420023BFBE /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; 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>"; };
0D827D761990F81E0010B8EF /* Shortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Shortcut.h; path = Framework/Shortcut.h; sourceTree = "<group>"; };
0D827D8319910AFF0010B8EF /* MASShortcutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MASShortcutTests.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>"; };
0DEDAA021C6BB479001605F5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
6EA6034E1CBF822000A3ED9C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
6EA6034F1CBF822800A3ED9C /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
6EA603501CBF822D00A3ED9C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
6EA603511CBF823600A3ED9C /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
76A0597D1C51DC940014B271 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
76A0597E1C51DC9F0014B271 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
EAFFDC811AACFF3300F38834 /* MASShortcut.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; name = MASShortcut.modulemap; path = Framework/MASShortcut.modulemap; sourceTree = "<group>"; };
ED8737791BCE459800BB1716 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; 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 /* MASShortcutTests.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 */,
EAFFDC811AACFF3300F38834 /* MASShortcut.modulemap */,
0D2CAB151B8332E5005431FC /* Localizable.strings */,
0D827D98199110F60010B8EF /* Prefix.pch */,
0D827D761990F81E0010B8EF /* Shortcut.h */,
);
name = Framework;
sourceTree = "<group>";
};
0D827D681990D6110010B8EF /* Demo */ = {
isa = PBXGroup;
children = (
0D2CAB231B834464005431FC /* Localizable.strings */,
0D827D691990D6110010B8EF /* AppDelegate.h */,
0D827D6A1990D6110010B8EF /* AppDelegate.m */,
0D2CAB1D1B83409C005431FC /* 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 = (
0D2CAB171B8339F4005431FC /* MASLocalization.h */,
0D2CAB181B8339F4005431FC /* MASLocalization.m */,
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 */,
0D2CAB191B8339F4005431FC /* MASLocalization.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 /* MASShortcutTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "MASShortcutTests" */;
buildPhases = (
0D827D7F19910AFF0010B8EF /* Sources */,
0D827D8019910AFF0010B8EF /* Frameworks */,
0D827D8119910AFF0010B8EF /* Resources */,
);
buildRules = (
);
dependencies = (
0D827D8F19910AFF0010B8EF /* PBXTargetDependency */,
);
name = MASShortcutTests;
productName = Tests;
productReference = 0D827D8319910AFF0010B8EF /* MASShortcutTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
0D827CCA1990D4420010B8EF /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0730;
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,
cs,
de,
es,
it,
fr,
ja,
"zh-Hans",
"zh-Hant",
pl,
ko,
ru,
nl,
);
mainGroup = 0D827CC91990D4420010B8EF;
productRefGroup = 0D827CD41990D4420010B8EF /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
0D827CD21990D4420010B8EF /* MASShortcut */,
0D827D8219910AFF0010B8EF /* MASShortcutTests */,
0D827D361990D5E70010B8EF /* Demo */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0D827CD11990D4420010B8EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0D2CAB131B8332E5005431FC /* Localizable.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0D827D351990D5E70010B8EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0D2CAB1B1B83409C005431FC /* MainMenu.xib in Resources */,
0D2CAB211B834464005431FC /* Localizable.strings 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 */,
0D2CAB1A1B8339F4005431FC /* MASLocalization.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 PBXVariantGroup section */
0D2CAB151B8332E5005431FC /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
0D2CAB141B8332E5005431FC /* en */,
0D2CAB161B8332EE005431FC /* cs */,
0D58DE521BA165FC0023BFBE /* de */,
0D58DE531BA166170023BFBE /* es */,
0D58DE541BA166270023BFBE /* it */,
0D58DE551BA166390023BFBE /* fr */,
0D58DE561BA166420023BFBE /* ja */,
76A0597D1C51DC940014B271 /* zh-Hans */,
76A0597E1C51DC9F0014B271 /* zh-Hant */,
6EA6034E1CBF822000A3ED9C /* pl */,
6EA6034F1CBF822800A3ED9C /* ko */,
6EA603501CBF822D00A3ED9C /* ru */,
6EA603511CBF823600A3ED9C /* nl */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
0D2CAB1D1B83409C005431FC /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
0D2CAB1C1B83409C005431FC /* en */,
0D2CAB1E1B8340A4005431FC /* cs */,
);
name = MainMenu.xib;
sourceTree = "<group>";
};
0D2CAB231B834464005431FC /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
0D2CAB221B834464005431FC /* en */,
0D2CAB241B834467005431FC /* cs */,
ED8737791BCE459800BB1716 /* ja */,
0DEDAA021C6BB479001605F5 /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup 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;
ENABLE_TESTABILITY = YES;
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;
DEFINES_MODULE = 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;
MODULEMAP_FILE = Framework/MASShortcut.modulemap;
PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
WRAPPER_EXTENSION = framework;
};
name = Debug;
};
0D827CFD1990D4420010B8EF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = 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;
MODULEMAP_FILE = Framework/MASShortcut.modulemap;
PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}";
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_BUNDLE_IDENTIFIER = "com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}";
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_BUNDLE_IDENTIFIER = "com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}";
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_BUNDLE_IDENTIFIER = "com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}";
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_BUNDLE_IDENTIFIER = "com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}";
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 "MASShortcutTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0D827D9019910AFF0010B8EF /* Debug */,
0D827D9119910AFF0010B8EF /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 0D827CCA1990D4420010B8EF /* Project object */;
}
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
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
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D361990D5E70010B8EF"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<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,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
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 = "MASShortcutTests.xctest"
BlueprintName = "MASShortcutTests"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827D8219910AFF0010B8EF"
BuildableName = "MASShortcutTests.xctest"
BlueprintName = "MASShortcutTests"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D827CD21990D4420010B8EF"
BuildableName = "MASShortcut.framework"
BlueprintName = "MASShortcut"
ReferencedContainer = "container:MASShortcut.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
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
-128
View File
@@ -1,128 +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
{
// First, stop observing previous shortcut view
objc_setAssociatedObject(self, kDefaultsObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Next, start observing current shortcut view
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 *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:(shortcut.data ?: [NSKeyedArchiver archivedDataWithRootObject:nil]) forKey:_userDefaultsKey];
[defaults synchronize];
_internalPreferenceChange = NO;
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
-17
View File
@@ -1,17 +0,0 @@
@class MASShortcut;
typedef enum {
MASShortcutViewAppearanceDefault = 0, // Height = 19 px
MASShortcutViewAppearanceTexturedRect, // Height = 25 px
MASShortcutViewAppearanceRounded // Height = 43 px
} 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
+10
View File
@@ -0,0 +1,10 @@
.PHONY: release
ifndef BUILDDIR
BUILDDIR := $(shell mktemp -d "$(TMPDIR)/MASShortcut.XXXXXX")
endif
release:
xcodebuild -scheme MASShortcut -configuration Release -derivedDataPath "$(BUILDDIR)" build
open "$(BUILDDIR)/Build/Products/Release"
+122 -16
View File
@@ -1,30 +1,136 @@
[![Build Status](https://travis-ci.org/shpakovski/MASShortcut.svg?branch=master)](https://travis-ci.org/shpakovski/MASShortcut)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
# 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.
![Screenshot of the demo project](https://raw.githubusercontent.com/shpakovski/MASShortcut/master/Demo/screenshot.png "This is how the demo looks like")
Features:
* Record and display keyboard shortcuts
* Watch for shortcuts and execute actions, system-wide
* A nice, [documented API](http://cocoadocs.org/docsets/MASShortcut/)
* Can be configured to be compatible with Shortcut Recorder
* Can be installed both through CocoaPods and as a Git submodule
* Mac App Store friendly
* Works on OS X 10.6 and up
* Hacking-friendly codebase covered with tests
Partially done:
* Accessibility support. Theres some basic accessibility code, testers and feedback welcome.
* Localisation. The English and Czech localization should be complete, theres basic support for German, French, Spanish, Italian, and Japanese. If youre a native speaker in one of the mentioned languages, please test the localization and report issues or add missing strings.
Pull requests welcome :)
# 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'
You can also install via [Carthage](https://github.com/Carthage/Carthage), or you can use Git submodules and link against the MASShortcut framework manually.
To build from the command line, type 'make release'. The framework will be created in a temporary directory and revealed in Finder when the build is complete.
# 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.
+25
View File
@@ -0,0 +1,25 @@
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.)
# Formatting Shortcuts
On different keyboard layouts (such as US and Czech), a single shortcut (a combination of physical keys) may be formatted into different strings.
For example, the default system shortcut for toggling directly to Space #2 is Control2. But when you switch to the Czech keyboard layout, the physical key with the `2` label now inserts the `ě` character. Thus, on most keyboard layouts the shortcut for toggling to Space #2 is called `^2`, but on the Czech layout its called `^ě`. (I stress that this is the same combination of hardware keys and the same `MASShortcut` instance.)
This is reflected by the system: When you open the System Preferences → Keyboard → Shortcuts pane, the shortcuts displayed depend on the currently selected keyboard layout (try switching between the US and Czech keyboard layouts and reopening the preference pane).
This means that the identity of a shortcut is given by its key code and modifiers (such as `kVK_ANSI_2` and `NSControlKeyMask`), not the `keyCodeString` returned by the `MASShortcut` class. This string may change depending on the current keyboard layout: `^2` with the US keyboard active, but `^ě` with the Czech keyboard active.
+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>$(PRODUCT_BUNDLE_IDENTIFIER)</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
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Zrušit";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Kliknutím nahrajete novou zkratku";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Smazat zkratku";
/* VoiceOver title */
"keyboard shortcut" = "klávesová zkratka";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Nahrát zkratku";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "zkratka smazána";
/* VoiceOver: Shortcut set */
"Shortcut set" = "zkratka nastavena";
/* Shortcut glyph name for SPACE key */
"Space" = "Mezerník";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "Kombinace %@ se nedá použít";
/* Message for alert when shortcut is already used by the system */
"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." = "Tato zkratka se nedá použít, protože už ji obsadil systém.\nKdybyste na ní trvali, většina systémových zkratek se dá přenastavit v Předvolbách systému.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Tato zkratka se nedá použít, protože už je použita pro menu (%@).";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Pokud chcete nahrát novou zkratku, stiskněte toto tlačítko a následně vybranou zkratku. Stisknutím Delete vymažete stávající zkratku.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Stiskněte zkratku";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Stiskněte zkratku";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Vrátit se k původní";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Abbrechen";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Klicken um neuen Kurzbefehl aufzunehmen";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Kurzbefehl Löschen";
/* VoiceOver title */
"keyboard shortcut" = "Kurzbefehl";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Kurzbefehl aufnehmen";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Kurzbefehl gelöscht";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Kurzbefehl gesetzt";
/* Shortcut glyph name for SPACE key */
"Space" = "Leertaste";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "Die Tastenkombination %@ kann nicht genutzt werden";
/* Message for alert when shortcut is already used by the system */
"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." = "Diese Kombination kann nicht genutzt werden, weil sie bereits als systemweiter Kurzbefehl genutzt wird.\nFalls du diese Tastenkombination wirklich benutzen willst, können die meisten Kurzbefehle in den Tastatur Systemeinstellungen geändert werden.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Dieser Kurzbefehl kann nicht genutzt werden, weil er bereits vom Menüpunkt „%@“ genutzt wird.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Drücke diesen Button, um einen neuen Kurzbefehl aufzunehmen. Tippe dann den neuen Kurzbefehl oder drücke Löschen, um den aktuellen Kurzbefehl zu löschen.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Neuen eingeben";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Kurzbefehl eingeben";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Alten nutzen";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Cancel";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Click to record new shortcut";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Delete shortcut";
/* VoiceOver title */
"keyboard shortcut" = "keyboard shortcut";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Record Shortcut";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Shortcut cleared";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Shortcut set";
/* Shortcut glyph name for SPACE key */
"Space" = "Space";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "The key combination %@ cannot be used";
/* Message for alert when shortcut is already used by the system */
"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." = "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 */
"This shortcut cannot be used because it is already used by the menu item %@." = "This shortcut cannot be used because it is already used by the menu item %@.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Type New Shortcut";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Type Shortcut";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Use Old Shortcut";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Cancelar";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Haga clic para grabar nuevo atajo";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Borrar atajo";
/* VoiceOver title */
"keyboard shortcut" = "atajo de teklado";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Grabar atajo";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Atajo borrado";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Atajo creado";
/* Shortcut glyph name for SPACE key */
"Space" = "Espacio";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "La combinación de claves %@ no se puede utilizada";
/* Message for alert when shortcut is already used by the system */
"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." = "Esta combinación no se puede utilizar debido a que ya es en us como atajo del sistema.\nSi realmente desea utilizar esta combinación de teclas, la mayoría de los atajos se puede cambiar en el panel de Teclado y Ratón de Preferencias del Sistema.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Este atajo no se puede utilizar debido a que ya es utilizado por el elemento de menú '%@'.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Para grabar un nuevo atajo, haga clic en este botón, a continuar, escriba el nuevo atajo, o pulse borrar para qutar un atajo existente.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Escribir atajo";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Escribir atajo";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Usa atajo anterior";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Annuler";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Cliquez pour enregistrer le raccourci";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Supprimer le raccourci";
/* VoiceOver title */
"keyboard shortcut" = "raccourci clavier";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Enregistrer le raccourci";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Raccourci supprimé";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Raccourci configuré";
/* Shortcut glyph name for SPACE key */
"Space" = "Espace";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "La combinaison %@ ne peut être utilisée";
/* Message for alert when shortcut is already used by the system */
"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." = "Cette combinaison de touches ne peut être utilisée parce quelle est réservée pour un raccourci du système.\nSi vous désirez lutiliser, la plupart des raccourcis peuvent être modifiés dans longlet Clavier, dans Préférences Système.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Ce raccourci ne peut être utilisé parce quil est déjà utilisé par le point de menu «%@».";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Pour enregistrer un nouveau raccourci, cliquez sur ce bouton et tapez le nouveau raccourci, ou bien, tapez sur «Supprimer» pour supprimer le raccourci configuré.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Saisir un raccourci";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Saisir un raccourci";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Revenir au raccourci précédent";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Annulla";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Click to record new shortcut";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Delete shortcut";
/* VoiceOver title */
"keyboard shortcut" = "keyboard shortcut";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Registra scorciatoia";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Shortcut cleared";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Shortcut set";
/* Shortcut glyph name for SPACE key */
"Space" = "Spazio";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "The key combination %@ cannot be used";
/* Message for alert when shortcut is already used by the system */
"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." = "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 */
"This shortcut cannot be used because it is already used by the menu item %@." = "This shortcut cannot be used because it is already used by the menu item %@.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Digita scorciatoia";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Digita scorciatoia";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Use Old Shortcut";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "キャンセルする";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "クリックしてショートカットを入力";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "ショートカットを削除";
/* VoiceOver title */
"keyboard shortcut" = "キーボードショートカット";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "ショートカットを入力";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "ショートカットが削除されました";
/* VoiceOver: Shortcut set */
"Shortcut set" = "ショートカットが設定されました";
/* Shortcut glyph name for SPACE key */
"Space" = "スペース";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "%@ はショートカットに設定できません";
/* Message for alert when shortcut is already used by the system */
"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." = "このショートカットは、システム全体で使用されているショートカットのため、設定することができません。\nもしこのショートカットを使用したい場合、「システム環境設定」の「キーボード」、「マウス」のパネルから既に設定されているショートカットを変更してください。";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "このショートカットは、メニュー操作の「%@」で使われているため、設定できません。";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "このボタンをクリックし、ショートカットを入力すると、新しいショートカットが設定されます。また、削除ボタンをおすと、ショートカットが削除されます。";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "ショートカットを入力";
/* Empty shortcut button in recording state */
"Type Shortcut" = "ショートカットを入力";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "古いショートカットを使用";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "취소";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "클릭해 단축 키를 입력";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "단축키 삭제";
/* VoiceOver title */
"keyboard shortcut" = "키보드 단축키";
/* Alert button when shortcut is already used */
"OK" = "좋아";
/* Empty shortcut button in normal state */
"Record Shortcut" = "단축키 입력";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "단축키 삭제됨";
/* VoiceOver: Shortcut set */
"Shortcut set" = "단축키 설정됨";
/* Shortcut glyph name for SPACE key */
"Space" = "스페이스 바";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "%@ 단축키로 설정할 수 없습니다";
/* Message for alert when shortcut is already used by the system */
"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." = "이 결합은 시스템 전체에서 사용 때문에 단축키로 설정 할 수 없습니다.\n단축키를 사용하고 싶으면 시스템 환경 설정의 키보드, 마우스 패널에서 이미 설정되어있는 단축키를 변경하십시오.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "이 단축키는 ‘%@’ 메뉴 아이템에 사용되고 있기 때문에 설정할 수 없습니다.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "이 버튼을 클릭하고 단축키를 입력하면 새로운 단축키가 설정됩니다. 또한 삭제 버튼을 누르면 단축키가 삭제됩니다.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "새 단축키 입력";
/* Empty shortcut button in recording state */
"Type Shortcut" = "단축키 입력";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "오래된 단축키를 사용";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Verbreken";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Druk om een nieuwe sneltoets in te voeren";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Verwijder sneltoets";
/* VoiceOver title */
"keyboard shortcut" = "sneltoets";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Sneltoets opnemen";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Sneltoets verwijderd";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Sneltoets zetten";
/* Shortcut glyph name for SPACE key */
"Space" = "Spatie";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "De sneltoetsencombinatie kan niet worden gebruikt";
/* Message for alert when shortcut is already used by the system */
"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." = "Deze combinatie kan niet worden gebruikt want hij wordt al gebruikt door een systeembreed sneltoets.\nAls je deze combinatie echt wilt gebruiken, kun je de meeste sneltoetsen binnen Toetsenbordinstellingen veranderen.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Deze sneltoets kan niet worden gebruikt want hij wordt al gebruikt door het menu item %@.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Om nieuwe sneltoets op te nemen, druk op deze knop, en voer een nieuwe sneltoets in, of druk op verwijder om bestaande sneltoets te verwijderen.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Voer Nieuwe Sneltoets in";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Voer Sneltoets in";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Gebruik Oude Sneltoets";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Anuluj";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Kliknij, by ustawić nowy skrót";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Usuń skrót";
/* VoiceOver title */
"keyboard shortcut" = "skrót klawiszowy";
/* Alert button when shortcut is already used */
"OK" = "OK";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Utwórz skrót";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Skrót usunięty";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Skrót ustawiony";
/* Shortcut glyph name for SPACE key */
"Space" = "Spacja";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "Nie można użyć kombinacji klawiszy %@";
/* Message for alert when shortcut is already used by the system */
"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." = "Nie można użyć tej kombinacji, ponieważ jest już zajęta przez skrót systemowy.\nMożesz to zmienić w panelu Klawiatura w Preferencjach systemowych.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Ten skrót nie może być użyty, ponieważ w menu ma już przypisaną funkcję %@.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Aby ustawić nowy skrót, użyj tego przycisku, a następnie wpisz nowy skrót albo naciśnij klawisz delete, by usunąć istniejący skrót";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Wpisz nowy skrót";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Wpisz skrót";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Użyj starego skrótu";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "Отмена";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "Нажмите для записи сочетания клавиш";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "Удалить горячую клавишу";
/* VoiceOver title */
"keyboard shortcut" = "сочетание клавиш";
/* Alert button when shortcut is already used */
"OK" = "ОК";
/* Empty shortcut button in normal state */
"Record Shortcut" = "Ввести сочетание";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "Сочетание клавиш удалено";
/* VoiceOver: Shortcut set */
"Shortcut set" = "Сочетание клавиш назначено";
/* Shortcut glyph name for SPACE key */
"Space" = "Пробел";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "Нельзя использовать сочетание клавиш %@";
/* Message for alert when shortcut is already used by the system */
"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." = "Нельзя использовать это сочетание клавиш, потому что оно уже используется в системе.\n Если вы хотите использовать это сочетание, измените существующее системное сочетание клавиш через панель Клавиатура в Cистемных настройках.";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "Нельзя использовать это сочетание, потому что оно уже связано с элементом ‘%@’.";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Чтобы назначить новое сочетание клавиш, нажмите эту кнопку и введите новое сочетание, или нажмите \"Удалить\", чтобы удалить действующее сочетание клавиш.";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "Введите сочетание";
/* Empty shortcut button in recording state */
"Type Shortcut" = "Введите сочетание";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "Вернуть старое";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "取消";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "点击以记录新快捷键";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "删除快捷键";
/* VoiceOver title */
"keyboard shortcut" = "键盘快捷键";
/* Alert button when shortcut is already used */
"OK" = "好";
/* Empty shortcut button in normal state */
"Record Shortcut" = "记录快捷键";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "快捷键已清除";
/* VoiceOver: Shortcut set */
"Shortcut set" = "快捷键已设置";
/* Shortcut glyph name for SPACE key */
"Space" = "空格键";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "按键组合“%@”无法使用";
/* Message for alert when shortcut is already used by the system */
"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." = "当前按键组合无法使用,因为它已经用作其他系统全局快捷键。\n如果您真的想使用这个按键组合,大部分系统全局快捷键能在“系统偏好设置”里的“键盘”和“鼠标”面板中重设。";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "当前快捷键无法使用,因为它已用作菜单项“%@”的快捷键。";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "若要记录新的快捷键,单击此按钮,然后键入新的快捷键,或者按“delete键”删除已经存在的快捷键。";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "键入新快捷键";
/* Empty shortcut button in recording state */
"Type Shortcut" = "键入快捷键";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "还原快捷键";
+47
View File
@@ -0,0 +1,47 @@
/* Cancel action button in recording state */
"Cancel" = "取消";
/* Tooltip for non-empty shortcut button */
"Click to record new shortcut" = "點選以記錄新快捷鍵";
/* Tooltip for hint button near the non-empty shortcut */
"Delete shortcut" = "刪除快捷鍵";
/* VoiceOver title */
"keyboard shortcut" = "鍵盤快捷鍵";
/* Alert button when shortcut is already used */
"OK" = "好";
/* Empty shortcut button in normal state */
"Record Shortcut" = "記錄快捷鍵";
/* VoiceOver: Shortcut cleared */
"Shortcut cleared" = "快捷鍵已清除";
/* VoiceOver: Shortcut set */
"Shortcut set" = "快捷鍵已設定";
/* Shortcut glyph name for SPACE key */
"Space" = "空格鍵";
/* Title for alert when shortcut is already used */
"The key combination %@ cannot be used" = "按鍵組合“%@”無法使用";
/* Message for alert when shortcut is already used by the system */
"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." = "當前按鍵組合無法使用,因為它已經用作其他系統全局快捷鍵。\n如果您真的想使用這個按鍵組合,大部分系統全局快捷鍵能在“系統偏好設定”裡的“鍵盤”和“滑鼠”面板中重設。";
/* Message for alert when shortcut is already used */
"This shortcut cannot be used because it is already used by the menu item %@." = "當前快捷鍵無法使用,因為它已用作選單項“%@”的快捷鍵。";
/* VoiceOver shortcut help */
"To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "若要記錄新的快捷鍵,單擊此按鈕,然後鍵入新的快捷鍵,或者按“delete鍵”刪除已經存在的快捷鍵。";
/* Non-empty shortcut button in recording state */
"Type New Shortcut" = "鍵入新快捷鍵";
/* Empty shortcut button in recording state */
"Type Shortcut" = "鍵入快捷鍵";
/* Cancel action button for non-empty shortcut in recording state */
"Use Old Shortcut" = "還原快捷鍵";