├── .gitignore
├── README.md
└── Symbols
├── README.md
└── Sync Symbol.sketchplugin
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sketch Plugins
2 |
3 | This is a collection of plugins I've written for Bohemian Coding's
4 | [Sketch](http://bohemiancoding.com/sketch/).
5 |
6 | ## 1. [Symbols](https://github.com/tisho/sketch-plugins/tree/master/Symbols)
7 |
8 | ## DEPRECATED: Symbols are now part of Sketch 3.
9 |
10 | Symbols lets you mimic basic Smart Objects / Symbols functionality by automatically syncing changes between layer groups named in a particular way. As an added bonus, you can mark certain text layers in your symbols as dynamic and have their styles replicated, but not their content.
11 |
12 | [Watch demo video (~2.5 min)](https://vimeo.com/83370438)
13 |
14 | [See installation and usage instuctions](https://github.com/tisho/sketch-plugins/tree/master/Symbols).
15 |
--------------------------------------------------------------------------------
/Symbols/README.md:
--------------------------------------------------------------------------------
1 | # Sketch Symbols
2 |
3 | ## DEPRECATED: Symbols are now part of Sketch 3.
4 |
5 | Sketch Symbols is a plug-in for [Sketch](http://bohemiancoding.com/sketch/) that lets you mimic basic Smart Objects / Symbols functionality by automatically syncing changes between layer groups named in a particular way. As an added bonus, you can mark certain text layers in your symbols as dynamic and have their styles replicated, but not their content.
6 |
7 | ## Demo
8 |
9 | [](https://vimeo.com/83370438)
10 |
11 | ## Installation
12 |
13 | 1. [Download the plugin.](https://github.com/tisho/sketch-plugins/archive/master.zip)
14 | 2. Double-click the file `Sync Symbol.sketchplugin` inside `Symbols/`. Sketch should open
15 | automatically and tell you that a new plugin was installed.
16 |
17 |
18 |
19 | You should see the **Sync Symbol** entry under the Plugins menu now.
20 |
21 |
22 |
23 | ## Usage
24 |
25 | 1. Create a layer group for your symbol. (`Cmd + G`)
26 | 2. Add **": symbol-name"** to its name to mark it as a symbol. *E.g.: "signup
27 | button : button-default".*
28 |
29 | 
30 |
31 | 3. Copy the same symbol to other parts of your document. You
32 | can change the name before the colon to whatever you like.
33 | E.g.: "ok button : button-default".
34 | 4. Make changes to any of the copies of the symbol you've created. With the symbol or one of its layers still selected, press `Cmd + E` to sync these changes with other instances of the symbol.
35 |
36 | **Bonus Tip:** Open *Preferences* and under *Layers* uncheck *"Append
37 | 'Copy' after duplicated layers"*, so you don't need to tweak the symbol
38 | name after you duplicate it.
39 |
40 | ## Dynamic Text Layers
41 |
42 | Put a `$` in front of the name of any text layer inside a symbol to mark
43 | it as dynamic. When you sync changes between symbols, dynamic text layers will
44 | not be replaced. Their styles, including font size, family and line height will be updated, but their content will remain
45 | intact. This lets you define a single symbol for a button, for example, but use
46 | different copy for each instance of that button.
47 |
48 | 
49 |
50 | ## Changing the Default Keyboard Shortcut
51 |
52 | 1. Open Sketch's plugins folder. You can do that easily by choosing
53 | Custom Script from the Plugins menu, then click the gear icon and
54 | choose "Open Plugins Folder".
55 | 2. Open the file "Sync Symbols.sketchplugin" in your favorite text
56 | editor.
57 | 3. The shortcut is on the first line:
58 |
59 | ```
60 | // Syncs all instances of a symbol tagged with ": symbol-name" (cmd e)
61 | ```
62 |
63 | Change it to whatever you like (ctrl shift s, for example), and you
64 | should be good to go. The following modifiers are all valid: `control ctrl alt option cmd command shift`.
65 |
66 | ## Updating the Plugin
67 |
68 | Right now there's no automated way to update plugins. You'll have to
69 | replace the plugin files manually.
70 |
71 | 1. [Download the latest version of the plugin.](https://github.com/tisho/sketch-plugins/archive/master.zip)
72 | 2. Open Sketch's plugins folder. You can do that easily by choosing
73 | Custom Script from the Plugins menu, then click the gear icon and
74 | choose "Open Plugins Folder".
75 | 3. Replace the file `Sync Symbol.sketchplugin` with its new version from
76 | the archive you downloaded.
77 |
78 | You don't need to restart Sketch. It will pick up the changes
79 | automatically.
80 |
81 | ## Issues and Questions
82 |
83 | [File an issue on Github](https://github.com/tisho/sketch-plugins/issues), send a message to [@tisho](http://twitter.com/tisho) on Twitter, or email .
84 |
85 | ## Thanks
86 |
87 | Bohemian Coding for creating [Sketch](http://bohemiancoding.com/sketch/) in the first place and [@bomberstudios](http://twitter.com/bomberstudios) for the wonderful [sketch-commands bundle](https://github.com/bomberstudios/sketch-commands), which proved a wonderful source for learning and inspiration.
88 |
89 | ## License
90 |
91 | The MIT License (MIT)
92 |
93 | Copyright (c) 2013 Tisho Georgiev
94 |
95 | Permission is hereby granted, free of charge, to any person obtaining a copy
96 | of this software and associated documentation files (the "Software"), to deal
97 | in the Software without restriction, including without limitation the rights
98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99 | copies of the Software, and to permit persons to whom the Software is
100 | furnished to do so, subject to the following conditions:
101 |
102 | The above copyright notice and this permission notice shall be included in
103 | all copies or substantial portions of the Software.
104 |
105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
111 | THE SOFTWARE.
112 |
--------------------------------------------------------------------------------
/Symbols/Sync Symbol.sketchplugin:
--------------------------------------------------------------------------------
1 | // Syncs all instances of a symbol tagged with ": symbol-name" (cmd e)
2 | // v0.2
3 |
4 | var tagPattern = /:\s*(.*)$/;
5 |
6 | function alert(msg, title) {
7 | title = title || "Whoops";
8 | var app = [NSApplication sharedApplication];
9 | [app displayDialog:msg withTitle:title];
10 | }
11 |
12 | function getNearestTaggedLayerGroup(ref) {
13 | var klass = [ref class];
14 | if(klass === MSArtboardGroup || klass === MSPage) {
15 | return null;
16 | }
17 |
18 | while(ref && ([ref class] !== MSLayerGroup || ([ref class] === MSLayerGroup && ![ref name].match(tagPattern)))) {
19 | ref = [ref parentGroup];
20 | }
21 |
22 | return ref;
23 | }
24 |
25 | function toJSArray(arr) {
26 | var len = arr.length(), res = [];
27 |
28 | while(len--) {
29 | res.push(arr[len]);
30 | }
31 | return res;
32 | }
33 |
34 | function filterNSArray(arr, test) {
35 | var len = arr.length(), res = [];
36 | while(len--) {
37 | if(test(arr[len])) {
38 | res.push(arr[len]);
39 | }
40 | }
41 | return res;
42 | }
43 |
44 | function isGroup(layer) {
45 | var klass = [layer class];
46 | return klass === MSLayerGroup || klass === MSArtboardGroup;
47 | }
48 |
49 | function getLayerGroupsByTag(parent, tag) {
50 | var all = [parent layers];
51 | // sometimes layers returns an instance of JSCocoaController, I'm not sure why
52 | if([all class] === JSCocoaController) return [];
53 |
54 | var groups = filterNSArray(all, isGroup),
55 | tagged = [],
56 | notTagged = [];
57 |
58 | groups.forEach(function(group) {
59 | var name = [group name];
60 | var groupTag = name.match(tagPattern);
61 | if(groupTag && groupTag[1] === tag) {
62 | tagged.push(group);
63 | } else {
64 | nested = getLayerGroupsByTag(group, tag);
65 | Array.prototype.push.apply(tagged, nested);
66 | }
67 | });
68 |
69 | return tagged;
70 | }
71 |
72 | function capitalize(str) {
73 | return str.slice(0, 1).toUpperCase() + str.slice(1);
74 | }
75 |
76 | function syncProperties(src, dst, props) {
77 | for(var j=0, k=props.length; j < k; j++) {
78 | var getter = props[j];
79 | var setter = 'set' + capitalize(getter);
80 |
81 | dst[setter](src[getter]());
82 | }
83 | }
84 |
85 | function copyLayerStyle(src, dst) {
86 | var srcStyle = [src style],
87 | dstStyle = [dst style],
88 | srcContext = [srcStyle contextSettings],
89 | dstContext = [dstStyle contextSettings],
90 | collections = ['borders', 'fills', 'shadows', 'innerShadows'],
91 | props = { 'borders': ['position', 'thickness', 'fillType', 'gradient', 'isEnabled'],
92 | 'fills': ['fillType', 'gradient', 'patternImage', 'noiseIntensity', 'isEnabled', 'color'],
93 | 'shadows': ['offsetX', 'offsetY', 'blurRadius', 'spread', 'color', 'isEnabled'],
94 | 'innerShadows': ['offsetX', 'offsetY', 'blurRadius', 'spread', 'color', 'isEnabled'],
95 | 'textLayer': ['fontSize', 'fontPostscriptName', 'textColor', 'textAlignment', 'characterSpacing', 'lineSpacing']
96 | };
97 |
98 | // copy layer styles
99 | collections.forEach(function(collection) {
100 | var srcCol = srcStyle[collection](),
101 | dstCol = dstStyle[collection](),
102 | propSet = props[collection];
103 |
104 | for(var i=dstCol.length()-1; i >= 0; i--) {
105 | dstCol.removeStylePartAtIndex(i);
106 | }
107 |
108 | for(var i=0, l=srcCol.length(); i < l; i++) {
109 | var style = srcCol[i];
110 | dstCol.addNewStylePart();
111 | var newStyle = dstCol[dstCol.length() - 1];
112 |
113 | syncProperties(style, newStyle, propSet);
114 | }
115 | })
116 |
117 | // copy context settings
118 | [dstContext setOpacity:[srcContext opacity]];
119 | [dstContext setBlendMode:[srcContext blendMode]];
120 |
121 | // text layer-specific properties (font size, line spacing, etc.)
122 | if([dst class] === MSTextLayer) {
123 | syncProperties(src, dst, props['textLayer']);
124 | }
125 | }
126 |
127 | function copyLayerPosition(src, dst) {
128 | var srcFrame = [src frame],
129 | dstFrame = [dst frame];
130 |
131 | if([src class] === MSTextLayer) {
132 | var textBehaviour = [src textBehaviour], // 0 = flexible, 1 = fixed
133 | alignment = [src textAlignment]; // 0 = left, 1 = right, 2 = center, 3 = justified
134 |
135 | if(textBehaviour === 0) { // flexible text behaviour
136 | switch(alignment) {
137 | case 0: // left
138 | [dstFrame setX:[srcFrame x]];
139 | [dstFrame setY:[srcFrame y]];
140 | break;
141 | case 1: // right
142 | [dstFrame setMaxX:[srcFrame maxX]];
143 | [dstFrame setMaxY:[srcFrame maxY]];
144 | break;
145 | case 2: // center
146 | case 3: // justified
147 | [dstFrame setMidX:[srcFrame midX]];
148 | [dstFrame setMidY:[srcFrame midY]];
149 | break;
150 | }
151 | } else { // fixed text behaviour
152 | [dstFrame setX:[srcFrame x]];
153 | [dstFrame setY:[srcFrame y]];
154 | [dstFrame setWidth:[srcFrame width]];
155 | [dstFrame setHeight:[srcFrame height]];
156 | }
157 |
158 | [dst setTextBehaviour:textBehaviour];
159 | } else {
160 | [dstFrame setX:[srcFrame x]];
161 | [dstFrame setY:[srcFrame y]];
162 | [dstFrame setWidth:[srcFrame width]];
163 | [dstFrame setHeight:[srcFrame height]];
164 | }
165 | }
166 |
167 | (function main() {
168 |
169 | // HACK: on a freshly started Sketch instance, 'selection' is null until you select an object
170 | if(!(selection && [selection length])) {
171 | alert("Make sure you've selected a symbol, or a layer that belongs to one before you try to sync.");
172 | return;
173 | }
174 |
175 | var layerGroup = getNearestTaggedLayerGroup(selection[0]);
176 | if(!layerGroup) {
177 | alert("Make sure you've selected a symbol, or a layer that belongs to one before you try to sync.");
178 | return;
179 | }
180 |
181 | var name = [layerGroup name];
182 | var tag = name.match(tagPattern);
183 |
184 | var tag = tag[1],
185 | pages = [doc pages],
186 | groups = [];
187 |
188 | for(var i=0, l=pages.length(); i < l; i++) {
189 | groups = Array.prototype.concat.apply(groups, getLayerGroupsByTag(pages[i], tag));
190 | }
191 |
192 | var layers = toJSArray([layerGroup layers]);
193 |
194 | groups.forEach(function(group, i) {
195 | if(group === layerGroup) return;
196 |
197 | var targetLayers = toJSArray([group layers]),
198 | protectedLayerNames = [],
199 | protectedLayers = [];
200 |
201 | for(var i=0,l=targetLayers.length; i < l; i++) {
202 | var layer = targetLayers[i],
203 | name = ''+[layer name];
204 |
205 | if(name.slice(0, 1) === '$') {
206 | protectedLayerNames.push(name);
207 | protectedLayers.push(targetLayers[i]);
208 | }
209 |
210 | group.removeLayer(targetLayers[i]);
211 | }
212 |
213 | for(var i=layers.length - 1; i >= 0; i--) {
214 | var layer = layers[i],
215 | name = ''+[layer name];
216 |
217 | if(protectedLayerNames.indexOf(name) !== -1) {
218 | var protected = protectedLayers.pop();
219 | copyLayerStyle(layer, protected);
220 | copyLayerPosition(layer, protected);
221 | group.addLayer(protected);
222 |
223 | } else {
224 | var copy = [layer duplicate];
225 | layerGroup.removeLayer(copy);
226 | group.addLayer(copy);
227 | }
228 | }
229 |
230 | group.resizeRoot();
231 | });
232 | })();
233 |
--------------------------------------------------------------------------------