├── README.md └── State Switch Master.sketchplugin └── Contents └── Sketch ├── manifest.json └── scripts.cocoascript /README.md: -------------------------------------------------------------------------------- 1 | # State Switch Master 2 | 3 | Check out [my Medium post](https://medium.com/design-prototype-develop/state-switch-master-sketch-plugin-baec61c1e943) for the details. 4 | 5 | # Changelog 6 | 7 | ## v1.3 8 | - Fixed compatibility issues with Sketch 3.5. 9 | - Copied and pasted state layers is hidden by default now. 10 | - Plugin update notification is added. I'm working on another update, so you will be notified soon hopefully! 11 | 12 | ## Installation 13 | 14 | #### Better Way: 15 | 1. [Download the Sketch Toolbox plugin manager app](http://sketchtoolbox.com) and install. 16 | 2. Search `State Switch Master` in the app and install. 17 | 18 | #### Manuel: 19 | 1. [Download the files](https://github.com/einancunlu/Sketch-State-Switch-Master/archive/master.zip) and unzip. 20 | 2. Double click the `State Switch Master.sketchplugin` bundle to install. 21 | 22 | 23 | ## Contact 24 | 25 | [Twitter](https://twitter.com/einancunlu). -------------------------------------------------------------------------------- /State Switch Master.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "State Switch Master", 3 | "description": "Define different states and switch between them like a boss.", 4 | "author": "Emin Inanc Unlu", 5 | "homepage": "https://github.com/einancunlu/Sketch-State-Switch-Master", 6 | "version": 1.3, 7 | "compatibleVersion": 3.4, 8 | "identifier": "com.einancunlu.sketch-state-switch-master", 9 | "commands": [ 10 | { 11 | "handler": "switchStates", 12 | "identifier": "switch-states", 13 | "name": "Switch State(s)", 14 | "shortcut" : "shift control s", 15 | "script": "scripts.cocoascript" 16 | }, 17 | { 18 | "handler": "changeScope", 19 | "identifier": "change-scope", 20 | "name": "Change Scope", 21 | "shortcut" : "shift control d", 22 | "script": "scripts.cocoascript" 23 | }, 24 | { 25 | "handler": "setReferenceOrCreateStateLayer", 26 | "identifier": "set-reference", 27 | "name": "Set Reference", 28 | "shortcut" : "shift control c", 29 | "script": "scripts.cocoascript" 30 | }, 31 | { 32 | "handler": "setReferenceOrCreateStateLayer", 33 | "identifier": "create-state-layer", 34 | "name": "Create State Layer", 35 | "shortcut" : "shift control v", 36 | "script": "scripts.cocoascript" 37 | } 38 | ], 39 | "menu": { 40 | "title": "State Switch Master", 41 | "items": [ 42 | "switch-states", 43 | "set-reference", 44 | "create-state-layer", 45 | "change-scope" 46 | ] 47 | } 48 | } -------------------------------------------------------------------------------- /State Switch Master.sketchplugin/Contents/Sketch/scripts.cocoascript: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // Created by Emin İnanç Ünlü. 5 | // 6 | 7 | /* 8 | The MIT License (MIT) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | */ 28 | 29 | 30 | //-------------------------------------- 31 | // Global Variables 32 | //-------------------------------------- 33 | 34 | 35 | var doc, 36 | app = [NSApplication sharedApplication], 37 | defaults = [NSUserDefaults standardUserDefaults], 38 | kPluginDomain = "com.einancunlu.sketch-state-switch-master", 39 | stateLayerNameKey = kPluginDomain + ".nameForStateLayer", 40 | switchScopeKey = kPluginDomain + ".switchScope", 41 | kLastUpdateCheckDayKey = kPluginDomain + ".lastUpdateCheckDayKey", 42 | selection, 43 | command, 44 | scriptPath, 45 | scriptFolder 46 | 47 | 48 | //-------------------------------------- 49 | // Menu Commands 50 | //-------------------------------------- 51 | 52 | 53 | function setReferenceOrCreateStateLayer(context) { 54 | 55 | doc = context.document 56 | initCommand(context) 57 | 58 | var selectionCount = [selection count] 59 | switch (true) { 60 | case (selectionCount == 0): 61 | [doc showMessage: "Oops, you need to select something!"] 62 | return 63 | case (selectionCount == 1): 64 | break 65 | case (selectionCount > 1): 66 | [doc showMessage: "Select only one thing."] 67 | return 68 | } 69 | 70 | var selectedLayer = selection.objectAtIndex(0) 71 | if (isChildOfStateGroup(selectedLayer)) { 72 | // Generate state layer name and save for reuse 73 | var stateLayerName = generateLayerNameToToggle(selection.objectAtIndex(0)) 74 | saveObjectToUserDefaults(stateLayerName, stateLayerNameKey) 75 | 76 | [doc showMessage: "New reference: " + stateLayerName] 77 | } else if ([selectedLayer class] === MSLayerGroup) { 78 | if (isStateGroup(selectedLayer) ) { 79 | [doc showMessage: "Select a layer inside of this group to create a reference."] 80 | return 81 | } 82 | 83 | // Load saved state layer name 84 | var stateLayerName = [defaults objectForKey: stateLayerNameKey] 85 | if (stateLayerName != nil) { 86 | var layers = findLayersNamed_inContainer_filterByType(stateLayerName, selectedLayer) 87 | var message = "Layer created: \"" + stateLayerName + "\"" 88 | var layerCount = [layers count] 89 | 90 | switch (true) { 91 | case (layerCount > 1): 92 | [doc showMessage: "Multiple state layer found!"] 93 | return 94 | case (layerCount == 1): 95 | // Delete existing state layer and resize selected group 96 | selectedLayer.removeLayer(layers[0]) 97 | if (getSketchVersionNumber() >= 350) { 98 | selectedLayer.resizeToFitChildrenWithOption(1) 99 | } else { 100 | selectedLayer.resizeRoot(true) 101 | } 102 | var message = "Layer updated: \"" + stateLayerName + "\"" 103 | break 104 | } 105 | 106 | // Create and add the state layer into the selected group 107 | var stateLayer = selectedLayer.addLayerOfType("rectangle") 108 | stateLayer.name = stateLayerName 109 | [stateLayer setIsVisible: false] 110 | [[stateLayer frame] setWidth: [[selectedLayer frame] width]] 111 | [[stateLayer frame] setHeight: [[selectedLayer frame] height]] 112 | [[stateLayer frame] setX: 0] 113 | [[stateLayer frame] setY: 0] 114 | [doc showMessage: message] 115 | } else { 116 | [doc showMessage: "No reference is set. Firstly, you need to select a layer inside of a state group and run the 'Set Reference' command."] 117 | } 118 | } else { 119 | [doc showMessage: "Select a group."] 120 | } 121 | } 122 | 123 | function switchStates(context) { 124 | 125 | doc = context.document 126 | initCommand(context) 127 | 128 | if ([selection count] == 0) { 129 | [app displayDialog: "Oops, you need to select something!"] 130 | return 131 | } 132 | 133 | // Toggle layers in the selected state groups 134 | var notChildOfStateGroupError = false 135 | for (var i = 0; i < [selection count]; i++) { 136 | var selectedLayer = selection.objectAtIndex(i) 137 | if (isChildOfStateGroup(selectedLayer)) { 138 | toggleStateLayer(selectedLayer) 139 | } else { 140 | notChildOfStateGroupError = true 141 | } 142 | } 143 | if (notChildOfStateGroupError) { 144 | [app displayDialog: "One or more of the selected items wasn't child of a state group. Nothing is applied to these."] 145 | } 146 | } 147 | 148 | function changeScope(context) { 149 | 150 | doc = context.document 151 | var selectedIndex = [defaults objectForKey: switchScopeKey] || 1 152 | var comboBoxItems = ['All Pages', 'Current Page', 'Artboard'] 153 | selectedIndex++ 154 | selectedIndex = selectedIndex % 3 155 | saveObjectToUserDefaults(selectedIndex, switchScopeKey) 156 | [doc showMessage: "Scope: " + comboBoxItems[selectedIndex]] 157 | } 158 | 159 | 160 | //-------------------------------------- 161 | // Helper Functions 162 | //-------------------------------------- 163 | 164 | 165 | function initCommand(context) { 166 | 167 | doc = context.document 168 | command = context.command 169 | selection = context.selection 170 | scriptPath = context.scriptPath 171 | scriptFolder = [scriptPath stringByDeletingLastPathComponent] 172 | 173 | if (isTodayNewDay() && checkPluginUpdate()) { 174 | app.displayDialog_withTitle("Please redownload the plugin to install the new version.", "There is a new version of the plugin!") 175 | } 176 | } 177 | 178 | function isTodayNewDay() { 179 | 180 | var lastUpdateCheckDay = [defaults objectForKey: kLastUpdateCheckDayKey] 181 | 182 | var formatter = [[NSDateFormatter alloc] init] 183 | [formatter setDateStyle: NSDateFormatterShortStyle] 184 | var today = [formatter stringFromDate: [NSDate date]] 185 | saveObjectToUserDefaults(today, kLastUpdateCheckDayKey) 186 | 187 | if (lastUpdateCheckDay) { 188 | return lastUpdateCheckDay != today 189 | } else { 190 | return true 191 | } 192 | } 193 | 194 | function saveObjectToUserDefaults(object, key) { 195 | 196 | var configs = [NSMutableDictionary dictionary] 197 | [configs setObject: object forKey: key] 198 | [defaults registerDefaults: configs] 199 | [defaults synchronize] 200 | } 201 | 202 | function scanAllLayersToToggle(stateLayer, layerNameToToggle, isVisible) { 203 | 204 | var switchScope = [defaults objectForKey: switchScopeKey] 205 | var container 206 | switch (parseInt(switchScope)) { 207 | case 0: // All Pages 208 | container = nil 209 | break 210 | case 1: // Currpent Page 211 | container = doc.currentPage() 212 | break 213 | case 2: // Current Artboard 214 | container = artboardOfLayer(stateLayer) 215 | break 216 | default: 217 | container = doc.currentPage() 218 | saveObjectToUserDefaults(1, switchScopeKey) 219 | } 220 | 221 | // Toggle layers 222 | var layers = findLayersNamed_inContainer_filterByType(layerNameToToggle, container) 223 | var loop = layers.objectEnumerator() 224 | while (layer = loop.nextObject()) { 225 | var parent = [layer parentGroup] 226 | if (parent) { 227 | [parent setIsVisible: isVisible] 228 | } 229 | } 230 | } 231 | 232 | function artboardOfLayer(layer) { 233 | 234 | var parent = [layer parentGroup] 235 | if (parent) { 236 | if ([parent class] === MSArtboardGroup) { 237 | return parent 238 | } else { 239 | return artboardOfLayer(parent) 240 | } 241 | } else { 242 | return nil 243 | } 244 | } 245 | 246 | function toggleStateLayer(layer) { 247 | 248 | var parent = [layer parentGroup] 249 | if (parent) { 250 | var children = [parent layers] 251 | for (var i = 0; i < [children count]; i++) { 252 | var child = children.objectAtIndex(i) 253 | var layerName = parent.name() + child.name() 254 | [child setIsVisible: (child === layer)] 255 | var layerNameToToggle = generateLayerNameToToggle(child) 256 | var isVisible = child.isVisible() 257 | scanAllLayersToToggle(layer, layerNameToToggle, isVisible) 258 | } 259 | [layer setIsVisible: true] 260 | var layerNameToToggle = generateLayerNameToToggle(layer) 261 | scanAllLayersToToggle(layer, layerNameToToggle, true) 262 | 263 | if ([parent class] === MSLayerGroup && isStateGroup(parent)) { 264 | return 265 | } else { 266 | toggleStateLayer(parent) 267 | } 268 | } 269 | } 270 | 271 | function isChildOfStateGroup(layer) { 272 | 273 | var parent = [layer parentGroup] 274 | if (parent) { 275 | if ([parent class] === MSLayerGroup && isStateGroup(parent)) { 276 | return true 277 | } else { 278 | return isChildOfStateGroup(parent) 279 | } 280 | } else { 281 | return false 282 | } 283 | } 284 | 285 | function generateLayerNameToToggle(layer) { 286 | 287 | if (isStateGroup(layer)) { 288 | return "S: " + layer.name().substring(8) 289 | } else { 290 | return generateLayerNameToToggle([layer parentGroup]) + " / " + layer.name() 291 | } 292 | } 293 | 294 | function isStateGroup(layerGroup) { 295 | 296 | return layerGroup.name().substring(0, 8) == "State - " 297 | } 298 | 299 | var findLayersMatchingPredicate_inContainer_filterByType = function (predicate, container, layerType) { 300 | 301 | var scope 302 | switch (layerType) { 303 | case MSPage: 304 | scope = doc.pages() 305 | return scope.filteredArrayUsingPredicate(predicate) 306 | break 307 | case MSArtboardGroup: 308 | if(typeof container !== 'undefined' && container != nil) { 309 | if (container.className == "MSPage") { 310 | scope = container.artboards() 311 | return scope.filteredArrayUsingPredicate(predicate) 312 | } 313 | } else { 314 | // Search all pages 315 | var filteredArray = NSArray.array() 316 | var loopPages = doc.pages().objectEnumerator(), page; 317 | while (page = loopPages.nextObject()) { 318 | scope = page.artboards() 319 | filteredArray = filteredArray.arrayByAddingObjectsFromArray(scope.filteredArrayUsingPredicate(predicate)) 320 | } 321 | return filteredArray 322 | } 323 | break 324 | default: 325 | if(typeof container !== 'undefined' && container != nil) { 326 | scope = container.children() 327 | return scope.filteredArrayUsingPredicate(predicate) 328 | } else { 329 | // Search all pages 330 | var filteredArray = NSArray.array() 331 | var loopPages = doc.pages().objectEnumerator(), page; 332 | while (page = loopPages.nextObject()) { 333 | scope = page.children() 334 | filteredArray = filteredArray.arrayByAddingObjectsFromArray(scope.filteredArrayUsingPredicate(predicate)) 335 | } 336 | return filteredArray 337 | } 338 | } 339 | return NSArray.array() // Return an empty array if no matches were found 340 | } 341 | 342 | var findLayersNamed_inContainer_filterByType = function (layerName, container, layerType) { 343 | 344 | var predicate = (typeof layerType === 'undefined' || layerType == nil) ? NSPredicate.predicateWithFormat("name == %@", layerName) : NSPredicate.predicateWithFormat("name == %@ && class == %@", layerName, layerType) 345 | return findLayersMatchingPredicate_inContainer_filterByType(predicate, container) 346 | } 347 | 348 | function checkPluginUpdate() { 349 | 350 | var manifestFilePath = scriptFolder + "/manifest.json" 351 | var manifestJSON = getJSONFromFile(manifestFilePath) 352 | var isThereNewVersion = false 353 | try { 354 | var response = getJSONFromURL('https://github.com/einancunlu/Sketch-State-Switch-Master/raw/master/State%20Switch%20Master.sketchplugin/Contents/Sketch/manifest.json') 355 | if (response && response.version) { 356 | if (response.version.toString() != manifestJSON.version.toString()) { 357 | isThereNewVersion = true 358 | } 359 | } 360 | } catch (e) { 361 | log(e) 362 | return false 363 | } 364 | return isThereNewVersion 365 | } 366 | 367 | function getSketchVersionNumber() { 368 | 369 | const version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] 370 | var versionNumber = version.stringByReplacingOccurrencesOfString_withString(".", "") + "" 371 | while(versionNumber.length != 3) { 372 | versionNumber += "0" 373 | } 374 | return parseInt(versionNumber) 375 | } 376 | 377 | 378 | //-------------------------------------- 379 | // JSON 380 | //-------------------------------------- 381 | 382 | 383 | function getJSONFromFile(filePath) { 384 | 385 | var data = [NSData dataWithContentsOfFile: filePath] 386 | return [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil] 387 | } 388 | 389 | function getJSONFromURL(url) { 390 | 391 | var request = [NSURLRequest requestWithURL: [NSURL URLWithString:url]], 392 | response = [NSURLConnection sendSynchronousRequest: request returningResponse: nil error: nil], 393 | responseObj = [NSJSONSerialization JSONObjectWithData: response options: nil error: nil] 394 | return responseObj 395 | } 396 | 397 | --------------------------------------------------------------------------------