├── .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 | --------------------------------------------------------------------------------