├── .appcast.xml
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── Wanderer.sketchplugin
└── Contents
│ └── Sketch
│ ├── manifest.json
│ └── plugin.js
├── docs
├── control-arrows-shortcuts.png
├── intro-screencast.gif
├── mission-control-shortcuts.png
├── replacible-shortcuts.png
├── runner-installation.png
└── switching-shortcuts-schema.png
├── package-lock.json
├── package.json
└── src
├── actions-manager.js
├── commands
├── collapse.js
├── expand.js
├── move.js
├── select-artboard.js
├── select.js
└── switch-shortcuts-schema.js
├── constants.js
├── manifest.json
├── plugin.js
└── utils.js
/.appcast.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,json,html,md}]
2 | charset = utf-8
3 | indent_style = space
4 | indent_size = 2
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | end_of_line = lf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # build artefacts
3 | Wanderer.sketchplugin.zip
4 |
5 | node_modules
6 | .DS_Store
7 | .idea
8 | npm-debug.log
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Andrey Shakhmin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sketch Wanderer
2 |
3 | A small SketchApp extension that introduce frictionless `Finder` like navigation in layer list by using beloved arrow keys and simple shortcuts. No more `shift`/`shift-tab` nightmare!
4 |
5 | > Huge thanks to [Sasha Okunev](https://twitter.com/okunev) who inspired me to build this plugin! :)
6 |
7 | 
8 |
9 |
10 | ## Install with Sketch Runner
11 |
12 | With Sketch Runner, just go to the `install` command and search for `Wanderer`. Runner allows you to manage plugins and do much more to speed up your workflow in Sketch. [Download Runner here](http://www.sketchrunner.com).
13 |
14 | 
15 |
16 |
17 |
18 | ## Manual Installation
19 |
20 | 1. [Download](https://github.com/turbobabr/sketch-wanderer/releases/download/v1.0.1/Wanderer.sketchplugin.zip) the plugin.
21 | 2. Unpack the archive and double click on `Wanderer.sketchplugin` file to install it into Sketch plugins folder.
22 | 3. Enjoy! :)
23 |
24 |
25 | ## Usage
26 |
27 | Wanderer is very simple to use, just recall how navigation works in Finder and use any arrow keys with `control-option` modifiers.
28 |
29 |
30 | ## The complete list of supported commands:
31 |
32 |
33 |
34 |
35 | Command |
36 | Shortcut |
37 | Description |
38 |
39 |
40 |
41 |
42 | Move Up |
43 | ctrl + option + ↑ |
44 | Selects layer above |
45 |
46 |
47 | Move Down |
48 | ctrl + option + ↓ |
49 | Selects layer below |
50 |
51 |
52 | Expand Group |
53 | ctrl + option + → |
54 | Expands selected group, shape group or artboard |
55 |
56 |
57 | Collapse Group |
58 | ctrl + option + ← |
59 | Collapses selected group, shape group or artboard. When run on already collapsed group, it’s parent will be collapsed. |
60 |
61 |
62 | Select Up |
63 | ctrl + shift + ↑ |
64 | Finder like multi-select by holding shift and selecting files via arrows. |
65 |
66 |
67 | Select Down |
68 | ctrl + shift + ↓ |
69 | Finder like multi-select by holding shift and selecting files via arrows. |
70 |
71 |
72 | Expand Artboard |
73 | ctrl + shift + → |
74 | Expands all child groups in the current artboard. |
75 |
76 |
77 | Collapse Artboard |
78 | ctrl + shift + ← |
79 | Collapses all child groups in the current artboard and artboard itself. |
80 |
81 |
82 | Select Artboard Above |
83 | ctrl + option + shift + ↑ |
84 | Selects artboard above current arboard. In case some child layer is selected, this command will select artboard itself. |
85 |
86 |
87 | Select Artboard Below |
88 | ctrl + option + shift + ↓ |
89 | Selects artboard below current artboard. |
90 |
91 |
92 |
93 |
94 |
95 |
96 | ## Simple Shortcuts Schema & Mission Control
97 |
98 | By default Wanderer uses `control-option+arrows` shortcuts for the most common commands like `Move Up`, `Move Down` and `Expand/Collapse Group`:
99 |
100 | 
101 |
102 | It's quite nice, but not perfect - the better solution is to use shorter set `control+arrows`, which is by default occupied by `macOS: Mission Control`. Just in case you don't use these shortcuts, you could quickly replace them by using a special command by doing these simple steps:
103 |
104 | 1) Open `System Preferences -> Keyboard -> Shortcuts -> Mission Control` and disable the following highlighted system shortcuts:
105 |
106 | 
107 |
108 | 2) In Sketch select `Plugins -> Wanderer -> Shortcuts Schema -> control+arrows` command:
109 |
110 | 
111 |
112 | 3) Wait 10-15 seconds while Sketch reloading changes and check whether new shortcuts are setup correctly:
113 |
114 | 
115 |
116 | > Note: In case you'll need to revert changes, just run `Plugins -> Wanderer -> Shortcuts Schema -> control-option+arrows` command.
117 |
118 |
119 |
120 | ## Version history
121 |
122 | **Wanderer - 1.0.1: 3/16/2018**
123 | * Sketch 49 support
124 | * New plugins auto-updating system support
125 |
126 | **Wanderer 1.0.0: 12/5/2016**
127 | * First version
128 |
129 |
130 | ## Feedback
131 |
132 | If you discover any issue or have any suggestions for improvement of the plugin, please [open an issue](https://github.com/turbobabr/sketch-wanderer/issues) or find me on twitter [@turbobabr](http://twitter.com/turbobabr).
133 |
134 |
135 |
136 | ## License
137 |
138 | The MIT License (MIT)
139 |
140 | Copyright (c) 2017-2018 Andrey Shakhmin
141 |
142 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
143 |
144 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
145 |
146 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
147 |
--------------------------------------------------------------------------------
/Wanderer.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "com.turbobabr.sketch.wanderer",
3 | "compatibleVersion": 3,
4 | "bundleVersion": 1,
5 | "commands": [
6 | {
7 | "name": "Move Up",
8 | "identifier": "moveUp",
9 | "shortcut": "control option ↑",
10 | "script": "plugin.js"
11 | },
12 | {
13 | "name": "Move Down",
14 | "identifier": "moveDown",
15 | "shortcut": "control option ↓",
16 | "script": "plugin.js"
17 | },
18 | {
19 | "name": "Select Up",
20 | "identifier": "selectUp",
21 | "shortcut": "control shift ↑",
22 | "script": "plugin.js"
23 | },
24 | {
25 | "name": "Select Down",
26 | "identifier": "selectDown",
27 | "shortcut": "control shift ↓",
28 | "script": "plugin.js"
29 | },
30 | {
31 | "name": "Select Artboard Above",
32 | "identifier": "selectArtboardAbove",
33 | "shortcut": "control shift option ↑",
34 | "script": "plugin.js"
35 | },
36 | {
37 | "name": "Select Artboard Below",
38 | "identifier": "selectArtboardBelow",
39 | "shortcut": "control shift option ↓",
40 | "script": "plugin.js"
41 | },
42 | {
43 | "name": "Expand Group",
44 | "identifier": "expand",
45 | "shortcut": "control option →",
46 | "script": "plugin.js"
47 | },
48 | {
49 | "name": "Expand Artboard",
50 | "identifier": "expandArtboard",
51 | "shortcut": "control shift →",
52 | "script": "plugin.js"
53 | },
54 | {
55 | "name": "Collapse Group",
56 | "identifier": "collapse",
57 | "shortcut": "control option ←",
58 | "script": "plugin.js"
59 | },
60 | {
61 | "name": "Collapse Artboard",
62 | "identifier": "collapseArtboard",
63 | "shortcut": "control shift ←",
64 | "script": "plugin.js"
65 | },
66 | {
67 | "name": "✓ control-option+arrows",
68 | "identifier": "controlOptionSchema",
69 | "script": "plugin.js"
70 | },
71 | {
72 | "name": " control+arrows",
73 | "identifier": "controlSchema",
74 | "script": "plugin.js"
75 | },
76 | {
77 | "name": "Help...",
78 | "identifier": "help",
79 | "script": "plugin.js"
80 | }
81 | ],
82 | "menu": {
83 | "items": [
84 | "moveUp",
85 | "moveDown",
86 | "-",
87 | "selectUp",
88 | "selectDown",
89 | "-",
90 | "expand",
91 | "collapse",
92 | "-",
93 | "expandArtboard",
94 | "collapseArtboard",
95 | "-",
96 | "selectArtboardAbove",
97 | "selectArtboardBelow",
98 | "-",
99 | {
100 | "title": "Shortcuts Schema",
101 | "items": [
102 | "controlOptionSchema",
103 | "controlSchema"
104 | ]
105 | },
106 | "-",
107 | "help"
108 | ]
109 | },
110 | "version": "1.0.1",
111 | "description": "A small SketchApp extension that introduces 'Finder' like features for working with layer list.",
112 | "homepage": "https://github.com/turbobabr/sketch-wanderer#readme",
113 | "name": "Wanderer",
114 | "disableCocoaScriptPreprocessor": true,
115 | "appcast": "https://raw.githubusercontent.com/turbobabr/sketch-wanderer/master/.appcast.xml",
116 | "author": "Andrey Shakhmin",
117 | "authorEmail": "andrey.shakhmin@gmail.com"
118 | }
--------------------------------------------------------------------------------
/docs/control-arrows-shortcuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/control-arrows-shortcuts.png
--------------------------------------------------------------------------------
/docs/intro-screencast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/intro-screencast.gif
--------------------------------------------------------------------------------
/docs/mission-control-shortcuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/mission-control-shortcuts.png
--------------------------------------------------------------------------------
/docs/replacible-shortcuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/replacible-shortcuts.png
--------------------------------------------------------------------------------
/docs/runner-installation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/runner-installation.png
--------------------------------------------------------------------------------
/docs/switching-shortcuts-schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/turbobabr/sketch-wanderer/149e4e84d3478147bb2c48d8da14168baa6f47ab/docs/switching-shortcuts-schema.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sketch-wanderer",
3 | "version": "1.0.1",
4 | "description": "A small SketchApp extension that introduces 'Finder' like features for working with layer list.",
5 | "scripts": {
6 | "build": "skpm-build",
7 | "watch": "skpm-build --watch",
8 | "start": "skpm-build --watch --run",
9 | "postinstall": "npm run build && skpm-link"
10 | },
11 | "skpm": {
12 | "name": "Wanderer",
13 | "manifest": "src/manifest.json",
14 | "main": "Wanderer.sketchplugin",
15 | "assets": [
16 | "assets/**/*"
17 | ]
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/turbobabr/sketch-wanderer.git"
22 | },
23 | "keywords": [
24 | "sketch",
25 | "extension",
26 | "plugin",
27 | "navigation"
28 | ],
29 | "author": "Andrey Shakhmin ",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/turbobabr/sketch-wanderer/issues"
33 | },
34 | "homepage": "https://github.com/turbobabr/sketch-wanderer#readme",
35 | "devDependencies": {
36 | "@skpm/builder": "^0.4.0"
37 | },
38 | "dependencies": {
39 | "lodash": "^4.17.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/actions-manager.js:
--------------------------------------------------------------------------------
1 |
2 | import Utils from './utils';
3 |
4 | export const ActionType = {
5 | None: 'none',
6 | SelectUp: 'selectUp',
7 | SelectDown: 'selectDown',
8 | MoveUp: 'moveUp',
9 | MoveDown: 'moveDown',
10 | Expand: 'expand',
11 | Collapse: 'collapse',
12 | SelectArtboardAbove: 'selectArtboardAbove',
13 | SelectArtboardBelow: 'selectArtboardBelow'
14 | };
15 |
16 | const keyForProperty = (prop) => {
17 | return `com.turbobabr.sketch.wanderer.${prop}`;
18 | };
19 |
20 | const LAST_ACTION_KEY = keyForProperty('last-action');
21 | const BASE_ROW_KEY = keyForProperty('base-row');
22 | const BERSERK_BASE_ROW_KEY = keyForProperty('berserk-base-row');
23 |
24 | class ActionsManager {
25 | get ThreadDictionary() {
26 | return NSThread.currentThread().threadDictionary();
27 | }
28 |
29 | get(prop) {
30 | return Utils.normalize(this.ThreadDictionary[prop]);
31 | }
32 |
33 | set(prop,value) {
34 | this.ThreadDictionary[prop] = value;
35 | }
36 |
37 | get lastAction() {
38 | return this.get(LAST_ACTION_KEY) || ActionType.None;
39 | }
40 | set lastAction(value) {
41 | this.set(LAST_ACTION_KEY,value);
42 | }
43 |
44 | setLastAction(action) {
45 | this.lastAction = action;
46 | }
47 |
48 | get baseRowIndex() {
49 | return this.get(BASE_ROW_KEY) || -1;
50 | }
51 | set baseRowIndex(value) {
52 | this.set(BASE_ROW_KEY,value);
53 | }
54 |
55 | get berserkBaseRowIndex() {
56 | return this.get(BERSERK_BASE_ROW_KEY) || -1;
57 | }
58 | set berserkBaseRowIndex(value) {
59 | this.set(BERSERK_BASE_ROW_KEY,value);
60 | }
61 | }
62 |
63 |
64 | export default new ActionsManager();
65 |
--------------------------------------------------------------------------------
/src/commands/collapse.js:
--------------------------------------------------------------------------------
1 | import Utils from '../utils';
2 | import { GroupExpandedType, TargetGroupType } from '../constants';
3 |
4 | const collapse = (context,target) => {
5 | const { selection, document } = context;
6 | const page = document.currentPage();
7 | const currentArtboard = page.currentArtboard();
8 |
9 | if(target == TargetGroupType.ArtboardGraph) {
10 | const targetArtboards = Utils.normalize(selection.valueForKeyPath("@distinctUnionOfObjects.parentArtboard"));
11 | _.each(targetArtboards,(artboard) => {
12 | const predicate = NSPredicate.predicateWithFormat('(SELF isKindOfClass:%@) AND (expandableInLayerList == TRUE) AND (isExpanded == TRUE)', MSLayerGroup.class());
13 | const targetGroups = artboard.children().filteredArrayUsingPredicate(predicate);
14 | _.each(Utils.normalize(targetGroups),layer => layer.layerListExpandedType = GroupExpandedType.Collapsed);
15 | });
16 |
17 | const resultingResponder = currentArtboard || _.first(targetArtboards);
18 | if(resultingResponder) {
19 | resultingResponder.select_byExpandingSelection(true,false);
20 | }
21 |
22 | Utils.refreshLayerList(context);
23 | } else if(target == TargetGroupType.Selection) {
24 | const layer = selection.firstObject();
25 | if (layer && layer.isKindOfClass(MSLayerGroup) && layer.expandableInLayerList() && layer.isExpanded()) {
26 | layer.layerListExpandedType = GroupExpandedType.Collapsed;
27 | Utils.refreshLayerList(context);
28 | } else if (layer) {
29 | const parent = layer.parentGroup();
30 | if (parent.isKindOfClass(MSPage)) {
31 | return;
32 | }
33 |
34 | parent.layerListExpandedType = GroupExpandedType.Collapsed;
35 | parent.select_byExpandingSelection(true, false);
36 | Utils.refreshLayerList(context);
37 | }
38 | } else if(target == TargetGroupType.PageGraph) {
39 | // TODO: TO IMPLEMENT
40 | }
41 | };
42 |
43 | export default collapse;
--------------------------------------------------------------------------------
/src/commands/expand.js:
--------------------------------------------------------------------------------
1 |
2 | import _ from 'lodash';
3 | import Utils from '../utils';
4 | import { GroupExpandedType, TargetGroupType } from '../constants';
5 |
6 | const expand = (context,target) => {
7 | const { selection } = context;
8 |
9 | if(target == TargetGroupType.ArtboardGraph) {
10 | var artboards = selection.valueForKeyPath("@distinctUnionOfObjects.parentArtboard");
11 | _.each(Utils.normalize(artboards),(artboard) => {
12 | var predicate = NSPredicate.predicateWithFormat('(SELF isKindOfClass:%@) AND (expandableInLayerList == TRUE) AND (isExpanded == FALSE)', MSLayerGroup.class());
13 | var targetGroups = artboard.children().filteredArrayUsingPredicate(predicate);
14 | _.each(Utils.normalize(targetGroups),(layer) => {
15 | layer.layerListExpandedType = GroupExpandedType.Expanded;
16 | });
17 | });
18 |
19 | Utils.refreshLayerList(context);
20 | } else if(target == TargetGroupType.Selection) {
21 | const layer = selection.firstObject();
22 | if (layer && layer.isKindOfClass(MSLayerGroup) && layer.expandableInLayerList() && !layer.isExpanded()) {
23 | layer.layerListExpandedType = GroupExpandedType.Expanded;
24 | Utils.refreshLayerList(context);
25 | } else {
26 | const parent = layer.parentGroup();
27 | if (!parent) {
28 | return;
29 | }
30 |
31 | var predicate = NSPredicate.predicateWithFormat('(SELF isKindOfClass:%@) AND (expandableInLayerList == TRUE) AND (isExpanded == FALSE)', MSLayerGroup.class());
32 | _.each(Utils.normalize(parent.layers().filteredArrayUsingPredicate(predicate)), (layer) => {
33 | layer.layerListExpandedType = GroupExpandedType.Expanded;
34 | });
35 |
36 | Utils.refreshLayerList(context);
37 | }
38 | }
39 | };
40 |
41 | export default expand;
--------------------------------------------------------------------------------
/src/commands/move.js:
--------------------------------------------------------------------------------
1 |
2 | import Utils from '../utils';
3 | import { Direction, GroupExpandedType } from '../constants';
4 |
5 | const move = (context,direction) => {
6 | const view = Utils.layerListView();
7 | var index = view.selectedRow();
8 | const currentLayer = view.itemAtRow(index);
9 |
10 | index = index != -1 ? index : 0;
11 |
12 | switch(direction) {
13 | case Direction.Up:
14 | index--;
15 | break;
16 |
17 | case Direction.Down:
18 | index++;
19 | break;
20 | }
21 |
22 | const numberOfRows = view.numberOfRows();
23 | if(index < 0) {
24 | index = numberOfRows - 1;
25 | } else if(index >= numberOfRows) {
26 | index = 0;
27 | }
28 |
29 | var targetLayer = view.itemAtRow(index);
30 | if(currentLayer && targetLayer && direction == Direction.Up &&
31 | currentLayer.valueForKeyPath('parentGroup.objectID') == targetLayer.objectID() &&
32 | targetLayer.layerListExpandedType() == GroupExpandedType.Undecided) {
33 | console.log("Have to expand item directly!");
34 | targetLayer.layerListExpandedType = GroupExpandedType.Expanded;
35 | }
36 |
37 |
38 | view.selectRowIndexes_byExtendingSelection(NSIndexSet.indexSetWithIndex(index),false);
39 | };
40 |
41 | export default move;
42 |
--------------------------------------------------------------------------------
/src/commands/select-artboard.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import Utils from '../utils';
3 | import { Direction, CanvasAction } from '../constants';
4 |
5 | const performCanvasAction = (targetArtboard,action) => {
6 | if(action == CanvasAction.None) {
7 | return;
8 | }
9 |
10 | switch(action) {
11 | case CanvasAction.Center:
12 | // TODO: To implement
13 | break;
14 |
15 | case CanvasAction.ZoomToFit:
16 | // TODO: To implement
17 | break;
18 | }
19 | };
20 |
21 | const selectArtboard = (context, direction,action = CanvasAction.None) => {
22 | const { document, selection } = context;
23 | const page = document.currentPage();
24 | const currentArtboard = page.currentArtboard();
25 | if(!currentArtboard) {
26 | // TODO: Add warning message.
27 | return;
28 | }
29 |
30 | const isPartOfSelection = Utils.findOne(selection,`objectID == '${currentArtboard.objectID()}'`) != null;
31 | if(!isPartOfSelection && direction == Direction.Up) {
32 | currentArtboard.select_byExpandingSelection(true,false);
33 | return;
34 | }
35 |
36 | const artboards = _.reverse(Utils.normalize(page.artboards()));
37 | let index = _.findIndex(artboards,ab => ab.objectID() == currentArtboard.objectID());
38 | if(index == -1) {
39 | // TODO: Add warning message.
40 | return;
41 | }
42 |
43 | switch(direction) {
44 | case Direction.Up:
45 | index--;
46 | break;
47 |
48 | case Direction.Down:
49 | index++;
50 | break;
51 | }
52 |
53 | if(index<0) {
54 | index = artboards.length - 1;
55 | } else if(index>=artboards.length) {
56 | index = 0;
57 | }
58 |
59 | const targetArtboard = artboards[index];
60 | targetArtboard.select_byExpandingSelection(true,false);
61 | performCanvasAction(targetArtboard,action);
62 | };
63 |
64 | export default selectArtboard;
65 |
--------------------------------------------------------------------------------
/src/commands/select.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import Utils from '../utils';
3 | import { Direction, GroupExpandedType } from '../constants';
4 | import ActionsManager, { ActionType } from '../actions-manager';
5 |
6 | const select = (context, direction) => {
7 | var view = Utils.layerListView();
8 | if (!_.includes([ActionType.SelectDown, ActionType.SelectUp],ActionsManager.lastAction)) {
9 | ActionsManager.baseRowIndex = view.selectedRow();
10 | }
11 |
12 | ActionsManager.berserkBaseRowIndex = view.selectedRow();
13 | var baseRowIndex = ActionsManager.baseRowIndex;
14 |
15 | const selectedIndexes = view.selectedRowIndexes();
16 | const firstSelectedIndex = selectedIndexes.firstIndex();
17 | const lastSelectedIndex = selectedIndexes.lastIndex();
18 |
19 | const selectIndex = (index) => {
20 | var item = view.itemAtRow(index);
21 | if(item) { item.select_byExpandingSelection(true, true); }
22 | };
23 |
24 | const deselectIndex = (index) => {
25 | var item = view.itemAtRow(index);
26 | if(item) { item.select_byExpandingSelection(false, true); }
27 | };
28 |
29 | if (direction == Direction.Up) {
30 | if (lastSelectedIndex > baseRowIndex) {
31 | deselectIndex(lastSelectedIndex);
32 | } else if (firstSelectedIndex <= baseRowIndex) {
33 | selectIndex(firstSelectedIndex - 1);
34 | }
35 | } else if (direction == Direction.Down) {
36 | if(firstSelectedIndex < baseRowIndex) {
37 | deselectIndex(firstSelectedIndex);
38 | } else if (lastSelectedIndex >= baseRowIndex) {
39 | selectIndex(lastSelectedIndex + 1);
40 | }
41 | }
42 | };
43 |
44 | export default select;
45 |
--------------------------------------------------------------------------------
/src/commands/switch-shortcuts-schema.js:
--------------------------------------------------------------------------------
1 |
2 | import Utils from '../utils';
3 | import { ShortcutsSchema } from '../constants';
4 |
5 | const KeyMap = {
6 | Up: '↑',
7 | Down: '↓',
8 | Right: '→',
9 | Left: '←',
10 | Control: 'control',
11 | Options: 'option'
12 | };
13 |
14 | const switchShortcutsSchema = (schema) => {
15 | const symbolsMap = {
16 | moveUp: KeyMap.Up,
17 | moveDown: KeyMap.Down,
18 | collapse: KeyMap.Left,
19 | expand: KeyMap.Right
20 | };
21 |
22 | let commandsPatch = {};
23 |
24 | const modifiers = schema == ShortcutsSchema.Default ? [KeyMap.Control,KeyMap.Options] : [KeyMap.Control];
25 | _.each(symbolsMap,(value,key) => {
26 | const shortcut = modifiers.concat([value]).join(' ');
27 | commandsPatch[key] = { shortcut };
28 | });
29 |
30 | commandsPatch['controlOptionSchema'] = { name: schema === ShortcutsSchema.Default ? "✓ control-option+arrows" : " control-option+arrows" };
31 | commandsPatch['controlSchema'] = { name: schema !== ShortcutsSchema.Default ? "✓ control+arrows" : " control+arrows" };
32 |
33 | const filePath = Utils.manifestFilePath();
34 | let manifest = Utils.readJSON(filePath);
35 |
36 | manifest = _.assign({},manifest,{
37 | commands: _.map(manifest.commands,(command) =>{
38 | const id = command.identifier;
39 | if(commandsPatch[id]) {
40 | return _.assign({},command,commandsPatch[id]);
41 | }
42 |
43 | return command;
44 | })
45 | });
46 |
47 | Utils.writeJSON(filePath,manifest);
48 | };
49 |
50 | export default switchShortcutsSchema;
51 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 |
2 | export const Direction = {
3 | Up: 'up',
4 | Down: 'down'
5 | };
6 |
7 | export const GroupExpandedType = {
8 | Undecided: 0,
9 | Collapsed: 1,
10 | Expanded : 2
11 | };
12 |
13 | export const ShortcutsSchema = {
14 | Default: 'default',
15 | ControlOnly: 'controlOnly'
16 | };
17 |
18 | export const TargetGroupType = {
19 | Selection: 'selection',
20 | Artboard: 'artboard',
21 | ArtboardGraph: 'artboardGraph',
22 | PageGraph: 'pageGraph'
23 | };
24 |
25 | export const CanvasAction = {
26 | None: 'none',
27 | Center: 'center',
28 | ZoomToFit: 'center'
29 | };
30 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "com.turbobabr.sketch.wanderer",
3 | "compatibleVersion": 3,
4 | "bundleVersion": 1,
5 | "commands": [{
6 | "name": "Move Up",
7 | "identifier": "moveUp",
8 | "shortcut": "control option ↑",
9 | "script": "./plugin.js"
10 | },
11 | {
12 | "name": "Move Down",
13 | "identifier": "moveDown",
14 | "shortcut": "control option ↓",
15 | "script": "./plugin.js"
16 | },
17 | {
18 | "name": "Select Up",
19 | "identifier": "selectUp",
20 | "shortcut": "control shift ↑",
21 | "script": "./plugin.js"
22 | },
23 | {
24 | "name": "Select Down",
25 | "identifier": "selectDown",
26 | "shortcut": "control shift ↓",
27 | "script": "./plugin.js"
28 | },
29 | {
30 | "name": "Select Artboard Above",
31 | "identifier": "selectArtboardAbove",
32 | "shortcut": "control shift option ↑",
33 | "script": "./plugin.js"
34 | },
35 | {
36 | "name": "Select Artboard Below",
37 | "identifier": "selectArtboardBelow",
38 | "shortcut": "control shift option ↓",
39 | "script": "./plugin.js"
40 | },
41 | {
42 | "name": "Expand Group",
43 | "identifier": "expand",
44 | "shortcut": "control option →",
45 | "script": "./plugin.js"
46 | },
47 | {
48 | "name": "Expand Artboard",
49 | "identifier": "expandArtboard",
50 | "shortcut": "control shift →",
51 | "script": "./plugin.js"
52 | },
53 | {
54 | "name": "Collapse Group",
55 | "identifier": "collapse",
56 | "shortcut": "control option ←",
57 | "script": "./plugin.js"
58 | },
59 | {
60 | "name": "Collapse Artboard",
61 | "identifier": "collapseArtboard",
62 | "shortcut": "control shift ←",
63 | "script": "./plugin.js"
64 | },
65 | {
66 | "name": "✓ control-option+arrows",
67 | "identifier": "controlOptionSchema",
68 | "script": "./plugin.js"
69 | },
70 | {
71 | "name": " control+arrows",
72 | "identifier": "controlSchema",
73 | "script": "./plugin.js"
74 | },
75 | {
76 | "name": "Help...",
77 | "identifier": "help",
78 | "script": "./plugin.js"
79 | }
80 | ],
81 | "menu": {
82 | "items": [
83 | "moveUp",
84 | "moveDown",
85 | "-",
86 | "selectUp",
87 | "selectDown",
88 | "-",
89 | "expand",
90 | "collapse",
91 | "-",
92 | "expandArtboard",
93 | "collapseArtboard",
94 | "-",
95 | "selectArtboardAbove",
96 | "selectArtboardBelow",
97 | "-",
98 | {
99 | "title": "Shortcuts Schema",
100 | "items": [
101 | "controlOptionSchema",
102 | "controlSchema"
103 | ]
104 | },
105 | "-",
106 | "help"
107 | ]
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
1 | import { includes } from 'lodash';
2 | import Utils from './utils';
3 |
4 | import move from './commands/move';
5 | import expand from './commands/expand';
6 | import collapse from './commands/collapse';
7 | import select from './commands/select';
8 | import selectArtboard from './commands/select-artboard';
9 | import switchShortcutsSchema from './commands/switch-shortcuts-schema';
10 | import ActionsManager, {
11 | ActionType
12 | } from './actions-manager';
13 | import {
14 | Direction,
15 | ShortcutsSchema,
16 | TargetGroupType
17 | } from './constants';
18 |
19 | const commandHandlers = {
20 | moveUp: (context) => {
21 | move(context, Direction.Up);
22 | ActionsManager.setLastAction(ActionType.MoveUp);
23 | },
24 | moveDown: (context) => {
25 | move(context, Direction.Down);
26 | ActionsManager.setLastAction(ActionType.MoveDown);
27 | },
28 | selectUp: (context) => {
29 | select(context, Direction.Up);
30 | ActionsManager.setLastAction(ActionType.SelectUp);
31 | },
32 | selectDown: (context) => {
33 | select(context, Direction.Down);
34 | ActionsManager.setLastAction(ActionType.SelectDown);
35 | },
36 | selectArtboardAbove: (context) => {
37 | selectArtboard(context, Direction.Up);
38 | ActionsManager.setLastAction(ActionType.SelectArtboardAbove);
39 | },
40 | selectArtboardBelow: (context) => {
41 | selectArtboard(context, Direction.Down);
42 | ActionsManager.setLastAction(ActionType.SelectArtboardBelow);
43 | },
44 | expand: (context) => {
45 | expand(context, TargetGroupType.Selection);
46 | ActionsManager.setLastAction(ActionType.Expand);
47 | },
48 | expandArtboard: (context) => {
49 | expand(context, TargetGroupType.ArtboardGraph);
50 | ActionsManager.setLastAction(ActionType.Expand);
51 | },
52 | collapse: (context) => {
53 | collapse(context, TargetGroupType.Selection);
54 | ActionsManager.setLastAction(ActionType.Collapse);
55 | },
56 | collapseArtboard: (context) => {
57 | collapse(context, TargetGroupType.ArtboardGraph);
58 | ActionsManager.setLastAction(ActionType.Collapse);
59 | },
60 | controlOptionSchema: (context) => {
61 | switchShortcutsSchema(ShortcutsSchema.Default);
62 | },
63 | controlSchema: (context) => {
64 | switchShortcutsSchema(ShortcutsSchema.ControlOnly);
65 | },
66 | help: (context) => {
67 | Utils.openUrlInDefaultBrowser('https://github.com/turbobabr/sketch-wanderer');
68 | }
69 | };
70 |
71 | const runForeverBlackList = ['controlOptionSchema', 'controlSchema', 'help'];
72 | const runCommand = (id, contex) => {
73 | if (!commandHandlers[id]) {
74 | console.warn(`[sketch-wanderer]: Unknown command - '${id}'`);
75 | return;
76 | }
77 |
78 | if (!includes(runForeverBlackList, id)) {
79 | Utils.runForever();
80 | }
81 |
82 | commandHandlers[id](context);
83 | }
84 |
85 | export default function (context) {
86 | const commandIdentifier = Utils.normalize(context.command.identifier());
87 | runCommand(commandIdentifier, context);
88 | }
89 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | const Utils = {};
4 |
5 | Utils.normalizeObject = (obj) => {
6 | if(!obj) {
7 | return null;
8 | }
9 |
10 | if(obj.isKindOfClass(NSString)) {
11 | return obj.UTF8String();
12 | } else if(obj.isKindOfClass(NSValue)) {
13 | return obj + 0;
14 | } else if(obj.isKindOfClass(NSDictionary)) {
15 | return _.fromPairs(_.map(obj,(value,key) => {
16 | return [key,Utils.normalizeObject(value)];
17 | }));
18 | } else if(obj.isKindOfClass(NSArray)) {
19 | return _.map(obj,(value) => {
20 | return Utils.normalizeObject(value);
21 | });
22 | }
23 |
24 | return obj;
25 | };
26 |
27 | Utils.normalize = (obj) => { // Alias for `normalizeObject`
28 | return Utils.normalizeObject(obj);
29 | };
30 |
31 | Utils.readJSON = (filePath) => {
32 | let str = NSString.stringWithContentsOfFile_encoding_error(filePath,NSUTF8StringEncoding,null);
33 | if(!str) {
34 | return null;
35 | }
36 |
37 | str = Utils.normalize(str);
38 | try {
39 | return JSON.parse(str);
40 | } catch(e) {
41 | return null;
42 | }
43 | };
44 |
45 | Utils.writeJSON = (filePath,obj,prettyPrint = true) => {
46 | let str = prettyPrint ? JSON.stringify(obj,null,4) : JSON.stringify(obj);
47 | str = NSString.stringWithString(str);
48 | return str.writeToFile_atomically_encoding_error(filePath,true,NSUTF8StringEncoding,null);
49 | };
50 |
51 | Utils.currentDocument = () => {
52 | return MSDocument.currentDocument();
53 | };
54 |
55 | Utils.currentCommand = () => {
56 | return coscript.printController();
57 | };
58 |
59 | Utils.currentPluginBundle = () => {
60 | return Utils.currentCommand().pluginBundle();
61 | };
62 |
63 | Utils.manifestFilePath = () => {
64 | const pluginBundle = Utils.currentPluginBundle();
65 | return `${Utils.normalize(pluginBundle.url().path())}/Contents/Sketch/manifest.json`;
66 | };
67 |
68 | Utils.runForever = () => {
69 | coscript.setShouldKeepAround(true);
70 | };
71 |
72 | Utils.stopRunningForever = () => {
73 | coscript.setShouldKeepAround(false);
74 | };
75 |
76 | Utils.refreshLayerList = () => {
77 | Utils.layerListViewController().refresh();
78 | };
79 |
80 | Utils.filterArray = (array,predicateFormat) => {
81 | return array.filteredArrayUsingPredicate(NSPredicate.predicateWithFormat(predicateFormat));
82 | };
83 |
84 | Utils.findOne = (array,predicateFormat) => {
85 | return Utils.filterArray(array,predicateFormat).firstObject();
86 | };
87 |
88 | Utils.showMessage = (msg) => {
89 | const document = Utils.currentDocument();
90 | document.showMessage(`[walker]: ${msg}`);
91 | };
92 |
93 | Utils.openUrlInDefaultBrowser = (url) => {
94 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url));
95 | };
96 |
97 | Utils.layerListViewController = () => {
98 | return Utils.currentDocument().valueForKeyPath('sidebarController.layerListViewController');
99 | };
100 |
101 |
102 | Utils.layerListView = () => {
103 | return Utils.currentDocument().valueForKeyPath('sidebarController.layerListViewController.outlineView');
104 | };
105 |
106 |
107 | export default Utils;
--------------------------------------------------------------------------------