├── .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 | ![Screencast](https://github.com/turbobabr/sketch-wanderer/blob/master/docs/intro-screencast.gif?raw=true) 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 | ![Sketch Runner screenshot](https://raw.githubusercontent.com/turbobabr/sketch-wanderer/master/docs/runner-installation.png) 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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
CommandShortcutDescription
Move Upctrl + option + Selects layer above
Move Downctrl + option + Selects layer below
Expand Groupctrl + option + Expands selected group, shape group or artboard
Collapse Groupctrl + option + Collapses selected group, shape group or artboard. When run on already collapsed group, it’s parent will be collapsed.
Select Upctrl + shift + Finder like multi-select by holding shift and selecting files via arrows.
Select Downctrl + shift + Finder like multi-select by holding shift and selecting files via arrows.
Expand Artboardctrl + shift + Expands all child groups in the current artboard.
Collapse Artboardctrl + shift + Collapses all child groups in the current artboard and artboard itself.
Select Artboard Abovectrl + option + shift + Selects artboard above current arboard. In case some child layer is selected, this command will select artboard itself.
Select Artboard Belowctrl + option + shift + Selects artboard below current artboard.
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 | ![Mission Control Shortcuts](https://github.com/turbobabr/sketch-wanderer/blob/master/docs/replacible-shortcuts.png?raw=true) 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 | ![Mission Control Shortcuts](https://github.com/turbobabr/sketch-wanderer/blob/master/docs/mission-control-shortcuts.png?raw=true) 107 | 108 | 2) In Sketch select `Plugins -> Wanderer -> Shortcuts Schema -> control+arrows` command: 109 | 110 | ![Switching shortcuts schema](https://github.com/turbobabr/sketch-wanderer/blob/master/docs/switching-shortcuts-schema.png?raw=true) 111 | 112 | 3) Wait 10-15 seconds while Sketch reloading changes and check whether new shortcuts are setup correctly: 113 | 114 | ![Successful Setup](https://github.com/turbobabr/sketch-wanderer/blob/master/docs/control-arrows-shortcuts.png?raw=true) 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; --------------------------------------------------------------------------------