├── README.md
├── Slicer.sketchplugin
└── Contents
│ ├── Resources
│ └── UIBundle
│ │ └── Contents
│ │ └── Resources
│ │ ├── MyNibUI.nib
│ │ ├── designable.nib
│ │ └── keyedobjects.nib
│ │ ├── icon-error@2x.png
│ │ ├── icon@2x.png
│ │ ├── patch-error-bottom-padding@2x.png
│ │ ├── patch-error-bottom@2x.png
│ │ ├── patch-error-left-padding@2x.png
│ │ ├── patch-error-left@2x.png
│ │ ├── patch-error-right-padding@2x.png
│ │ ├── patch-error-right@2x.png
│ │ ├── patch-error-structure@2x.gif
│ │ ├── patch-error-top-padding@2x.png
│ │ └── patch-error-top@2x.png
│ └── Sketch
│ ├── commands.js
│ ├── core.js
│ ├── manifest.json
│ ├── sizes.js
│ └── sketch-nibui.js
├── appcast.xml
└── docs
├── assets
├── 9patch-guide@2x.gif
├── 9patch@2x.gif
├── demo@2x.gif
├── notmuch.css
├── ogimage@2x.png
├── presets@2x.gif
├── repeat@2x.gif
└── slicer@2x.png
└── index.html
/README.md:
--------------------------------------------------------------------------------
1 | # Slicer
2 |
3 | 
4 |
5 | Your friendly Sketch slicing helper.
6 |
7 | 
8 |
9 | Read more at https://ozzik.github.io/Slicer.
10 |
11 | ## How to install
12 | 1. Download and open ```Slicer-master.zip```
13 | 2. Open ```Slicer.sketchplugin``` (Sketch will magically install the plugin)
14 |
15 | ## Wha's new
16 | * 0.4.4 (Jun 18)
17 | * Adds support for Sketch 45 auto update system so y'all stop re-downloading from here
18 | * 0.4.3 (Jan 15)
19 | * Fixes unwillingness of short-name layers to get exported to Android (thanks Ronit Klein!)
20 | * 0.4.2 (Dec 8)
21 | * Fixes trimming of transparent pixels when exporting layers (by superhero @girafic)
22 | * 0.4.1 (Nov 8)
23 | * Fixes Sketch 41 bugs (slices not being exported at all)
24 | * 0.4.0 (Oct 9)
25 | * Now exporting layers using exportables for all you wild exporters (for visually ignoring everything below those layers. Slices are still being exported using, well, slices)
26 | * Adds "xxxhdpi" (4x) export size for Android
27 | * 0.3.1 (Sep 26)
28 | * Hello world (that's a funny version number but yep)
29 |
30 | ## Notes
31 | * Tested on Sketch 41
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/MyNibUI.nib/designable.nib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
155 |
163 |
171 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
196 |
204 |
212 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/MyNibUI.nib/keyedobjects.nib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/MyNibUI.nib/keyedobjects.nib
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/icon-error@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/icon-error@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/icon@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-bottom-padding@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-bottom-padding@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-bottom@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-bottom@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-left-padding@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-left-padding@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-left@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-left@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-right-padding@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-right-padding@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-right@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-right@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-structure@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-structure@2x.gif
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-top-padding@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-top-padding@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-top@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/Slicer.sketchplugin/Contents/Resources/UIBundle/Contents/Resources/patch-error-top@2x.png
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Sketch/commands.js:
--------------------------------------------------------------------------------
1 | @import "core.js";
2 |
3 | function exportToFolder(context) {
4 | SL.Slicer.exportToFolder(context);
5 | }
6 |
7 | function exportToFolderAs(context) {
8 | SL.Slicer.exportToFolder(context, true);
9 | }
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Sketch/core.js:
--------------------------------------------------------------------------------
1 | @import "sketch-nibui.js";
2 | @import "sizes.js";
3 |
4 | var SL = {}; // Namespace
5 |
6 | /* === Core Slicer === */
7 | SL.Slicer = {
8 | documentMetadata: null,
9 |
10 | exportToFolder: function(context, isRequestNewConfig) {
11 | if (!context.selection.count()) { return; }
12 |
13 | SL.Slicer.documentMetadata = context.document.mutableUIMetadata();
14 |
15 | var exportConfig = SL.ExportConfig.get(context, isRequestNewConfig),
16 | isSuccess;
17 |
18 | if (!exportConfig) { return; }
19 |
20 | isSuccess = SL.Slicer._export(context, exportConfig);
21 |
22 | if (!isSuccess) { return; }
23 |
24 | if (exportConfig.isOpenFolderPostExport) {
25 | url = NSURL.URLWithString("file://" + exportConfig.directory.replace(/ /g, "%20"));
26 | NSWorkspace.sharedWorkspace().openURL(url);
27 | } else {
28 | context.document.showMessage("All done!");
29 | }
30 | },
31 |
32 | _export: function(context, config) {
33 | var selection = context.selection,
34 | doc = context.document,
35 | platforms = { "android": "Android", "ios": "iOS" }, // Fake keys
36 | isSuccess = true,
37 | previousShouldFixArtboardBackground,
38 | previousShouldFixSliceBackground,
39 | isStop;
40 |
41 | // Each layer
42 | for (var s = 0; s < selection.count() && !isStop; s++) {
43 | // Checking for possible background color annoyances
44 | if (selection[s].class() == MSArtboardGroup && selection[s].class && (!selection[s].includeBackgroundColorInExport() || !selection[s].hasBackgroundColor())) {
45 | previousShouldFixArtboardBackground = SL.Slicer._tryToFixArtboardBackground(context, selection[s], config, previousShouldFixArtboardBackground);
46 | isStop = !previousShouldFixArtboardBackground ? true : false;
47 | } else if (selection[s].class() != MSArtboardGroup && doc.currentPage().currentArtboard() && doc.currentPage().currentArtboard().includeBackgroundColorInExport() && doc.currentPage().currentArtboard().hasBackgroundColor()) {
48 | previousShouldFixSliceBackground = SL.Slicer._tryToFixSliceBackground(context, selection[s], config, previousShouldFixSliceBackground);
49 | isStop = !previousShouldFixSliceBackground ? true : false;
50 | }
51 | if (isStop) { continue; }
52 |
53 | // Each platform
54 | for (var platform in platforms) {
55 | if (!config[platform].length) { continue; }
56 |
57 | config.nestedFolder = (config.android.length && config.ios.length) ? platforms[platform] + "/" : "";
58 |
59 | // Possible 9 patch layer
60 | if (platform == "android" && selection[s].name().indexOf(".9") == selection[s].name().length() - 2 && selection[s].name().length >= 3) {
61 | isSuccess &= SL.NinePatch.try(selection[s], context, config);
62 | } else if (selection[s].class() == "MSSliceLayer") { // Slice
63 | SL.Slicer._exportSlice(selection[s], platform, config, context);
64 | } else { // Layer/group
65 | SL.Slicer._exportLayer(selection[s], platform, config, context);
66 | }
67 | }
68 | }
69 | isSuccess &= !isStop;
70 |
71 | return isSuccess;
72 | },
73 |
74 | _exportSlice: function(selection, platform, config, context) {
75 | var ancestry = MSImmutableLayerAncestry.ancestryWithMSLayer(selection),
76 | sizeData,
77 | exportFormat,
78 | slice,
79 | fileName;
80 |
81 | for (var i in config[platform]) {
82 | sizeData = config[platform][i];
83 | sizeData = _SIZES[platform][sizeData];
84 | exportFormat = MSExportFormat.formatWithScale_name_fileFormat(sizeData.size, "", "png");
85 |
86 | slice = MSExportRequest.exportRequestsFromLayerAncestry_exportFormats(ancestry, [ exportFormat ])[0];
87 | SL.Slicer._saveSliceToFile(slice, selection, platform, sizeData, config, context);
88 | }
89 | },
90 |
91 | _exportLayer: function(selection, platform, config, context) {
92 | var slices,
93 | sizeData,
94 | exportOption,
95 | fileName;
96 |
97 | var rect = selection.absoluteRect().rect();
98 |
99 | for (var i in config[platform]) {
100 | sizeData = config[platform][i];
101 | sizeData = _SIZES[platform][sizeData];
102 |
103 | selection.exportOptions().removeAllExportFormats();
104 | exportOption = selection.exportOptions().addExportFormat();
105 | exportOption.setName("");
106 | exportOption.setScale(sizeData.size);
107 |
108 | slices = MSExportRequest.exportRequestsFromExportableLayer(selection);
109 | slices[0].rect = rect;
110 |
111 | SL.Slicer._saveSliceToFile(slices[0], selection, platform, sizeData, config, context);
112 | }
113 |
114 | selection.exportOptions().removeAllExportFormats();
115 | },
116 |
117 | _saveSliceToFile: function(slice, selection, platform, sizeData, config, context) {
118 | var fileName;
119 |
120 | if (platform != "android") {
121 | fileName = (config.nestedFolder || "") + selection.name() + sizeData.name + ".png";
122 | } else {
123 | fileName = (config.nestedFolder || "") + "drawable-" + sizeData.name + "/" + selection.name() + ".png";
124 | }
125 |
126 | context.document.saveArtboardOrSlice_toFile(slice, (config.directory + fileName));
127 | },
128 |
129 | _tryToFixArtboardBackground: function(context, artboard, config, previousShouldFix) {
130 | var shouldFix;
131 |
132 | if (config.isIgnoreArtboardBackground) { return 2; }
133 |
134 | if (!previousShouldFix) {
135 | shouldFix = SL.UI.showError(context, {
136 | title: "Did you forget to set a background color to some artboards?",
137 | message: "If you continue, the exported artboards will have a transparent color.",
138 | confirmCaption: "Fix and export",
139 | alternativeCaption: "Export anyway"
140 | });
141 | } else {
142 | shouldFix = previousShouldFix;
143 | }
144 |
145 | if (shouldFix == 1) {
146 | artboard.setHasBackgroundColor(true);
147 | artboard.setIncludeBackgroundColorInExport(true);
148 | } else if (shouldFix == 2) {
149 | SL.ExportConfig.setIgnoreArtboardBackground(true);
150 | }
151 |
152 | return shouldFix;
153 | },
154 |
155 | _tryToFixSliceBackground: function(context, slice, config, previousShouldFix) {
156 | var shouldFix;
157 |
158 | if (config.isIgnoreSliceBackground) { return 2; }
159 |
160 | if (!previousShouldFix) {
161 | shouldFix = SL.UI.showError(context, {
162 | title: "Heads up: Your slices will have a background color",
163 | message: "If you continue, your slices will have the background color of their artboard instead of being transparent.",
164 | confirmCaption: "Fix and export",
165 | alternativeCaption: "Export anyway"
166 | });
167 | } else {
168 | shouldFix = previousShouldFix;
169 | }
170 |
171 | if (shouldFix == 1) {
172 | context.document.currentPage().currentArtboard().setIncludeBackgroundColorInExport(false);
173 | } else if (shouldFix == 2) {
174 | SL.ExportConfig.setIgnoreSliceBackground(true);
175 | }
176 |
177 | return shouldFix;
178 | }
179 | };
180 |
181 | /* === Config === */
182 | SL.ExportConfig = {
183 | KEY: "Slicer.exportConfig",
184 |
185 | get: function(context, isRequestNewConfig) {
186 | var config = SL.ExportConfig.getSaved(SL.Slicer.documentMetadata);
187 |
188 | if (!config || isRequestNewConfig) {
189 | config = SL.ExportConfig.getNew(context, config);
190 | }
191 |
192 | return config ? SL.ExportConfig._parse(config) : null;
193 | },
194 |
195 | getSaved: function() {
196 | var configData = SL.Slicer.documentMetadata[SL.ExportConfig.KEY] || "";
197 |
198 | try {
199 | configData = JSON.parse(configData);
200 | } catch(e) {
201 | configData = null;
202 | }
203 |
204 | return configData;
205 | },
206 |
207 | getNew: function(context, currentConfig) {
208 | var config,
209 | exportDirectory;
210 |
211 | // Getting preset/sizes
212 | config = SL.ExportConfig._requestPreset(context);
213 | if (!config) { return; }
214 |
215 | // Getting folder export
216 | exportDirectory = SL.UI.requestDirectory(context, currentConfig && currentConfig.directory);
217 | if (!exportDirectory) { return; }
218 |
219 | config.directory = exportDirectory + "/";
220 |
221 | SL.ExportConfig._save(config);
222 |
223 | return config;
224 | },
225 |
226 | _parse: function(config) {
227 | var androidSizes = [],
228 | iosSizes = [];
229 |
230 | if (config.android || config.ios) {
231 | androidSizes = config.android;
232 | iosSizes = config.ios;
233 | } else {
234 | switch (config.preset) {
235 | case 0:
236 | iosSizes = [ 0, 1, 2 ];
237 | break;
238 | case 1:
239 | iosSizes = [ 0, 1 ];
240 | break;
241 | case 2:
242 | androidSizes = [ 0, 1, 2, 3, 4 ];
243 | break;
244 | case 3:
245 | androidSizes = [ 1, 3 ];
246 | break;
247 | }
248 | }
249 |
250 | return {
251 | directory: config.directory,
252 | android: androidSizes,
253 | ios: iosSizes,
254 | isOpenFolderPostExport: config.isOpenFolderPostExport,
255 | isIgnoreArtboardBackground: config.isIgnoreArtboardBackground,
256 | isIgnoreSliceBackground: config.isIgnoreSliceBackground
257 | };
258 | },
259 |
260 | _requestPreset: function(context) {
261 | var alertData = SL.UI.requestConfig(context),
262 | nibui = alertData.nibui,
263 | selected = [],
264 | i = 0,
265 | config = {};
266 |
267 | if (!alertData.isConfirm) { return; }
268 |
269 | // Detecting config
270 | if (nibui.tabView.selectedTabViewItem().label() == "Presets") {
271 | while (i < 4 && !selected.length) {
272 | nibui["radioPreset" + i].state() && selected.push(i);
273 | i++;
274 | }
275 |
276 | config.preset = selected[0];
277 | } else {
278 | config.android = [];
279 | config.ios = [];
280 |
281 | for (var i in _SIZES.android) {
282 | nibui["checkAndroid" + i].state() && config.android.push(parseInt(i, 10));
283 | }
284 | for (var i in _SIZES.ios) {
285 | nibui["checkIos" + i].state() && config.ios.push(parseInt(i, 10));
286 | }
287 | }
288 |
289 | config.isOpenFolderPostExport = nibui.checkOpenFolderPostExport.state();
290 |
291 | nibui.destroy();
292 |
293 | return config;
294 | },
295 |
296 | _save: function(config) {
297 | SL.Slicer.documentMetadata[SL.ExportConfig.KEY] = JSON.stringify(config);
298 | },
299 |
300 | setIgnoreArtboardBackground: function(value) {
301 | var config = SL.ExportConfig.getSaved();
302 | config.isIgnoreArtboardBackground = value;
303 | SL.ExportConfig._save(config);
304 | },
305 |
306 | setIgnoreSliceBackground: function(value) {
307 | var config = SL.ExportConfig.getSaved();
308 | config.isIgnoreSliceBackground = value;
309 | SL.ExportConfig._save(config);
310 | }
311 | };
312 |
313 | /* === UI === */
314 | SL.UI = {
315 | requestConfig: function(context) {
316 | var alert = NSAlert.alloc().init();
317 |
318 | alert.setMessageText("Let's export!");
319 | alert.setInformativeText("Select a size preset or go make things complicated...");
320 | alert.addButtonWithTitle("Export");
321 | alert.addButtonWithTitle("Cancel");
322 | alert.setIcon(NSImage.alloc().initWithContentsOfFile(context.plugin.urlForResourceNamed("UIBundle/Contents/Resources/icon@2x.png").path()));
323 |
324 | var nibui = new NibUI(context, "UIBundle", "MyNibUI",
325 | [
326 | "tabView",
327 | "radioPreset0", "radioPreset1", "radioPreset2", "radioPreset3",
328 | "checkIos0", "checkIos1", "checkIos2",
329 | "checkAndroid0", "checkAndroid1", "checkAndroid2", "checkAndroid3", "checkAndroid4",
330 | "checkOpenFolderPostExport"
331 | ]
332 | );
333 |
334 | alert.setAccessoryView(nibui.view);
335 |
336 | // Updating state to saved config
337 | nibui.tabView.selectTabViewItemAtIndex(1);
338 | nibui.tabView.selectTabViewItemAtIndex(0);
339 |
340 | nibui.radioPreset0.becomeFirstResponder();
341 |
342 | var alertAction = alert.runModal();
343 |
344 | return {
345 | nibui: nibui,
346 | isConfirm: alertAction == NSAlertFirstButtonReturn
347 | };
348 | },
349 |
350 | requestDirectory: function(context, latestPath) {
351 | var panel = NSOpenPanel.openPanel(),
352 | defaultPath,
353 | path;
354 |
355 | if (context.document.fileURL() && !latestPath) {
356 | defaultPath = context.document.fileURL().URLByDeletingLastPathComponent();
357 | } else {
358 | defaultPath = NSURL.URLWithString(latestPath || "~/Desktop");
359 | }
360 |
361 | panel.setDirectoryURL(defaultPath);
362 | panel.setCanChooseDirectories(true);
363 | panel.setAllowsMultipleSelection(true);
364 | panel.setCanCreateDirectories(true);
365 | panel.setMessage("Select a directory to export to");
366 |
367 | if (panel.runModal() == NSOKButton) {
368 | path = panel.URL().path();
369 | }
370 |
371 | return path;
372 | },
373 |
374 | showError: function(context, options) {
375 | var alert = NSAlert.alloc().init();
376 |
377 | alert.setMessageText(options.title);
378 | alert.setInformativeText(options.message);
379 |
380 | options.confirmCaption && alert.addButtonWithTitle(options.confirmCaption);
381 | options.alternativeCaption && alert.addButtonWithTitle(options.alternativeCaption);
382 | alert.addButtonWithTitle(options.confirmCaption ? "Cancel" : "Gotcha");
383 |
384 | if (options.image) {
385 | var imageView = NSImageView.alloc().initWithFrame(NSMakeRect(0, 0, options.imageWidth, options.imageHeight));
386 | imageView.setImageScaling(NSScaleToFit);
387 | imageView.setImage(NSImage.alloc().initWithContentsOfFile(context.plugin.urlForResourceNamed("UIBundle/Contents/Resources/" + options.image).path()));
388 | alert.setAccessoryView(imageView);
389 | }
390 |
391 | alert.setIcon(NSImage.alloc().initWithContentsOfFile(context.plugin.urlForResourceNamed("UIBundle/Contents/Resources/icon-error@2x.png").path()));
392 |
393 | var alertAction = alert.runModal();
394 |
395 | if (alertAction == NSAlertFirstButtonReturn) {
396 | return 1
397 | } else {
398 | return (options.alternativeCaption && alertAction == NSAlertSecondButtonReturn) ? 2 : 0;
399 | }
400 | }
401 | };
402 |
403 | /* === 9-Patch === */
404 | SL.NinePatch = {
405 | try: function(target, context, config) {
406 | var layers = target.layers(),
407 | inferData = SL.NinePatch._infer(layers),
408 | size,
409 | fileName,
410 | currentPage,
411 | tempPage;
412 |
413 | if (!inferData.didFind) {
414 | SL.UI.showError(context, {
415 | title: "😱 Can't export \"" + layers[0].parentGroup().name() + "\"",
416 | message: "Exporting 9-patch works only when the group holds 2 groups: one holding 4 \"patch lines\" and another holding the actual slice content (their names don't really matter).",
417 | image: "patch-error-structure@2x.gif",
418 | imageWidth: 270,
419 | imageHeight: 150
420 | });
421 | return;
422 | }
423 | // Validating patch sizes for 1.5x
424 | if (config.android.indexOf(1) != -1 && !SL.NinePatch._validate(context, layers, inferData)) { return; } // Bad patch
425 |
426 | // Dummy holding page
427 | currentPage = context.document.currentPage();
428 | tempPage = context.document.addBlankPage();
429 |
430 | for (var i in config.android) {
431 | size = config.android[i];
432 | fileName = (config.nestedFolder || "") + "drawable-" + _SIZES.android[size].name + "/" + target.name() + ".png";
433 |
434 | SL.NinePatch._create(target, inferData.iSlice, inferData.iPatch, tempPage, context, _SIZES.android[size].size, fileName, config.directory);
435 | }
436 |
437 | context.document.removePage(tempPage);
438 | context.document.setCurrentPage(currentPage);
439 |
440 | return true;
441 | },
442 |
443 | _infer: function(layers) {
444 | var BLACK = MSColor.blackColor();
445 | var i = 0,
446 | patch,
447 | slice,
448 | iPatch = 0,
449 | iSlice = 0,
450 | currentLayer,
451 | subLayers,
452 | didFind;
453 |
454 | // Skipping check if layer count is bad
455 | if (layers.count() != 2) {
456 | i = 5;
457 | }
458 |
459 | // Inferring 9patch + slice
460 | while ((!patch && !slice) && i < 2) {
461 | currentLayer = layers.objectAtIndex(i);
462 | blackCount = 0;
463 |
464 | // 9patch
465 | if (currentLayer.class() == "MSLayerGroup") {
466 | subLayers = currentLayer.layers()
467 | for (var s = 0; s < subLayers.count(); s++) {
468 | if (subLayers.objectAtIndex(s).class() == MSSliceLayer) { continue; }
469 | blackCount += (subLayers.objectAtIndex(s).style().fills().objectAtIndex(0).color().isEqual(BLACK)) ? 1 : 0;
470 | }
471 |
472 | if (blackCount == 4) {
473 | patch = currentLayer;
474 | slice = layers.objectAtIndex(iSlice);
475 | iPatch = i;
476 | iSlice = i ? 0 : 1;
477 | didFind = true;
478 | }
479 | }
480 |
481 | i++;
482 | }
483 |
484 | return {
485 | iPatch: iPatch,
486 | iSlice: iSlice,
487 | didFind: didFind
488 | };
489 | },
490 |
491 | _validate: function(context, layers, data) {
492 | var isValid = true,
493 | patches = layers[data.iPatch].layers(),
494 | patchName,
495 | isHorizontalPatch,
496 | error,
497 | frame,
498 | i = 0,
499 | sliceName = layers[data.iSlice].parentGroup().name(),
500 | shouldFix;
501 |
502 | // Slice size
503 | frame = layers[data.iSlice].frame();
504 | isValid = (frame.width() % 2 == 0 && frame.height() % 2 == 0);
505 |
506 | if (!isValid) {
507 | SL.UI.showError(context, {
508 | title: "😱 Can't export \"" + sliceName + "\" at 1.5x",
509 | message: "The width and height of your slice's content must be even (current size: " + frame.width() + "x" + frame.height() + "). Try stretching your slice or just adding an extra space pixel."
510 | });
511 | }
512 |
513 | // Slice sizes
514 | while (i < patches.count() && isValid) {
515 | frame = patches[i].frame();
516 | patchName = SL.NinePatch._detectPatch(frame, layers[data.iSlice]);
517 | isHorizontalPatch = (patchName == "top" || patchName == "bottom");
518 |
519 | // Width/height
520 | isValid = isHorizontalPatch ? (frame.width() % 2 == 0) : (frame.height() % 2 == 0);
521 | if (!isValid) {
522 | error = "Your " + patchName + " patch's " + (isHorizontalPatch ? "width" : "height") + " should be even ";
523 | error += "(current is " + (isHorizontalPatch ? frame.width() : frame.height()) + "px).";
524 |
525 | shouldFix = SL.UI.showError(context, {
526 | title: "😱 Can't export \"" + sliceName + "\" at 1.5x",
527 | message: error,
528 | confirmCaption: "Fix and continue",
529 | image: "patch-error-" + patchName + "@2x.png",
530 | imageWidth: 134,
531 | imageHeight: 98
532 | });
533 |
534 | if (shouldFix) {
535 | SL.NinePatch._fixSize(patches[i], isHorizontalPatch);
536 | isValid = true;
537 | } else {
538 | i = patches.count(); // Stopping
539 | }
540 | } else { // Verifying padding
541 | isValid = isHorizontalPatch ? ((frame.x() - layers[data.iSlice].frame().x()) % 2 == 0) : ((frame.y() - layers[data.iSlice].frame().y()) % 2 == 0);
542 |
543 | if (!isValid) {
544 | error = "Your " + patchName + " patch's padding should be even ";
545 | error += "(current is " + (isHorizontalPatch ? (frame.x() - layers[data.iSlice].frame().x()) : (frame.y() - layers[data.iSlice].frame().y())) + "px).";
546 | shouldFix = SL.UI.showError(context, {
547 | title: "😱 Can't export \"" + sliceName + "\" at 1.5x",
548 | message: error,
549 | confirmCaption: "Fix and continue",
550 | image: "patch-error-" + patchName + "-padding@2x.png",
551 | imageWidth: 134,
552 | imageHeight: 98
553 | });
554 |
555 | if (shouldFix) {
556 | SL.NinePatch._fixPadding(patches[i], isHorizontalPatch);
557 | isValid = true;
558 | } else {
559 | i = patches.count(); // Stopping
560 | }
561 |
562 | } else {
563 | i++;
564 | }
565 | }
566 | }
567 |
568 | return isValid;
569 | },
570 |
571 | _fixPadding: function(patch, isHorizontalPatch) {
572 | if (isHorizontalPatch) {
573 | patch.frame().setX(patch.frame().x() - 1);
574 | } else {
575 | patch.frame().setY(patch.frame().y() - 1);
576 | }
577 | },
578 |
579 | _fixSize: function(patch, isHorizontalPatch) {
580 | if (isHorizontalPatch) {
581 | patch.frame().setWidth(patch.frame().width() > 1 ? (patch.frame().width() + 1) : 2);
582 | } else {
583 | patch.frame().setHeight(patch.frame().height() > 1 ? (patch.frame().height() + 1) : 2);
584 | }
585 | },
586 |
587 | _detectPatch: function(curPatch, slice) {
588 | var patch = "";
589 |
590 | if (curPatch.x() >= slice.frame().x() && curPatch.x() < slice.frame().x() + slice.frame().width()) {
591 | patch = (curPatch.y() < slice.frame().y()) ? "top" : "bottom";
592 | } else {
593 | patch = (curPatch.x() < slice.frame().x()) ? "left" : "right";
594 | }
595 |
596 | return patch;
597 | },
598 |
599 | _create: function(target, iSlice, iPatch, tempPage, context, factor, fileName, exportPath) {
600 | var ditto = target.duplicate(),
601 | dittoSliceOriginalX,
602 | dittoSliceOriginalY;
603 |
604 | ditto.parentGroup().removeLayer(ditto);
605 | tempPage.addLayers([ditto]);
606 |
607 | ditto.frame().setX(ditto.frame().x() + ditto.frame().width() + 10);
608 | ditto.frame().setY(ditto.frame().y());
609 |
610 | var dittoPatch = ditto.layers().objectAtIndex(iPatch),
611 | dittoSlice = ditto.layers().objectAtIndex(iSlice),
612 | dittoPatchLayers = dittoPatch.layers(),
613 | curPatch;
614 |
615 | for (var i = 0; i < dittoPatchLayers.count(); i++) {
616 | curPatch = dittoPatchLayers.objectAtIndex(i).frame();
617 |
618 | // Top/bottom
619 | if (curPatch.x() >= dittoSlice.frame().x() && curPatch.x() < dittoSlice.frame().x() + dittoSlice.frame().width()) {
620 | curPatch.setWidth(curPatch.width() * factor);
621 | curPatch.setX((curPatch.x() - 1) * factor + 1);
622 |
623 | if (curPatch.y() > dittoSlice.frame().y()) {
624 | curPatch.setY((curPatch.y() - 1) * factor + 1);
625 | }
626 | } else { // Left/right
627 | curPatch.setHeight(curPatch.height() * factor);
628 | curPatch.setY((curPatch.y() - 1) * factor + 1);
629 |
630 | if (curPatch.x() > dittoSlice.frame().x()) {
631 | curPatch.setX((curPatch.x() - 1) * factor + 1);
632 | }
633 | }
634 | }
635 |
636 | dittoSliceOriginalX = dittoSlice.frame().x();
637 | dittoSliceOriginalY = dittoSlice.frame().y();
638 |
639 | dittoSlice.multiplyBy(factor);
640 | dittoSlice.makeRectIntegral();
641 | dittoSlice.frame().setX(dittoSliceOriginalX);
642 | dittoSlice.frame().setY(dittoSliceOriginalY);
643 |
644 | dittoPatch.resizeToFitChildrenWithOption(0);
645 | ditto.resizeToFitChildrenWithOption(0);
646 |
647 | var ancestry = MSImmutableLayerAncestry.ancestryWithMSLayer(ditto),
648 | exportFormat = MSExportFormat.formatWithScale_name_fileFormat(1, "", "png");
649 | slice = MSExportRequest.exportRequestsFromLayerAncestry_exportFormats(ancestry, [ exportFormat ])[0];
650 |
651 | context.document.saveArtboardOrSlice_toFile(slice, exportPath + fileName);
652 |
653 | tempPage.removeLayer(ditto);
654 | }
655 | };
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "author" : "Oz Pinhas",
3 | "commands" : [
4 | {
5 | "script" : "commands.js",
6 | "handler" : "exportToFolder",
7 | "name" : "Export to Folder",
8 | "shortcut" : "cmd e",
9 | "identifier" : "exportToFolder"
10 | },
11 | {
12 | "script" : "commands.js",
13 | "handler" : "exportToFolderAs",
14 | "name" : "Export to Folder As...",
15 | "shortcut" : "cmd alt e",
16 | "identifier" : "exportToFolderAs"
17 | }
18 | ],
19 | "menu": {
20 | "isRoot": true,
21 | "items": [
22 | {
23 | "title": "Slicer",
24 | "items": [
25 | "exportToFolder",
26 | "exportToFolderAs",
27 | "do9patch"
28 | ]
29 | }
30 | ]
31 | },
32 | "identifier" : "co.ozzik.slicer",
33 | "version" : "0.4.4",
34 | "description" : "Exporting life",
35 | "authorEmail" : "hey@ozzik.co",
36 | "name" : "Slicer",
37 | "appcast": "https://raw.githubusercontent.com/ozzik/slicer/master/appcast.xml"
38 | }
39 |
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Sketch/sizes.js:
--------------------------------------------------------------------------------
1 | var _SIZES = {
2 | android: [
3 | {
4 | size: 1,
5 | name: "mdpi"
6 | },
7 | {
8 | size: 1.5,
9 | name: "hdpi"
10 | },
11 | {
12 | size: 2,
13 | name: "xhdpi"
14 | },
15 | {
16 | size: 3,
17 | name: "xxhdpi"
18 | },
19 | {
20 | size: 4,
21 | name: "xxxhdpi"
22 | }
23 | ],
24 | ios: [
25 | {
26 | size: 1,
27 | name: ""
28 | },
29 | {
30 | size: 2,
31 | name: "@2x"
32 | },
33 | {
34 | size: 3,
35 | name: "@3x"
36 | },
37 | ]
38 | };
--------------------------------------------------------------------------------
/Slicer.sketchplugin/Contents/Sketch/sketch-nibui.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | function NibUI(context, bundleResourceName, nibName, bindViewNames) {
18 | bindViewNames = bindViewNames || [];
19 |
20 | var bundlePath = context.plugin.urlForResourceNamed(bundleResourceName).path();
21 | this._bundle = NSBundle.bundleWithPath(bundlePath);
22 |
23 | var superclass = NSClassFromString('NSObject');
24 |
25 | // create a class name that doesn't exist yet. note that we can't reuse the same
26 | // definition lest Sketch will throw an MOJavaScriptException when binding the UI,
27 | // probably due to JavaScript context / plugin lifecycle incompatibility
28 |
29 | var tempClassName;
30 | while (true) {
31 | tempClassName = 'NibOwner' + _randomId();
32 | if (NSClassFromString(tempClassName) == null) {
33 | break;
34 | }
35 | }
36 |
37 | var me = this;
38 |
39 | // register the temporary class and set up instance methods that will be called for
40 | // each bound view
41 |
42 | this._cls = MOClassDescription.allocateDescriptionForClassWithName_superclass_(tempClassName, superclass);
43 |
44 | bindViewNames.forEach(function(bindViewName) {
45 | var setterName = 'set' + bindViewName.substring(0, 1).toUpperCase() + bindViewName.substring(1);
46 | me._cls.addInstanceMethodWithSelector_function_(
47 | NSSelectorFromString(setterName + ':'),
48 | function(arg) {
49 | me[bindViewName] = arg;
50 | });
51 | });
52 |
53 | this._cls.registerClass();
54 | this._nibOwner = NSClassFromString(tempClassName).alloc().init();
55 |
56 | // Radio button thingy
57 | var selector = NSSelectorFromString('radioButtonSelected:');
58 | this._cls.addInstanceMethodWithSelector_function_(
59 | selector,
60 | function() {});
61 |
62 | var tloPointer = MOPointer.alloc().initWithValue(null);
63 |
64 | if (this._bundle.loadNibNamed_owner_topLevelObjects_(nibName, this._nibOwner, tloPointer)) {
65 | var topLevelObjects = tloPointer.value();
66 | for (var i = 0; i < topLevelObjects.count(); i++) {
67 | var obj = topLevelObjects.objectAtIndex(i);
68 | if (obj.className().endsWith('View')) {
69 | this.view = obj;
70 | break;
71 | }
72 | }
73 | } else {
74 | throw new Error('Could not load nib');
75 | }
76 | }
77 |
78 | function _randomId() {
79 | return (1000000 * Math.random()).toFixed(0);
80 | }
81 |
82 | /**
83 | * Helper function for making click handlers (for use in NSButton.setAction).
84 | */
85 | NibUI.prototype.attachTargetAndAction = function(view, fn) {
86 | if (!this._clickActionNames) {
87 | this._clickActionNames = {};
88 | }
89 |
90 | var clickActionName;
91 | while (true) {
92 | clickActionName = 'zzzTempClickAction' + _randomId();
93 | if (!(clickActionName in this._clickActionNames)) {
94 | break;
95 | }
96 | }
97 |
98 | this._clickActionNames[clickActionName] = true;
99 |
100 | var selector = NSSelectorFromString(clickActionName + ':');
101 | this._cls.addInstanceMethodWithSelector_function_(
102 | selector,
103 | function() {
104 | fn();
105 | });
106 |
107 | view.setTarget(this._nibOwner);
108 | view.setAction(selector);
109 | };
110 |
111 | /**
112 | * Release all resources.
113 | */
114 | NibUI.prototype.destroy = function() {
115 | this._bundle.unload();
116 | };
--------------------------------------------------------------------------------
/appcast.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Slicer
5 | https://raw.githubusercontent.com/ozzik/slicer/master/appcast.xml
6 | Exporting life
7 | en
8 | -
9 | Version 0.4.4
10 |
11 |
13 | Minor update v0.4.4
14 |
15 | ]]>
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/assets/9patch-guide@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/9patch-guide@2x.gif
--------------------------------------------------------------------------------
/docs/assets/9patch@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/9patch@2x.gif
--------------------------------------------------------------------------------
/docs/assets/demo@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/demo@2x.gif
--------------------------------------------------------------------------------
/docs/assets/notmuch.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: 'Source Sans Pro', sans-serif;
5 | background: #fff;
6 | font-size: 15px;
7 | color: #515151;
8 | }
9 | p { margin: 0; }
10 | ol {
11 | margin: 5px 0;
12 | padding: 0 0 0 20px;
13 | list-style: disc;
14 | }
15 |
16 | /* Flex things */
17 | .box-h {
18 | display: flex;
19 | flex-direction: row;
20 | }
21 | .box-v {
22 | display: flex;
23 | flex-direction: column;
24 | }
25 | .box-main { flex: 0 1 auto; }
26 |
27 |
28 | .wrapper {
29 | max-width: 780px;
30 | margin: 0 auto;
31 | }
32 |
33 | .header {
34 | position: relative;
35 | background: #9E80D0;
36 | height: 230px;
37 | color: #fff;
38 |
39 | }
40 | .header-p { margin: 0; }
41 |
42 | /* Header */
43 | .header-wrapper {
44 | max-width: 380px;
45 | margin: 0 auto;
46 | padding-top: 130px;
47 | }
48 | .header-meta {
49 | margin: -50px 0 0 40px;
50 | }
51 | .slicer-image {
52 | width: 82px;
53 | height: 123px;
54 | background: url("slicer@2x.png") no-repeat;
55 | background-size: 82px;
56 | }
57 |
58 | h1 {
59 | margin: 0;
60 | font-weight: normal;;
61 | font-size: 30px;
62 | }
63 |
64 | .header-buttons { margin-top: 20px; }
65 | .button {
66 | display: inline-block;
67 | background: #fff;
68 | box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.10), 0px 4px 4px 0px rgba(0,0,0,0.10);
69 | border-radius: 6px;
70 | color: #494949;
71 | font-weight: bold;
72 | font-size: 15px;
73 | text-decoration: none;
74 | padding: 8px 20px;
75 | transition: all .4s cubic-bezier(.5, 1, 0, 1.1);
76 | transition-property: box-shadow, background, transform;
77 | }
78 | .button:hover {
79 | transform: translate3d(0,-1px,0);
80 | box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.10), 0px 5px 10px 0px rgba(0,0,0,0.10);
81 | }
82 | .button:active { background: #e8e8e8; }
83 |
84 | .button.secondary {
85 | margin-left: 6px;
86 | background: #3e3252;
87 | color: #fff;
88 | }
89 | .button.secondary:hover {
90 | transform: translate3d(0,-1px,0);
91 | background: #594875;
92 | box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.10), 0px 5px 10px 0px rgba(0,0,0,0.10);
93 | }
94 | .button.secondary:active { background: #2d243c; }
95 |
96 | /* Content */
97 | .content { margin-top: 80px; }
98 |
99 | .demo {
100 | margin: 0 auto 40px;
101 | border-radius: 4px;
102 | border: 1px solid #E5E5E5;
103 | }
104 | .column {
105 | width: 26%;
106 | text-align: center;
107 | }
108 | .column:not(:first-child) { margin-left: 8%; }
109 |
110 | h2 {
111 | margin: 0 auto 3px;
112 | font-wight: 700;
113 | font-size: 17px;
114 | color: #141414;
115 | }
116 |
117 | .separator {
118 | width: 100%;
119 | margin: 60px 0;
120 | height: 1px;
121 | background: #E5E5E5;
122 | }
123 |
124 | .extra .meta { margin-right: 20px; }
125 |
126 | /* Footer */
127 | .footer {
128 | margin-top: 60px;
129 | padding: 20px 0;
130 | text-align: center;
131 | color: rgba(0,0,0,.4);
132 | font-size: 14px;
133 | }
134 | .footer a {
135 | color: #64577A;
136 | opacity: .6;
137 | transition: opacity .4s cubic-bezier(.5, 1, 0, 1.1);
138 | }
139 | .footer a:hover { opacity: .8; }
--------------------------------------------------------------------------------
/docs/assets/ogimage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/ogimage@2x.png
--------------------------------------------------------------------------------
/docs/assets/presets@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/presets@2x.gif
--------------------------------------------------------------------------------
/docs/assets/repeat@2x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/repeat@2x.gif
--------------------------------------------------------------------------------
/docs/assets/slicer@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozzik/Slicer/6dcee5fba3565bb47faaa6ae945b3d08ec3a0fdb/docs/assets/slicer@2x.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Slicer - your friendly Sketch slicing helper
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
28 |
29 |
30 |

31 |
32 |
33 |
34 |

35 |
Presets woot!
36 |
Select a preset (or customize one) and your slices are named and sorted by folders neatly
37 |
38 |
39 |

40 |
Repeat!
41 |
Setup your slices once and hit ⌘ + E endlessly to export again using the same settings
42 |
43 |
44 |

45 |
9-patch the world!
46 |
Draw 4 black rectangles (i.e. patches) once and watch them exported into all Android sizes
47 |
48 |
49 |
50 |
65 |
71 |
72 |
73 |
--------------------------------------------------------------------------------