├── .gitignore
├── Flatten.sketchplugin
└── Contents
│ ├── Resources
│ ├── Icons
│ │ ├── about.png
│ │ ├── addTagToSelection.png
│ │ ├── createPreview.png
│ │ ├── donation.png
│ │ ├── feedbackByMail.png
│ │ ├── feedbackByTwitter.png
│ │ ├── flatten.png
│ │ ├── flattenAll.png
│ │ ├── manual.png
│ │ ├── restoreSelection.png
│ │ ├── settings.png
│ │ ├── switchToImageMode.png
│ │ ├── switchToLayerMode.png
│ │ └── toggleSelection.png
│ └── logo.png
│ └── Sketch
│ ├── manifest.json
│ └── script.js
├── README.md
└── appcast.xml
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/about.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/addTagToSelection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/addTagToSelection.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/createPreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/createPreview.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/donation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/donation.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/feedbackByMail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/feedbackByMail.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/feedbackByTwitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/feedbackByTwitter.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/flatten.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/flatten.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/flattenAll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/flattenAll.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/manual.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/manual.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/restoreSelection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/restoreSelection.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/settings.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/switchToImageMode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/switchToImageMode.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/switchToLayerMode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/switchToLayerMode.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/Icons/toggleSelection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/Icons/toggleSelection.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/476ed2feef5535406fd1d5e4714abd6dc2b73365/Flatten.sketchplugin/Contents/Resources/logo.png
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Flatten",
3 | "description": "Flatten and mirror layers without destructing and update them like a boss.",
4 | "icon": "logo.png",
5 | "author": "Emin Inanc Unlu",
6 | "homepage": "https://github.com/einancunlu/Flatten-Plugin-for-Sketch",
7 | "version": "2.1.0",
8 | "appcast": "https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/master/appcast.xml",
9 | "compatibleVersion": 50,
10 | "identifier": "com.einancunlu.sketch-plugins.flatten",
11 | "commands": [
12 | {
13 | "name": "📷 Flatten",
14 | "shortcut": "shift control f",
15 | "description" : "Flatten the selection or the current artboard",
16 | "icon" : "Icons/flatten.png",
17 | "identifier": "flatten",
18 | "handler": "flatten",
19 | "script": "script.js"
20 | },
21 | {
22 | "name": "🎥 Flatten All",
23 | "description" : "Update all the flattened layers",
24 | "icon" : "Icons/flattenAll.png",
25 | "identifier": "flattenAll",
26 | "handler": "flattenAll",
27 | "script": "script.js"
28 | },
29 | {
30 | "name": "🔍 Create Preview Layer",
31 | "description" : "For the selection (artboard or any layer)",
32 | "icon" : "Icons/createPreview.png",
33 | "identifier": "createPreview",
34 | "handler": "createPreview",
35 | "script": "script.js"
36 | },
37 | {
38 | "name": "♻️ Restore",
39 | "description" : "Unflatten, rename the image layer or delete the preview",
40 | "icon" : "Icons/restoreSelection.png",
41 | "identifier": "restoreSelection",
42 | "handler": "restoreSelection",
43 | "script": "script.js"
44 | },
45 | {
46 | "name": "Selection Changed",
47 | "handlers": {
48 | "actions": {
49 | "SelectionChanged.finish": "onSelectionChanged"
50 | }
51 | },
52 | "script": "script.js"
53 | },
54 | {
55 | "name": "🔄 Toggle",
56 | "shortcut": "shift control t",
57 | "description" : "Toggle between the image layer and the actual layer",
58 | "icon" : "Icons/toggleSelection.png",
59 | "identifier": "toggleSelection",
60 | "handler": "toggleSelection",
61 | "script": "script.js"
62 | },
63 | {
64 | "name": "🏞 Image Mode",
65 | "description" : "Switch to image mode in the selection or all",
66 | "icon" : "Icons/switchToImageMode.png",
67 | "identifier": "switchToImageMode",
68 | "handler": "switchToImageMode",
69 | "script": "script.js"
70 | },
71 | {
72 | "name": "💎 Layer Mode",
73 | "description" : "Switch to layer mode in the selection or all",
74 | "icon" : "Icons/switchToLayerMode.png",
75 | "identifier": "switchToLayerMode",
76 | "handler": "switchToLayerMode",
77 | "script": "script.js"
78 | },
79 | {
80 | "name": "#flatten",
81 | "description" : "Add #flatten tag to the selection",
82 | "icon" : "Icons/addTagToSelection.png",
83 | "identifier": "addFlattenTagToSelection",
84 | "handler": "addFlattenTagToSelection",
85 | "script": "script.js"
86 | },
87 | {
88 | "name": "#exclude",
89 | "description" : "Add #exclude tag to the selection",
90 | "icon" : "Icons/addTagToSelection.png",
91 | "identifier": "addExcludeTagToSelection",
92 | "handler": "addExcludeTagToSelection",
93 | "script": "script.js"
94 | },
95 | {
96 | "name": "#s0.02 (Example Customized Scale)",
97 | "description" : "Add customized scale tag to the selection (other examples: #s3, #s2)",
98 | "icon" : "Icons/addTagToSelection.png",
99 | "identifier": "addScaleTagToSelection",
100 | "handler": "addScaleTagToSelection",
101 | "script": "script.js"
102 | },
103 | {
104 | "name": "#no-auto",
105 | "description" : "Add #no-auto tag to the selection",
106 | "icon" : "Icons/addTagToSelection.png",
107 | "identifier": "addDisableAutoTagToSelection",
108 | "handler": "addDisableAutoTagToSelection",
109 | "script": "script.js"
110 | },
111 | {
112 | "name": "#stay-hidden",
113 | "description" : "Add #stay-hidden tag to the selection",
114 | "icon" : "Icons/addTagToSelection.png",
115 | "identifier": "addStayHiddenTagToSelection",
116 | "handler": "addStayHiddenTagToSelection",
117 | "script": "script.js"
118 | },
119 | {
120 | "name": "⚙️ Settings...",
121 | "description" : "Open the settings panel",
122 | "icon" : "Icons/settings.png",
123 | "identifier": "settings",
124 | "handler": "settings",
125 | "script": "script.js"
126 | },
127 | {
128 | "name": "📖 Manual",
129 | "description" : "Learn all the details and how to use the plugin",
130 | "icon" : "Icons/manual.png",
131 | "identifier": "manual",
132 | "handler": "manual",
133 | "script": "script.js"
134 | },
135 | {
136 | "name": "Mail",
137 | "description" : "Contact via email",
138 | "icon" : "Icons/feedbackByMail.png",
139 | "identifier": "feedbackByMail",
140 | "handler": "feedbackByMail",
141 | "script": "script.js"
142 | },
143 | {
144 | "name": "Twitter",
145 | "description" : "Contact via Twitter",
146 | "icon" : "Icons/feedbackByTwitter.png",
147 | "identifier": "feedbackByTwitter",
148 | "handler": "feedbackByTwitter",
149 | "script": "script.js"
150 | },
151 | {
152 | "name": "☕️ Buy me a Cup of Coffee",
153 | "description" : "Support the development and encoure me for more updates!",
154 | "icon" : "Icons/donation.png",
155 | "identifier": "donation",
156 | "handler": "donation",
157 | "script": "script.js"
158 | },
159 | {
160 | "name": "👋 About",
161 | "description" : "Open my website: www.emin.space",
162 | "icon" : "Icons/about.png",
163 | "identifier": "about",
164 | "handler": "about",
165 | "script": "script.js"
166 | }
167 | ],
168 | "menu": {
169 | "title": "Flatten 📷",
170 | "items": [
171 | "flatten",
172 | "flattenAll",
173 | "createPreview",
174 | "restoreSelection",
175 | {
176 | "title": "🏷 Add Tag",
177 | "items": [
178 | "addFlattenTagToSelection",
179 | "addExcludeTagToSelection",
180 | "addScaleTagToSelection",
181 | "addStayHiddenTagToSelection",
182 | "addDisableAutoTagToSelection"
183 | ]
184 | },
185 | "-",
186 | "toggleSelection",
187 | "switchToLayerMode",
188 | "switchToImageMode",
189 | "-",
190 | "settings",
191 | "manual",
192 | {
193 | "title": "💬 Feedback / Contact",
194 | "items": [
195 | "feedbackByMail",
196 | "feedbackByTwitter"
197 | ]
198 | },
199 | "-",
200 | "donation",
201 | "about"
202 | ]
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/Flatten.sketchplugin/Contents/Sketch/script.js:
--------------------------------------------------------------------------------
1 |
2 | //------------------------------------------------------------------------------
3 | // Created by Emin Inanc Unlu 🐼 🏠 emin.space
4 | //------------------------------------------------------------------------------
5 |
6 | /*
7 | The MIT License (MIT)
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the 'Software'), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 | */
27 |
28 | //------------------------------------------------------------------------------
29 | // GLOBAL
30 | //------------------------------------------------------------------------------
31 |
32 | const sketch = require('sketch/dom'),
33 | UI = require('sketch/ui'),
34 | Settings = require('sketch/settings'),
35 |
36 | // Keys
37 | kPluginDomain = 'com.einancunlu.sketch-plugins.flatten',
38 | kGroupKey = kPluginDomain + '.groupKey',
39 | kImageLayerKey = kPluginDomain + '.imageLayerKey',
40 | kArtboardOfImageLayerKey = kPluginDomain + '.artboardOfImageLayerKey',
41 | kTempBgLayerKey = kPluginDomain + '.tempBgLayerKey',
42 | kReferenceLayerOfPreviewLayerKey = kPluginDomain + '.layerOfPreviewLayerKey',
43 | kLayerToBeFlattened = kPluginDomain + '.layerToBeFlattened',
44 | kLayerVisibilityKey = kPluginDomain + '.layerVisibilityKey',
45 | kImageLayerNameKey = kPluginDomain + '.imageLayerNameKey',
46 | kSharedStyleNamePrefixKey = kPluginDomain + '.sharedStyleNamePrefixKey',
47 | kAutoFunctionsEnabledKey = kPluginDomain + '.autoFunctionsEnabledKey',
48 | kDefaultFlattenScaleKey = kPluginDomain + '.defaultFlattenScaleKey',
49 |
50 | // Constants
51 | flattenTag = '#flatten',
52 | excludeTag = '#exclude',
53 | scaleTagPrefix = '#s',
54 | disableAutoTag = '#no-auto',
55 | stayHiddenTag = '#stay-hidden',
56 | maxFlatteningScale = 10,
57 | minFlatteningScale = 0.05,
58 |
59 | // Texts
60 | noLayerFoundMessage = "⚠️ No layer found to be flattened. Use 'Flatten' command to create one.",
61 | emptySelectionMessage = '⚠️ Please select something!',
62 |
63 | // Utilitiess
64 | FillType = { Solid: 0, Gradient: 1, Pattern: 4, Noise: 5 },
65 | PatternFillType = { Tile: 0, Fill: 1, Stretch: 2, Fit: 3 }
66 |
67 | var document, savedContext, selection, developmentMode = false,
68 |
69 | // Settable constants
70 | imageLayerName = Settings.settingForKey(kImageLayerNameKey),
71 | imageLayerSharedStyleNamePrefix = Settings.settingForKey(kSharedStyleNamePrefixKey),
72 | autoFunctionsEnabled = Settings.settingForKey(kAutoFunctionsEnabledKey),
73 | defaultFlattenScale = Settings.settingForKey(kDefaultFlattenScaleKey)
74 |
75 | if (!imageLayerName) imageLayerName = 'Flattened Image'
76 | if (!imageLayerSharedStyleNamePrefix) imageLayerSharedStyleNamePrefix = 'Artboards / '
77 | if (!autoFunctionsEnabled) autoFunctionsEnabled = 1
78 | if (!defaultFlattenScale) defaultFlattenScale = 1
79 |
80 | //------------------------------------------------------------------------------
81 | // MENU COMMANDS
82 | //------------------------------------------------------------------------------
83 |
84 | ////////
85 | function initCommand(context) {
86 |
87 | document = sketch.getSelectedDocument()
88 | savedContext = context
89 | selection = document.selectedLayers
90 | }
91 |
92 | ////////
93 | function flatten(context) {
94 |
95 | initCommand(context)
96 | sendEvent(context, 'Command', 'Flatten', 'Started', selection.length)
97 | var firstSelection = selection
98 | var layersToBeFlattened
99 | var imageLayer
100 | if (selection.length === 0) {
101 | // Check if there is a last selected artboard
102 | const currentArtboard = context.document.currentPage().currentArtboard()
103 | if (!currentArtboard) {
104 | UI.message("⚠️ Couldn't detect the current artboard, please select an artboard.")
105 | }
106 | layersToBeFlattened = findLayersByTag_inContainer(flattenTag, currentArtboard)
107 | layersToBeFlattened = fromNativeArray(layersToBeFlattened)
108 | sendEvent(context, 'Command', 'Flatten', 'Current artboard')
109 | } else {
110 | // If the selection is an artboard
111 | if (selection.length === 1) {
112 | const layer = firstSelection.layers[0]
113 | if (isImageLayer(layer) && isArtboard(layer.parent)) {
114 | if (isImageLayerOfArtboard(layer, layer.parent) && hasTag(layer, flattenTag)) {
115 | selection = layer.parent
116 | sendEvent(context, 'Command', 'Flatten', 'Selected artboard')
117 | } else {
118 | return
119 | }
120 | }
121 | }
122 | // Find all sublayers of selection that contain flatten tag
123 | layersToBeFlattened = findAllSublayersWithFlattenTag(selection.layers)
124 | }
125 | // If there is no layer with flatten tag found, add flatten tag to the all of
126 | // the selected layers
127 | if (layersToBeFlattened.length === 0) {
128 | addTagToLayers(selection.layers, flattenTag)
129 | layersToBeFlattened = findAllSublayersWithFlattenTag(selection.layers)
130 | }
131 | // Flatten all layers
132 | imageLayer = flattenLayers(layersToBeFlattened)
133 | if (firstSelection.length === 1) {
134 | const layer = firstSelection.layers[0]
135 | if (isArtboard(layer)) {
136 | const artboard = layer
137 | if (!hasImageLayer(artboard)) {
138 | // Create imageLayer for the artboard and ask to create a shared style
139 | imageLayer = flattenLayers(firstSelection.layers)
140 | const placeholder = imageLayerSharedStyleNamePrefix + artboard.name
141 | const alert = COSAlertWindow.new()
142 | alert.setMessageText('Create Shared Layer Style')
143 | alert.setInformativeText('The artboard is flattened. Do you want to create a shared layer style for it?\n\nName of the shared layer style:')
144 | alert.addTextFieldWithValue(placeholder)
145 | alert.addButtonWithTitle('Create')
146 | alert.addButtonWithTitle('Not Now')
147 | const responseCode = alert.runModal()
148 | if (responseCode == 1000) {
149 | // Create shared style
150 | const textfield = alert.viewAtIndex(0)
151 | input = textfield.stringValue()
152 | layerStyle = MSSharedStyle.alloc().initWithName_firstInstance(input, imageLayer.sketchObject.style())
153 | context.document.documentData().layerStyles().addSharedObject(layerStyle)
154 | document.sketchObject.reloadInspector()
155 | sendEvent(context, 'Command', 'Flatten', 'Created new artboard style')
156 | }
157 | }
158 | }
159 | }
160 | if (imageLayer) {
161 | // TODO: This line might be needed later when the new API allows grouping from layers
162 | // imageLayer.parent.sketchObject.select_byExpandingSelection(true, false)
163 | } else {
164 | // TODO: Select all layers back after flattening
165 | }
166 | }
167 |
168 | ////////
169 | function flattenAll(context) {
170 |
171 | initCommand(context)
172 | const layersToBeFlattened = findLayersByTag_inContainer(flattenTag)
173 | sendEvent(context, 'Command', 'Flatten All', 'Started', layersToBeFlattened.length)
174 | if (layersToBeFlattened.length === 0) {
175 | UI.message(noLayerFoundMessage)
176 | } else {
177 | flattenLayers(fromNativeArray(layersToBeFlattened))
178 | UI.message("Flattening process is completed.")
179 | }
180 | }
181 |
182 | ////////
183 | function createPreview(context) {
184 |
185 | initCommand(context)
186 | sendEvent(context, 'Command', 'Generate Preview', 'Started', selection.length)
187 |
188 | if (selection.length === 0) {
189 | UI.message(emptySelectionMessage)
190 | return
191 | }
192 | for (const layer of selection.layers) {
193 | // Create image layer
194 | var imageLayer, parent
195 | if (isArtboard(layer)) {
196 | imageLayer = getImageLayer(layer)
197 | parent = imageLayer.parent
198 | } else {
199 | addTagToLayers([layer], flattenTag)
200 | imageLayer = getImageLayer(layer)
201 | parent = imageLayer.parent
202 | }
203 | // Flatten the layer with stay-hidden and scale tags
204 | addTagToLayers([imageLayer], stayHiddenTag)
205 | addTagToLayers([imageLayer], scaleTagPrefix + '4')
206 | flattenLayers([layer])
207 | // Create shared layer style
208 | const styleName = 'Temporary/Preview'
209 | const layerStyle = MSSharedStyle.alloc().initWithName_firstInstance(styleName, imageLayer.sketchObject.style())
210 | document.sketchObject.documentData().layerStyles().addSharedObject(layerStyle)
211 | // Create preview layer
212 | const duplicateImageLayer = imageLayer.duplicate()
213 | Settings.setLayerSettingForKey(duplicateImageLayer, kReferenceLayerOfPreviewLayerKey, parent.id)
214 | duplicateImageLayer.sketchObject.moveToLayer_beforeLayer(parent.parent.sketchObject, parent.sketchObject)
215 | duplicateImageLayer.moveForward()
216 | duplicateImageLayer.name = 'Preview'
217 | duplicateImageLayer.hidden = false
218 | duplicateImageLayer.frame = parent.frame
219 | duplicateImageLayer.frame.x = parent.frame.x + parent.frame.width + 1
220 | duplicateImageLayer.frame.y = parent.frame.y
221 | const zoomValue = document.sketchObject.zoomValue()
222 | duplicateImageLayer.frame.scale(1/zoomValue)
223 | selection.clear()
224 | parent.sketchObject.select_byExpandingSelection(true, false)
225 | }
226 | }
227 |
228 | ////////
229 | function restoreSelection(context) {
230 |
231 | initCommand(context)
232 | sendEvent(context, 'Command', 'Recover Selection', 'Started', selection.length)
233 |
234 | if (selection.length === 0) {
235 | UI.message(emptySelectionMessage)
236 | return
237 | }
238 | for (const layer of selection.layers) {
239 | recoverLayer(layer)
240 | }
241 | }
242 |
243 | ////////
244 | function toggleSelection(context) {
245 |
246 | initCommand(context)
247 | sendEvent(context, 'Command', 'Toggle Selection', 'Started', selection.length)
248 |
249 | if (selection.length === 0) {
250 | UI.message(emptySelectionMessage)
251 | } else {
252 | setImageModeForSelectionOrAll()
253 | }
254 | }
255 |
256 | ////////
257 | function switchToImageMode(context) {
258 |
259 | initCommand(context)
260 | sendEvent(context, 'Command', 'Switch to Image Mode', 'Started', selection.length)
261 |
262 | setImageModeForSelectionOrAll(true)
263 | if (selection.length === 0) {
264 | UI.message("Switched to image mode in all flattened groups. (Switch in specific groups by selecting groups.)")
265 | } else {
266 | UI.message("Switched to image mode in selected groups. (Switch in all groups by emptying your selection.)")
267 | }
268 | }
269 |
270 | ////////
271 | function switchToLayerMode(context) {
272 |
273 | initCommand(context)
274 | sendEvent(context, 'Command', 'Switch to Layer Mode', 'Started', selection.length)
275 |
276 | setImageModeForSelectionOrAll(false)
277 | if (selection.length === 0) {
278 | UI.message("Switched to layer mode in all flattened groups. (Switch in specific groups by selecting groups.)")
279 | } else {
280 | UI.message("Switched to layer mode in selected groups. (Switch in all groups by emptying your selection.)")
281 | }
282 | }
283 |
284 | ////////
285 | function addFlattenTagToSelection(context) {
286 |
287 | initCommand(context)
288 | sendEvent(context, 'Command', 'Add Flatten Tag', 'Started', selection.length)
289 |
290 | if (selection.length === 0) UI.message(emptySelectionMessage)
291 | else addTagToLayers(selection.layers, flattenTag)
292 | }
293 |
294 | ////////
295 | function addExcludeTagToSelection(context) {
296 |
297 | initCommand(context)
298 | sendEvent(context, 'Command', 'Add Exclude Tag', 'Started', selection.length)
299 |
300 | if (selection.length === 0) UI.message(emptySelectionMessage)
301 | else addTagToLayers(selection.layers, excludeTag)
302 | }
303 |
304 | ////////
305 | function addScaleTagToSelection(context) {
306 |
307 | initCommand(context)
308 | sendEvent(context, 'Command', 'Add Scale Tag', 'Started', selection.length)
309 |
310 | if (selection.length === 0) UI.message(emptySelectionMessage)
311 | else addTagToLayers(selection.layers, `${scaleTagPrefix}0.05`)
312 | }
313 |
314 | ////////
315 | function addDisableAutoTagToSelection(context) {
316 |
317 | initCommand(context)
318 | sendEvent(context, 'Command', 'Add Disable Auto Tag', 'Started', selection.length)
319 |
320 | if (selection.length === 0) UI.message(emptySelectionMessage)
321 | else addTagToLayers(selection.layers, disableAutoTag)
322 | }
323 |
324 | ////////
325 | function addStayHiddenTagToSelection(context) {
326 |
327 | initCommand(context)
328 | sendEvent(context, 'Command', 'Add Stay Hidden Tag', 'Started', selection.length)
329 |
330 | if (selection.length === 0) UI.message(emptySelectionMessage)
331 | else addTagToLayers(selection.layers, stayHiddenTag)
332 | }
333 |
334 | ////////
335 | function settings(context) {
336 |
337 | initCommand(context)
338 | sendEvent(context, 'Command', 'Settings', 'Started')
339 |
340 | const alert = COSAlertWindow.new()
341 | const path = context.plugin.urlForResourceNamed("logo.png").path()
342 | const icon = NSImage.alloc().initByReferencingFile(path)
343 | alert.setIcon(icon)
344 | alert.setMessageText("Settings")
345 |
346 | alert.addAccessoryView(createCheckbox("Enable auto flattening and toggling", autoFunctionsEnabled))
347 |
348 | alert.addTextLabelWithValue("Flattening scale / quality (e.g. 0.5, 1, 2, 3)")
349 | alert.addTextFieldWithValue(defaultFlattenScale)
350 |
351 | alert.addTextLabelWithValue("Image layer name")
352 | alert.addTextFieldWithValue(imageLayerName)
353 |
354 | alert.addTextLabelWithValue("Artboard shared style name prefix")
355 | alert.addTextFieldWithValue(imageLayerSharedStyleNamePrefix)
356 |
357 | alert.addButtonWithTitle('Save')
358 | alert.addButtonWithTitle('Cancel')
359 | alert.addButtonWithTitle('Reset')
360 |
361 | const responseCode = alert.runModal()
362 | if (responseCode == 1000) {
363 |
364 | // User clicked on save button
365 | autoFunctionsEnabled = String(alert.viewAtIndex(0).state())
366 | Settings.setSettingForKey(kAutoFunctionsEnabledKey, autoFunctionsEnabled)
367 |
368 | var scale = parseFloat(alert.viewAtIndex(2).stringValue().replace(',', '.')).toFixed(2)
369 | if (isNaN(scale)) {
370 | UI.message("⚠️ Failed to change 'Flattening scale'. Please enter a valid scale.")
371 | } else {
372 | Settings.setSettingForKey(kDefaultFlattenScaleKey, checkScale(scale))
373 | }
374 |
375 | imageLayerName = String(alert.viewAtIndex(4).stringValue())
376 | Settings.setSettingForKey(kImageLayerNameKey, imageLayerName)
377 |
378 | imageLayerSharedStyleNamePrefix = String(alert.viewAtIndex(6).stringValue())
379 | Settings.setSettingForKey(kSharedStyleNamePrefixKey, imageLayerSharedStyleNamePrefix)
380 |
381 | const value = `Auto: ${autoFunctionsEnabled}, Scale: ${scale}, Image Layer Name: ${imageLayerName}, Image Layer Shared Style Prefix: ${imageLayerSharedStyleNamePrefix}`
382 | sendEvent(context, 'Command', 'Settings', 'Save', value)
383 |
384 | } else if (responseCode == 1002) {
385 |
386 | // User clicked on reset button
387 | sendEvent(context, 'Command', 'Settings', 'Reset')
388 | autoFunctionsEnabled = '1'
389 | Settings.setSettingForKey(kAutoFunctionsEnabledKey, autoFunctionsEnabled)
390 |
391 | defaultFlattenScale = '1'
392 | Settings.setSettingForKey(kDefaultFlattenScaleKey, defaultFlattenScale)
393 |
394 | imageLayerName = 'Flattened Image'
395 | Settings.setSettingForKey(kImageLayerNameKey, imageLayerName)
396 |
397 | imageLayerSharedStyleNamePrefix = 'Artboards / '
398 | Settings.setSettingForKey(kSharedStyleNamePrefixKey, imageLayerSharedStyleNamePrefix)
399 | }
400 | }
401 |
402 | ////////
403 | function manual(context) {
404 |
405 | initCommand(context)
406 | sendEvent(context, 'Command', 'Manual', 'Started')
407 |
408 | const urlString = "https://medium.com/@einancunlu/flatten-2-0-sketch-plugin-f53984696990"
409 | openLinkInBrowser(urlString)
410 | }
411 |
412 | ////////
413 | function feedbackByMail(context) {
414 |
415 | initCommand(context)
416 | sendEvent(context, 'Command', 'Feedback by Mail', 'Started')
417 |
418 | const to = encodeURI("einancunlu" + "@gma" + "il.com")
419 | const urlString = "mailto:" + to
420 | openLinkInBrowser(urlString)
421 | }
422 |
423 | ////////
424 | function feedbackByTwitter(context) {
425 |
426 | initCommand(context)
427 | sendEvent(context, 'Command', 'Feedback by Twitter', 'Started')
428 |
429 | const urlString = "https://twitter.com/einancunlu"
430 | openLinkInBrowser(urlString)
431 | }
432 |
433 | ////////
434 | function about(context) {
435 |
436 | initCommand(context)
437 | sendEvent(context, 'Command', 'About', 'Started')
438 |
439 | const urlString = "http://emin.space/?ref=flattenplugin"
440 | openLinkInBrowser(urlString)
441 | }
442 |
443 | ////////
444 | function donation(context) {
445 |
446 | initCommand(context)
447 | sendEvent(context, 'Command', 'Donation', 'Started')
448 |
449 | const urlString = "https://www.buymeacoffee.com/6SXFyDupj"
450 | openLinkInBrowser(urlString)
451 | }
452 |
453 | //------------------------------------------------------------------------------
454 | // ACTIONS
455 | //------------------------------------------------------------------------------
456 |
457 | ////////
458 | function onSelectionChanged(context) {
459 |
460 | savedContext = context
461 | if (autoFunctionsEnabled === '0') return
462 | if (NSEvent.pressedMouseButtons() === 2) return // BUG: Doesn't solve completely.
463 | const action = context.actionContext
464 | const newSelection = fromNativeArray(action.newSelection)
465 | // const oldSelection = fromNativeArray(action.oldSelection)
466 |
467 | if (newSelection.length === 1) {
468 | const layer = newSelection[0]
469 | const parent = layer.parent
470 | if (isFlattenedGroup(parent) && isFlattenedGroupValid(parent)) {
471 | if (hasTag(layer, disableAutoTag)) return
472 | if (isImageLayer(layer)) {
473 | // Update it when an image layer is selected
474 | const actualLayer = parent.layers[(layer.index === 1 ? 0 : 1)]
475 | flattenLayers([actualLayer])
476 | sendEvent(context, 'Auto', 'Selected', 'Image layer')
477 | } else if (hasTag(layer, flattenTag)) {
478 | // Set actual layer visible when it's selected
479 | if (hasTag(layer, stayHiddenTag)) return
480 | const imageLayer = parent.layers[(layer.index === 1 ? 0 : 1)]
481 | layer.hidden = false
482 | imageLayer.hidden = true
483 | sendEvent(context, 'Auto', 'Selected', 'Actual layer')
484 | }
485 | } else if (isImageLayer(layer)) {
486 | if (hasTag(layer, disableAutoTag)) return
487 | // When the image layer of the artboard is selected, update all the
488 | // the flattened layers inside
489 | if (Settings.layerSettingForKey(layer, kArtboardOfImageLayerKey) === parent.id) {
490 | if (isArtboard(parent) && hasTag(layer, flattenTag)) {
491 | flattenLayers(findAllSublayersWithFlattenTag(parent.layers))
492 | sendEvent(context, 'Auto', 'Selected', 'Image layer of artboard')
493 | }
494 | }
495 | }
496 | }
497 | }
498 |
499 | //------------------------------------------------------------------------------
500 | // HELPER FUNCTIONS
501 | //------------------------------------------------------------------------------
502 |
503 | ////////
504 | function isFlattenedGroup(group) {
505 |
506 | return Settings.layerSettingForKey(group, kGroupKey)
507 | }
508 |
509 | ////////
510 | function isImageLayer(layer) {
511 |
512 | return Settings.layerSettingForKey(layer, kImageLayerKey)
513 | }
514 |
515 | ////////
516 | function hasImageLayer(artboard) {
517 |
518 | for (const layer of artboard.layers.reverse()) {
519 | if (isImageLayer(layer) && hasTag(layer, flattenTag)) return layer
520 | })
521 | return false
522 | }
523 |
524 | ////////
525 | function flattenLayers(layers) {
526 |
527 | sendEvent(savedContext, 'Function', 'FlattenLayers', 'Started', layers.length)
528 | const returnLayer = layers.length === 1
529 | for (var layer of layers) {
530 | if (isChildOfFlattenedGroup(layer)) continue
531 | const parent = layer.parent
532 | var layerIsArtboard = isArtboard(layer)
533 | var imageLayer
534 | if (isImageLayer(layer)) {
535 | imageLayer = layer
536 | if (isArtboard(parent)) {
537 | sendEvent(savedContext, 'Function', 'FlattenLayers', 'Flattened artboard')
538 | layer = parent
539 | layerIsArtboard = true
540 | } else {
541 | layer = parent.layers[(imageLayer.index === 1 ? 0 : 1)]
542 | if (!layer || !hasTag(layer, flattenTag)) continue
543 | sendEvent(savedContext, 'Function', 'FlattenLayers', 'Flattened group')
544 | }
545 | } else {
546 | // Skip the layer if it's hidden
547 | if (!isFlattenedGroup(parent)) {
548 | if (layer.hidden) continue
549 | }
550 | imageLayer = getImageLayer(layer)
551 | sendEvent(savedContext, 'Function', 'FlattenLayers', 'Layer')
552 | }
553 | // Prepare for flattening: set visibility of layers
554 | var backgroundLayer
555 | if (layerIsArtboard) {
556 | backgroundLayer = createArtboardBackgroudColorLayer(layer) // BUG: Right click.
557 | }
558 | imageLayer.hidden = true
559 | var layersToHide
560 | if (layerIsArtboard) {
561 | layersToHide = findLayersByTag_inContainer(excludeTag, layer)
562 | each(layersToHide, function(layerToHide) {
563 | Settings.setLayerSettingForKey(layerToHide, kLayerVisibilityKey, layerToHide.hidden)
564 | layerToHide.hidden = true
565 | })
566 | } else {
567 | layer.hidden = false
568 | }
569 | // Duplicate the layer to be flattened if it's not an artboard
570 | var duplicateArtboard, duplicateLayer = null
571 | if (!layerIsArtboard) {
572 | const artboard = getArtboardOfLayer(layer)
573 | if (artboard) {
574 | Settings.setLayerSettingForKey(layer, kLayerToBeFlattened, true)
575 | duplicateArtboard = artboard.duplicate() // BUG: Right click.
576 | duplicateArtboard.adjustToFit()
577 | createArtboardBackgroudColorLayer(duplicateArtboard)
578 | // Find the layer to be flattened in the duplicate
579 | const found = fromNativeArray(findLayersByLayerName_inContainer(layer.name, duplicateArtboard))
580 | for (const tempLayer of found) {
581 | if (Settings.layerSettingForKey(tempLayer, kLayerToBeFlattened))) {
582 | duplicateLayer = tempLayer
583 | }
584 | }
585 | Settings.setLayerSettingForKey(layer, kLayerToBeFlattened, false)
586 | }
587 | }
588 | if(!duplicateLayer) duplicateLayer = layer
589 | // Flatten
590 | const tempFolderPath = getTempFolderPath()
591 | const imagePath = exportLayerToPath(duplicateLayer, tempFolderPath)
592 | fillLayerWithImage(imageLayer, imagePath)
593 | cleanUpTempFolder(tempFolderPath)
594 | if (duplicateArtboard) duplicateArtboard.remove()
595 | if (backgroundLayer) backgroundLayer.remove()
596 | // Restore visibility of layers
597 | if (layerIsArtboard) {
598 | each(layersToHide, function(layerToHide) {
599 | const initialVisibility = Settings.layerSettingForKey(layerToHide, kLayerVisibilityKey)
600 | layerToHide.hidden = initialVisibility
601 | })
602 | // Sync shared style
603 | imageLayer.sketchObject.updateSharedStyleToMatchSelf()
604 | imageLayer.moveToFront() // BUG: Right click.
605 | } else {
606 | layer.hidden = true && !hasTag(imageLayer, stayHiddenTag)
607 | // Sync shared style if it exists
608 | if (imageLayer.sketchObject.style().sharedObjectID()) {
609 | imageLayer.sketchObject.updateSharedStyleToMatchSelf()
610 | }
611 | }
612 | imageLayer.hidden = layerIsArtboard || hasTag(imageLayer, stayHiddenTag)
613 | if (returnLayer) return imageLayer
614 | })
615 | }
616 |
617 | ////////
618 | function isChildOfFlattenedGroup(layer) {
619 |
620 | if (isArtboard(layer)) return false
621 | const parent = layer.parent
622 | if (parent && !isPage(parent)) {
623 | if (hasTag(parent.name, flattenTag)) {
624 | return true
625 | } else {
626 | return isChildOfFlattenedGroup(parent)
627 | }
628 | }
629 | return false
630 | }
631 |
632 | ////////
633 | function getImageLayer(referenceLayer) {
634 |
635 | if (isArtboard(referenceLayer)) {
636 | const imageLayer = hasImageLayer(referenceLayer)
637 | if (imageLayer) {
638 | updateImageLayerRect(imageLayer, referenceLayer)
639 | return imageLayer
640 | } else {
641 | return createImageLayer(referenceLayer, referenceLayer)
642 | }
643 | } else {
644 | // Check if the layer is in a proper group and has image layer in it
645 | const parent = referenceLayer.parent
646 | if (isFlattenedGroupValid(parent)) {
647 | for (const layer of parent.layers) {
648 | if (isImageLayer(layer)) {
649 | updateImageLayerRect(layer, referenceLayer)
650 | parent.adjustToFit()
651 | return layer
652 | }
653 | }
654 | } else {
655 | // If not, create a group and image layer
656 | const layersToGroup = MSLayerArray.arrayWithLayers([referenceLayer.sketchObject])
657 | const group = sketch.fromNative(MSLayerGroup.groupFromLayers(layersToGroup))
658 | group.name = generateGroupName(referenceLayer.name)
659 | // newGroup.setConstrainProportions(false)
660 | // Change later - couldn't set the index of the group
661 | // const group = new sketch.Group({
662 | // parent: parent, name: generateGroupName(referenceLayer.name),
663 | // layers: [referenceLayer]
664 | // })
665 | Settings.setLayerSettingForKey(group, kGroupKey, true)
666 | imageLayer = createImageLayer(referenceLayer, group)
667 | return imageLayer
668 | }
669 | }
670 | }
671 |
672 | ////////
673 | function isFlattenedGroupValid(group) {
674 |
675 | const isChildrenCountValid = group.layers.length === 2
676 | if (group && isChildrenCountValid && isFlattenedGroup(group)) {
677 | return true
678 | } else {
679 | return false
680 | }
681 | }
682 |
683 | ////////
684 | function updateImageLayerRect(imageLayer, referenceLayer) {
685 |
686 | imageLayer.frame = referenceLayer.frame
687 | }
688 |
689 | ////////
690 | function createImageLayer(referenceLayer) {
691 |
692 | const parent = referenceLayer.parent
693 | const imageLayer = new sketch.Shape({
694 | parent: isArtboard(referenceLayer) ? referenceLayer : parent,
695 | frame: referenceLayer.frame,
696 | style: { fills: [{ fillType: FillType.Pattern }] }
697 | })
698 | if (isArtboard(referenceLayer)) {
699 | imageLayer.frame.x = 0
700 | imageLayer.frame.y = 0
701 | imageLayer.name = imageLayerName + ' ' + flattenTag
702 | Settings.setLayerSettingForKey(imageLayer, kArtboardOfImageLayerKey, referenceLayer.id)
703 | } else {
704 | imageLayer.name = imageLayerName
705 | }
706 | Settings.setLayerSettingForKey(imageLayer, kImageLayerKey, true)
707 | return imageLayer
708 | }
709 |
710 | ////////
711 | function createArtboardBackgroudColorLayer(artboard) {
712 |
713 | var backgroundColor
714 | if (artboard.sketchObject.hasBackgroundColor()) {
715 | backgroundColor = artboard.sketchObject.backgroundColor()
716 | } else {
717 | backgroundColor = '#ffffff'
718 | }
719 | // BUG: Right click.
720 | const layer = new sketch.Shape({
721 | parent: artboard, name: 'temp-bg',
722 | frame: new sketch.Rectangle(0,0, artboard.frame.width, artboard.frame.height),
723 | style: {
724 | fills: [{ color: backgroundColor, fillType: sketch.Style.FillType.Color }]
725 | }
726 | })
727 | layer.moveToBack()
728 | return layer
729 | }
730 |
731 | ////////
732 | function generateGroupName(layerName) {
733 |
734 | const regex = new RegExp(' #flatten.*', 'g')
735 | return layerName.replace(regex, '')
736 | }
737 |
738 | ////////
739 | function addTagToLayers(layers, tag) {
740 |
741 | for (var layer of layers) {
742 | if (isArtboard(layer)) continue
743 | if (tag === flattenTag && isImageLayer(layer)) continue
744 | if (!hasTag(layer, tag)) {
745 | layer.name = layer.name + ' ' + tag)
746 | }
747 | }
748 | }
749 |
750 | ////////
751 | function hasTag(layer, tag) {
752 |
753 | const regex = new RegExp(`${tag}(\\s|$)`, 'g')
754 | return regex.test(layer.name)
755 | }
756 |
757 | ////////
758 | function findAllSublayersWithFlattenTag(layers) {
759 |
760 | var array = NSArray.array()
761 | for (const layer of layers) {
762 | const layersToAdd = findLayersByTag_inContainer(flattenTag, layer)
763 | array = array.arrayByAddingObjectsFromArray(layersToAdd)
764 | }
765 | return fromNativeArray(array)
766 | }
767 |
768 | ////////
769 | function exportLayerToPath(layer, folderPath) {
770 |
771 | var scale = defaultFlattenScale
772 |
773 | // Check if there is a custom scale tag
774 | var nameToCheck = ''
775 | if (isArtboard(layer)) {
776 | const imageLayer = hasImageLayer(layer)
777 | if (imageLayer) nameToCheck = imageLayer.name
778 | } else {
779 | nameToCheck = layer.name
780 | }
781 | const regexString = `${scaleTagPrefix}(\\d{1}[\\,\\.]?\\d{0,2})`
782 | const regex = new RegExp(regexString, 'g')
783 | const match = regex.exec(nameToCheck)
784 | if (match && match.length === 2) {
785 | scale = checkScale(parseFloat(match[1].replace(',', '.')))
786 | }
787 | // Export layer
788 | if (isArtboard(layer)) {
789 | const originalLayerName = layer.name
790 | layer.name = "temp"
791 | sketch.export(layer, {
792 | trimmed: false,
793 | output: folderPath,
794 | scales: scale,
795 | formats: 'png'
796 | })
797 | layer.name = originalLayerName
798 | } else {
799 | // TODO: Use slice layer to export (trimmed: false doesn't work for now)
800 | const parent = layer.parent
801 | const parentFrame = layer.parent.frame
802 | const rect = NSMakeRect(0, 0, parentFrame.width, parentFrame.height)
803 | const slice = MSSliceLayer.alloc().initWithFrame(rect)
804 | parent.sketchObject.insertLayer_atIndex(slice, layer.index + 1)
805 | const exportFormat = MSExportFormat.formatWithScale_name_fileFormat(scale, '', 'png')
806 | slice.exportOptions().insertExportFormat_atIndex(exportFormat, 0)
807 | slice.exportOptions().setLayerOptions(2)
808 | const exportRequests = MSExportRequest.exportRequestsFromExportableLayer(slice)
809 | const path = folderPath + '/' + String(layer.id) + '.png'
810 | if (!document) document = sketch.getSelectedDocument()
811 | document.sketchObject.saveExportRequest_toFile(exportRequests[0], path)
812 | slice.removeFromParent()
813 | }
814 |
815 | // Return the exported file path
816 | const fileManager = NSFileManager.defaultManager()
817 | const folder = fileManager.contentsOfDirectoryAtPath_error(folderPath, nil)
818 | const imageFileName = folder[0]
819 | return folderPath + '/' + imageFileName
820 | }
821 |
822 | ////////
823 | function fillLayerWithImage(layer, imagePath) {
824 |
825 | const image = NSImage.alloc().initWithContentsOfFile(imagePath)
826 | const imageData = MSImageData.alloc().initWithImage(image)
827 | if (image && layer.type === String(sketch.Types.Shape)) {
828 | if (layer.sketchObject) layer = layer.sketchObject
829 | var fill = layer.style().firstEnabledFill()
830 | if (!fill) fill = layer.style().addStylePartOfType(0)
831 | fill.setFillType(FillType.Pattern)
832 | fill.setPatternFillType(PatternFillType.Fill)
833 | fill.setImage(imageData)
834 | }
835 | }
836 |
837 | ////////
838 | function setImageModeForSelectionOrAll(isImageModeOn) {
839 |
840 | var layers
841 | if (selection.length === 0) {
842 | layers = findLayersByTag_inContainer(flattenTag)
843 | } else {
844 | layers = findAllSublayersWithFlattenTag(selection.layers)
845 | }
846 | if (layers.length === 0) {
847 | UI.message('No layer found to be switched.')
848 | return
849 | }
850 | each(layers, function(layer) {
851 | toggleGroupMode(layer, isImageModeOn)
852 | })
853 | }
854 |
855 | ////////
856 | function toggleGroupMode(actualLayer, isImageModeOn) {
857 |
858 | const parent = actualLayer.parent
859 | if (isArtboard(actualLayer)) return
860 | if (isFlattenedGroup(parent)) {
861 | if (!isFlattenedGroupValid(parent)) return
862 | if (isImageModeOn === undefined) isImageModeOn = !actualLayer.hidden
863 | actualLayer.hidden = isImageModeOn
864 | const imageLayer = parent.layers[(actualLayer.index === 1 ? 0 : 1)]
865 | imageLayer.hidden = !isImageModeOn
866 | }
867 | }
868 |
869 | ////////
870 | function checkScale(scale) {
871 |
872 | if (scale > maxFlatteningScale) UI.message(`⚠️ 'Flattening scale can't be bigger than ${maxFlatteningScale}. It's been converted to ${maxFlatteningScale}.`)
873 | if (scale < minFlatteningScale) UI.message(`⚠️ 'Flattening scale can't be smaller than ${minFlatteningScale} It's been converted to ${minFlatteningScale}.`)
874 | scale = Math.min(scale, maxFlatteningScale)
875 | scale = Math.max(scale, minFlatteningScale)
876 | return scale
877 | }
878 |
879 | ////////
880 | function isImageLayerOfArtboard(imageLayer, artboard) {
881 |
882 | const arboardID = Settings.layerSettingForKey(imageLayer, kArtboardOfImageLayerKey)
883 | return (arboardID && arboardID === artboard.id)
884 | }
885 |
886 | ////////
887 | function recoverLayer(layer) {
888 |
889 | if (isFlattenedGroup(layer)) {
890 | // Unflatten the flattened group
891 | if (!isFlattenedGroupValid(layer)) {
892 | UI.message("⚠️ It's isn't a proper flattened group. (Layer: " + layer.name + ")")
893 | } else {
894 | var imageLayer, actualLayer
895 | for (const childLayer of layer.layers) {
896 | if (isImageLayer(childLayer)) {
897 | imageLayer = childLayer
898 | } else if (hasTag(childLayer, flattenTag)) {
899 | actualLayer = childLayer
900 | }
901 | }
902 | if (imageLayer) imageLayer.remove()
903 | if (actualLayer) actualLayer.hidden = false
904 | actualLayer.name = generateGroupName(actualLayer.name)
905 | layer.sketchObject.ungroup()
906 | actualLayer.sketchObject.select_byExpandingSelection(true, false)
907 | sendEvent(savedContext, 'Function', 'Recover Layer', 'Flattened group')
908 | }
909 | } else if (isArtboard(layer))) {
910 | // Delete the image layer and shared style if exists
911 | if (hasImageLayer(layer)) {
912 | const imageLayer = getImageLayer(layer)
913 | const layerStyles = document.sketchObject.documentData().layerStyles()
914 | const sharedStyle = layerStyles.sharedStyleWithID(imageLayer.sketchObject.style().sharedObjectID())
915 | layerStyles.removeSharedStyle(sharedStyle)
916 | document.sketchObject.reloadInspector()
917 | imageLayer.remove()
918 | sendEvent(savedContext, 'Function', 'Recover Layer', 'Artboard')
919 | } else {
920 | UI.message("⚠️ It's isn't a flattened artboard. (Layer: " + layer.name + ")")
921 | }
922 | } else if (referenceLayerID = Settings.layerSettingForKey(layer, kReferenceLayerOfPreviewLayerKey)) {
923 | // Delete the preview layer and unflatten the attached flattened group
924 | const referenceLayer = document.getLayerWithID(referenceLayerID)
925 | if (!isArtboard(referenceLayer)) {
926 | const layerStyles = document.sketchObject.documentData().layerStyles()
927 | const sharedStyle = layerStyles.sharedStyleWithID(layer.sketchObject.style().sharedObjectID())
928 | layerStyles.removeSharedStyle(sharedStyle)
929 | document.sketchObject.reloadInspector()
930 | }
931 | layer.remove()
932 | recoverLayer(referenceLayer)
933 | sendEvent(savedContext, 'Function', 'Recover Layer', 'Preview layer')
934 | } else if (Settings.layerSettingForKey(layer, kArtboardOfImageLayerKey)) {
935 | // Rename the image layer of an artboard
936 | const arboardId = Settings.layerSettingForKey(layer, kArtboardOfImageLayerKey)
937 | const artboard = document.getLayerWithID(arboardId)
938 | if (artboard) layer.name = artboard.name
939 | else UI.message("⚠️ Couldn't recover the name. (Couldn't find any attached artboard.)")
940 | sendEvent(savedContext, 'Function', 'Recover Layer', 'Image layer')
941 | } else {
942 | UI.message("⚠️ Couldn't find anything to recover. (Layer: " + layer.name + ")")
943 | }
944 | }
945 |
946 | //------------------------------------------------------------------------------
947 | // UTILITIES
948 | //------------------------------------------------------------------------------
949 |
950 | ////////
951 | function each(array, handler) {
952 |
953 | const native = array.count ? true : false
954 | const count = array.count ? array.count() : array.length
955 | for (var i = 0; i < count; i++) {
956 | const layer = array[i]
957 | if (native) handler(sketch.fromNative(layer), i)
958 | else handler(layer, i)
959 | }
960 | }
961 |
962 | ////////
963 | function toArray(object) {
964 |
965 | if (Array.isArray(object)) {
966 | return object
967 | }
968 | var arr = []
969 | for (var j = 0; j < (object || []).length; j += 1) {
970 | arr.push(object[j])
971 | }
972 | return arr
973 | }
974 |
975 | ////////
976 | function fromNativeArray(array) {
977 |
978 | const jsArray = []
979 | each(array, function(layer) {
980 | jsArray.push(layer)
981 | })
982 | return jsArray
983 | }
984 |
985 | ////////
986 | function isArtboard(layer) {
987 |
988 | return layer.type === String(sketch.Types.Artboard)
989 | }
990 |
991 | ////////
992 | function isPage(layer) {
993 |
994 | return layer.type === String(sketch.Types.Page)
995 | }
996 |
997 | ////////
998 | function getArtboardOfLayer(layer) {
999 |
1000 | if (isArtboard(layer)) return layer
1001 | const parent = layer.parent
1002 | if (parent) {
1003 | if (isArtboard(parent)) return parent
1004 | else return getArtboardOfLayer(parent)
1005 | } else {
1006 | return
1007 | }
1008 | }
1009 |
1010 | ////////
1011 | function createCheckbox(label, state) {
1012 |
1013 | state = (state === '0') ? NSOffState : NSOnState
1014 | const checkbox = NSButton.alloc().initWithFrame(NSMakeRect(0, 0, 300, 18))
1015 | checkbox.setButtonType(NSSwitchButton)
1016 | checkbox.setTitle(label)
1017 | checkbox.setState(state)
1018 | return checkbox
1019 | }
1020 |
1021 | ////////
1022 | function openLinkInBrowser(urlString) {
1023 |
1024 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(urlString))
1025 | }
1026 |
1027 | ////////
1028 | function getTempFolderPath() {
1029 |
1030 | const fileManager = NSFileManager.defaultManager()
1031 | const cachesURL = fileManager.URLsForDirectory_inDomains(NSCachesDirectory, NSUserDomainMask).lastObject()
1032 | return cachesURL.URLByAppendingPathComponent(kPluginDomain).path() + '/' + NSDate.date().timeIntervalSince1970()
1033 | }
1034 |
1035 | ////////
1036 | function cleanUpTempFolder(folderPath) {
1037 |
1038 | NSFileManager.defaultManager().removeItemAtPath_error(folderPath, nil)
1039 | }
1040 |
1041 | // FINDING LAYERS - Thanks to Aby Nimbalkar > https://github.com/abynim
1042 |
1043 | ////////
1044 | function findLayersByLayerName_inContainer(layerName, container) {
1045 |
1046 | var predicate = NSPredicate.predicateWithFormat("name == %@", layerName)
1047 | return findLayersMatchingPredicate_inContainer_filterByType(predicate, container)
1048 | }
1049 |
1050 | ////////
1051 | function findLayersByTag_inContainer(tag, container) {
1052 |
1053 | var regex = '.*' + tag + '\\b.*'
1054 | var predicate = NSPredicate.predicateWithFormat('name MATCHES[c] %@', regex)
1055 | return findLayersMatchingPredicate_inContainer_filterByType(predicate, container)
1056 | }
1057 |
1058 | ////////
1059 | function findLayersMatchingPredicate_inContainer_filterByType(predicate, container, layerType) {
1060 |
1061 | if (container && container !== undefined && container.sketchObject) container = container.sketchObject
1062 | var scope
1063 | switch (layerType) {
1064 | case MSPage:
1065 | scope = document.sketchObject.pages()
1066 | return scope.filteredArrayUsingPredicate(predicate)
1067 | break
1068 | case MSArtboardGroup:
1069 | if (typeof container !== 'undefined' && container != nil) {
1070 | if (container.className == 'MSPage') {
1071 | scope = container.artboards()
1072 | return scope.filteredArrayUsingPredicate(predicate)
1073 | }
1074 | } else {
1075 | // search all pages
1076 | var filteredArray = NSArray.array()
1077 | var loopPages = document.sketchObject.pages().objectEnumerator(),
1078 | page;
1079 | while (page = loopPages.nextObject()) {
1080 | scope = page.artboards()
1081 | filteredArray = filteredArray.arrayByAddingObjectsFromArray(scope.filteredArrayUsingPredicate(predicate))
1082 | }
1083 | return filteredArray
1084 | }
1085 | break
1086 | default:
1087 | if (typeof container !== 'undefined' && container != nil) {
1088 | scope = container.children()
1089 | return scope.filteredArrayUsingPredicate(predicate)
1090 | } else {
1091 | // search all pages
1092 | var filteredArray = NSArray.array()
1093 | var loopPages = document.sketchObject.pages().objectEnumerator(),
1094 | page;
1095 | while (page = loopPages.nextObject()) {
1096 | scope = page.children()
1097 | filteredArray = filteredArray.arrayByAddingObjectsFromArray(scope.filteredArrayUsingPredicate(predicate))
1098 | }
1099 | return filteredArray
1100 | }
1101 | }
1102 | return NSArray.array() // Return an empty array if no matches were found
1103 | }
1104 |
1105 | //------------------------------------------------------------------------------
1106 | // ANALYTICS
1107 | //------------------------------------------------------------------------------
1108 |
1109 | var kUUIDKey = 'google.analytics.uuid'
1110 | var uuid = NSUserDefaults.standardUserDefaults().objectForKey(kUUIDKey)
1111 | if (!uuid) {
1112 | uuid = NSUUID.UUID().UUIDString()
1113 | NSUserDefaults.standardUserDefaults().setObject_forKey(uuid, kUUIDKey)
1114 | }
1115 |
1116 | ////////
1117 | function jsonToQueryString(json) {
1118 |
1119 | return '?' + Object.keys(json).map(function (key) {
1120 | return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
1121 | }).join('&')
1122 | }
1123 |
1124 | ////////
1125 | var index = function (context, trackingId, hitType, props) {
1126 |
1127 | var payload = {
1128 | v: 1,
1129 | tid: trackingId,
1130 | ds: 'Sketch ' + NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleShortVersionString"),
1131 | cid: uuid,
1132 | t: hitType,
1133 | an: context.plugin.name(),
1134 | aid: context.plugin.identifier(),
1135 | av: context.plugin.version()
1136 | }
1137 | if (props) {
1138 | Object.keys(props).forEach(function (key) {
1139 | payload[key] = props[key]
1140 | })
1141 | }
1142 |
1143 | var url = NSURL.URLWithString(
1144 | NSString.stringWithFormat("https://www.google-analytics.com/collect%@", jsonToQueryString(payload))
1145 | )
1146 |
1147 | if (url) {
1148 | NSURLSession.sharedSession().dataTaskWithURL(url).resume()
1149 | }
1150 | }
1151 |
1152 | ////////
1153 | function sendEvent(context, category, action, label, value) {
1154 |
1155 | log(category + " - " + action + " - " + label + " - " + value)
1156 | if (developmentMode) return
1157 | const payload = {}
1158 | if (category) payload.ec = category
1159 | if (action) payload.ea = action
1160 | if (label) payload.el = label
1161 | if (value) payload.ev = value
1162 | return index(context, 'UA-34242159-5', 'event', payload)
1163 | }
1164 |
1165 | //------------------------------------------------------------------------------
1166 | // DEPRECIATED
1167 | //------------------------------------------------------------------------------
1168 |
1169 | // ////////
1170 | // function isReallyVisible(layer) {
1171 | //
1172 | // if (!layer.isVisible()) { return false }
1173 | // if (layer.class() === MSArtboardGroup) { return true }
1174 | // var parent = [layer parentGroup]
1175 | // if (parent) {
1176 | // return isReallyVisible(parent)
1177 | // } else {
1178 | // return true
1179 | // }
1180 | // }
1181 | //
1182 | // ////////
1183 | // function suggestedLayers(context) {
1184 | //
1185 | // var doc = context.document
1186 | // var command = context.command
1187 | // selection = context.selection
1188 | // var numberOfSuggestedLayers = 0
1189 | // if (selection.firstObject()) {
1190 | // selection.firstObject().select_byExpandingSelection(false, false)
1191 | // }
1192 | // var children = doc.currentPage().children()
1193 | // for (var i = 0; i < [children count]; i++) {
1194 | // var layer = children[i]
1195 | // if (isReallyVisible(layer) == false) { continue }
1196 | // if (layer.style != undefined) {
1197 | // if (layer.style().blur().isEnabled()) {
1198 | // var find = new RegExp(flattenTag, "i")
1199 | // if (!layer.name().match(find)) {
1200 | // numberOfSuggestedLayers++
1201 | // layer.select_byExpandingSelection(true, true)
1202 | // }
1203 | // }
1204 | // }
1205 | // }
1206 | //
1207 | // if (numberOfSuggestedLayers == 0) {
1208 | // [doc showMessage: "All layers in this page seem good to me! No suggestion for this page. : )"]
1209 | // } else {
1210 | // var message
1211 | // if (numberOfSuggestedLayers == 1) {
1212 | // message = " layer which can be flattened to increase the performance of the Sketch."
1213 | // } else {
1214 | // message = " layers which can be flattened to increase the performance of the Sketch."
1215 | // }
1216 | // [doc showMessage: "Found " + numberOfSuggestedLayers + message]
1217 | // }
1218 | // }
1219 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flatten Plugin for Sketch
2 |
3 | Version 2 is here! Check out my new [Medium post](https://medium.com/@einancunlu/flatten-2-0-sketch-plugin-f53984696990) for all the new features!
4 |
5 | # Installation
6 |
7 | With Sketch Runner, just go to the `install` command and search for `Flatten`. Runner allows you to manage plugins and do much more to speed up your workflow in Sketch. [Download Runner here](http://www.sketchrunner.com).
8 |
9 |
10 |
11 |
12 |
13 | # Changelog
14 |
15 | ## Game Over...
16 | Here my answer to a message:
17 |
18 | Hi X,
19 |
20 | It's great to hear that some people use my plugin. I wish I had enough time and resources to fix it. Unfortunately it's really unlikely.
21 |
22 | I don't have enough time, but more importantly, Sketch doesn't make anything easier for plugin developers. They don't update their plugin development kit and documentation as they should do. So, every time they change things, we need to try and find out the problem. This is really abrasive. I updated my plugin so many times saying to myself that it's for the last time, but it just kept happening. So I really don't have any more patient left nor willing. Sketch team seems really reckless (or impulsive) about the plugins, developers, users and documentation.
23 |
24 | So, no more Sketch for me. I'm planning to switch to Figma soon. They stopped being revolutionary anyway, they are doing nothing good enough IMHO. Every update is mess and I really don't like how they plan things out. I suggest you to do the same. I'm just waiting for the second stage update of the plugin development kit of Figma (they did the first stage already). As soon as they have the plugin support, I will delete Sketch for forever, probably. Really sorry for that... :(
25 |
26 | Bests,
27 | Emin
28 |
29 | ## v2.1.0
30 |
31 | - Now, it's easier than ever to create a preview layer from an artboard or layer with a command instead of creating it yourself. It automatically detects the zoom value of the viewport and creates the preview layer scaled to be seen in 100% zoom value. I will update the Medium post soon to show the changes, stay tuned!
32 | - Delete this preview layer and restore all the related things easily with the restore command.
33 | - Added Sketchrunner command icons and descriptions. Now, it's easier to recognize the command items in Sketchrunner's search panel.
34 | - Some bug fixes and improvements.
35 |
36 | ## v2.0.1
37 |
38 | - Layer mirroring: Now you can mirror a normal layer by creating a shared style. The plugin will update it automatically when you flatten again.
39 | - Auto flattening: If you select "Flattened Image" layers in groups or artboards, they will be updated automatically.
40 | - Auto toggling: Select the hidden actual layer to make it visible automatically. Select the flattened image back to toggle it back and update.
41 | - New tags: #no-auto, #stay-hidden, #sx.xx, #exclude. (Check out the Medium post for all the details.)
42 | - Settings panel for customizing the values like flattening scale (quality), style name prefix etc. You can also turn on and off the automated features.
43 | - Unflattening: Select a flattened group and unflatten to revert it back. If you unflatten an artboard image layer, it will change the name to match the artboard it's connected to.
44 | - Now, there is no need to add artboard color or to keep the layer inside the borders of the artboard to be able to flatten it correctly. Flatten everything everywhere!
45 | - Sketch 5.0 fix.
46 |
47 | ## v1.6.3
48 | - Sketch 4.7 fix.
49 |
50 | ## v1.6
51 | - Sketch 4.5 fix.
52 | - Support for the plugin update feature of Sketch.
53 |
54 | ## v1.5
55 | - Sketch 4.3.1 fix.
56 |
57 | ## v1.4
58 | - Bug fix.
59 |
60 | ## v1.3
61 | - Sketch 3.9 fix.
62 | - Now artboard shared styles sync automatically after reflattening artboards.
63 | - New command: Toggle. You can toggle single or multiple flattened layers to edit them easily by selecting the group(s) and run the toggle command.
64 |
65 | ## v1.2
66 | - Sketch 3.8 fix.
67 | - Now when you flatten a single layer, the selection will be updated to the group created.
68 |
69 | ## v1.1
70 | - Fixed compatibility issues with Sketch 3.5.
71 | - Plugin menu improvements.
72 |
73 | # Contact
74 |
75 | [Twitter](https://twitter.com/einancunlu)
76 |
77 | # License
78 |
79 | The MIT License (MIT)
80 |
--------------------------------------------------------------------------------
/appcast.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Flatten
5 | https://raw.githubusercontent.com/einancunlu/Flatten-Plugin-for-Sketch/master/appcast.xml
6 | Flatten and mirror layers without destructing and update them like a boss.
7 | en
8 | -
9 | Version 2.1.0
10 | https://github.com/einancunlu/Flatten-Plugin-for-Sketch
11 |
12 |
13 | -
14 |
15 |
16 | -
17 |
18 |
19 | -
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------