diff --git a/.appcast.xml b/.appcast.xml index b157dc1..659116f 100644 --- a/.appcast.xml +++ b/.appcast.xml @@ -5,6 +5,11 @@ https://raw.githubusercontent.com/DWilliames/paddy-sketch-plugin/master/.appcast.xml Automated padding, spacing and alignment for your Sketch layers en + + Version 1.0.3 + More bug fixes + + Version 1.0.2 More bug fixes diff --git a/Paddy.sketchplugin/Contents/Sketch/layers.js b/Paddy.sketchplugin/Contents/Sketch/layers.js index 08573be..0a633f1 100644 --- a/Paddy.sketchplugin/Contents/Sketch/layers.js +++ b/Paddy.sketchplugin/Contents/Sketch/layers.js @@ -22,14 +22,16 @@ function getContainerFrameForBGLayer(bg) { return true }) - // takeIntoAccountStackViews = true + // Get the rect for each layer, only if it exists + var frames = [] - var frames = validLayers.map(function(layer) { - return rectForLayer(layer) + validLayers.forEach(function(layer) { + var rect = rectForLayer(layer, true) + if (rect) { + frames.push(rect) + } }) - // takeIntoAccountStackViews = false - return MSRect.rectWithUnionOfRects(frames) } @@ -39,9 +41,33 @@ var takeIntoAccountStackViews = true // Return the rect for a layer as an MSRect -function rectForLayer(layer) { - if (!takeIntoAccountStackViews || !layerIsStackView(layer)) { - return MSRect.rectWithRect(layer.frameForTransforms()) +function rectForLayer(layer, ignoreWithPrefix) { + if (!layer || (ignoreWithPrefix && layer.name().startsWith('-'))) { + return + } + + if (!alCommand || !takeIntoAccountStackViews || !layerIsStackView(layer)) { + + if (layer.isMemberOfClass(MSLayerGroup) && ignoreWithPrefix) { + + var frames = [] + + layer.layers().forEach(function(layer) { + var rect = rectForLayer(layer, true) + + if (rect) { + frames.push(rect) + } + }) + + var rect = MSRect.rectWithUnionOfRects(frames) + rect.x = rect.x() + layer.frame().x() + rect.y = rect.y() + layer.frame().y() + + return rect + } else { + return MSRect.rectWithRect(layer.frameForTransforms()) + } } // Calculate based on stack view @@ -64,7 +90,7 @@ function rectForLayer(layer) { // Map each layer to its rect var rects = layers.map(function(layer) { - return rectForLayer(layer) + return rectForLayer(layer, ignoreWithPrefix) }) // Sort the layers @@ -113,7 +139,7 @@ function getBackgroundForLayer(layer) { var layers = layer.parentGroup() ? layer.parentGroup().layers() : null // If it's a group or Artboard; check it's children - if (layer.isMemberOfClass(MSLayerGroup) || layer.isMemberOfClass(MSArtboardGroup) || layer.isMemberOfClass(MSSymbolMaster)) { + if (layer.isMemberOfClass(MSLayerGroup) || layer.isMemberOfClass(MSArtboardGroup) || layer.isMemberOfClass(MSSymbolMaster) || layer.isMemberOfClass(MSPage)) { layers = layer.layers() } else if (layer.isMemberOfClass(MSSymbolInstance)) { // return backgroundLayerForSymbol(layer) @@ -198,6 +224,10 @@ function containsLayerID(array, element) { function dependentLayersOfLayerIgnoringLayers(layer, objectIDsToIgnore) { + if (!layer) { + return + } + if (!objectIDsToIgnore) { objectIDsToIgnore = [] } @@ -300,10 +330,10 @@ function buildTreeMap(layers) { layers.forEach(function(layer) { fullDepthMap.push(layer) - // Add all it's children, if the layer was a group - if (layer.isMemberOfClass(MSLayerGroup)) { - fullDepthMap = fullDepthMap.concat(getAllChildrenForGroup(layer)) - } + // // Add all it's children, if the layer was a group + // if (layer.isMemberOfClass(MSLayerGroup)) { + // fullDepthMap = fullDepthMap.concat(getAllChildrenForGroup(layer)) + // } var parent = layer.parentGroup() while(parent) { diff --git a/Paddy.sketchplugin/Contents/Sketch/main.js b/Paddy.sketchplugin/Contents/Sketch/main.js index 737343d..c1e1c82 100644 --- a/Paddy.sketchplugin/Contents/Sketch/main.js +++ b/Paddy.sketchplugin/Contents/Sketch/main.js @@ -11,7 +11,10 @@ var selection, document, plugin, app, iconImage function onSetUp(context) { document = context.document plugin = context.plugin - coscript.setShouldKeepAround(true) + + if (PERSISTENT) { + coscript.setShouldKeepAround(true) + } } @@ -19,8 +22,15 @@ function onSetUp(context) { // Plugin command handlers // **************************** +function autoApplyPadding(context) { + applyPadding(context, false) +} -function applyPadding(context) { +function promptToApplyPadding(context) { + applyPadding(context, true) +} + +function applyPadding(context, promptUser) { log('RUN PLUGIN') document = context.document @@ -48,22 +58,31 @@ function applyPadding(context) { log('Existing padding BG layer', paddingBG) - var currentPadding = paddingBG ? getPaddingFromLayer(paddingBG) : defaultPadding() - var currentPaddingString = paddingToString(currentPadding) - log('First padding from selection is...', currentPaddingString) + var padding = defaultPadding() - var paddingString = context.api().getStringFromUser('Padding', currentPaddingString) - if (paddingString == nil) { - log('User cancelled submitting a padding string') - return + if (promptUser) { + var currentPadding = paddingBG ? getPaddingFromLayer(paddingBG) : defaultPadding() + var currentPaddingString = paddingToString(currentPadding) + log('First padding from selection is...', currentPaddingString) + + var paddingString = context.api().getStringFromUser('Padding', currentPaddingString) + if (paddingString == nil) { + log('User cancelled submitting a padding string') + return + } + + padding = paddingFromString(paddingString) } - var padding = paddingFromString(paddingString) + log('Padding: ', JSON.stringify(padding)) // Create new Background layers if necessary uniqueLayers.forEach(function(layer) { var bg = getBackgroundForLayer(layer) + + var layerPadding = padding + log('BG layer for: ' + layer.name(), bg) if (!bg) { // Create a new background if it doesn't have one @@ -111,9 +130,12 @@ function applyPadding(context) { parent.layerDidEndResize() } + + } else if (!promptUser) { + layerPadding = getAssumedPaddingForBackgroundLayer(bg) } - savePaddingToLayer(padding, bg) + savePaddingToLayer(layerPadding, bg) }) @@ -193,36 +215,66 @@ function textChanged(context) { // Store the properties of a layer when it is initially selected // Then we can see if they changed once the user deselects everything var initialSelectedProps = {} +var initialParentProps = {} + +// Layers that we 'will' need to update next time +var layers = [] // Keep this here, so that next time the selection is nothing, we can tell which layers need updating function selectionChanged(context) { startBenchmark() + // Only include layers that had properties change + // Particularly if their frame or position changed + // Only run if nothing is now selected if (context.actionContext.newSelection.length > 0) { + var previouslySelectedParentProps = initialParentProps + initialSelectedProps = {} + initialParentProps = {} + layers = [] context.actionContext.newSelection.forEach(function(layer) { - if (layer.isMemberOfClass(MSLayerGroup)) { - var frame = layer.frame() - var size = frame.size() - var origin = frame.origin() - - // Group - initialSelectedProps[layer.objectID()] = { - layer: layer, - width: size.width, - height: size.height, - x: origin.x, - y: origin.y, - name: layer.name(), - parent: layer.parentGroup() + var frame = layer.frame() + var size = frame.size() + var origin = frame.origin() + + var props = { + layer: layer, + frame: rectForLayer(layer), + name: layer.name(), + parent: layer.parentGroup() + } + + if (layer.isMemberOfClass(MSSymbolInstance)) { + props.overrides = layer.overrides() + } + + initialSelectedProps[layer.objectID()] = props + + if (layer.parentGroup()) { + var parent = layer.parentGroup() + + var parentLayers = [] + parent.layers().forEach(function(layer) { + parentLayers.push(layer) + }) + + initialParentProps[parent.objectID()] = { + children: parentLayers, + name: parent.name() } - } else if (layer.isMemberOfClass(MSSymbolInstance)) { - initialSelectedProps[layer.objectID()] = { - layer: layer, - overrides: layer.overrides() + + // Check if siblings have been added + var previousProps = previouslySelectedParentProps[parent.objectID()] + if (previousProps) { + + if (previousProps.children != props.siblings) { + // PROBABLY duplicated a layer + layers.push(parent) + } } } }) @@ -235,62 +287,55 @@ function selectionChanged(context) { // Update the padding for every layer that was previously selected log('Update every layer that WAS selected', context.actionContext.oldSelection) - // All layers that are 'unique' in the sense that they are unique - // within a set of sibling layers. - // Therefore, there shouldn't be more than one sibling. - var uniqueLayers = [] - context.actionContext.oldSelection.forEach(function(layer) { - // Ignore unique siblings, if it is a Symbol instance, or a layer group - if (layer.isMemberOfClass(MSSymbolInstance)) { - // Only add a symbol, if it actually changed props - var layerProps = initialSelectedProps[layer.objectID()] - if (layerProps) { - if (layerProps.overrides != layer.overrides()) { - uniqueLayers.push(layer) - } - } else { - uniqueLayers.push(layer) - } + context.actionContext.oldSelection.forEach(function(layer) { - } else if (layer.isMemberOfClass(MSLayerGroup)) { - // Only add a group, if it actually changed props - var layerProps = initialSelectedProps[layer.objectID()] + var layerProps = initialSelectedProps[layer.objectID()] - if (layerProps) { - var name = layerProps.name - var frame = layer.frame() + if (!layerProps) { + log(2, 'Layer wasn\'t previously selected') + return + } - var sameWidth = (layerProps.width == frame.size().width) - var sameHeight = (layerProps.height == frame.size().height) + var frame = rectForLayer(layer) + var previousFrame = layerProps['frame'] - var sameOrigin = (layerProps.x == frame.origin().x && layerProps.y == frame.origin().y) + if (layerProps['parent'] && !layer.parentGroup()) { + // Doesn't have a parent anymore... must've been deleted + log(2, 'Do not have a parent anymore', layer) + layers.push(layerProps.parent) + } else if (!CGSizeEqualToSize(frame.size(), previousFrame.size())) { + log(2, 'Changed frame size', layer) + layers.push(layer) - if (layerProps.parent && !layer.parentGroup()) { - // Doesn't have a parent anymore... must've been deleted - uniqueLayers.push(layerProps.parent) - } else if (name != layer.name() || !(sameHeight && sameWidth)) { - // Name or sizing changed - uniqueLayers.push(layer) - } else if (!sameOrigin) { - // Origin moved – then it's parent mmay need to update - uniqueLayers.push(layer.parentGroup()) - } else { - // Props haven't changed - } - } else { - // Layer has no previous props - uniqueLayers.push(layer) + // Add all it's children, if the layer was a group + if (layer.isMemberOfClass(MSLayerGroup)) { + layers = layers.concat(getAllChildrenForGroup(layer)) } - - } else if (!doesArrayContainSibling(uniqueLayers, layer)) { - uniqueLayers.push(layer) + } else if (!CGPointEqualToPoint(frame.origin(), previousFrame.origin())) { + log(2, 'Frame changed position', layer) + layers.push(layer.parentGroup()) + } else if (layerProps['name'] != layer.name()) { + log(2, 'Changed name', layer) + layers.push(layer) + } else if (layer.isMemberOfClass(MSSymbolInstance) && layerProps['overrides'] != layer.overrides()) { + log(2, 'Changed overrides', layer) + // Ignore unique siblings, if it is a Symbol instance, or a layer group + // Only add a symbol, if it actually changed props + layers.push(layer) + } else { + log(2, 'Layer did not change', layer) } }) + if (layers.length == 0) { + endBenchmark() + return + } + // Build a tree map of layers that need updating // This includes all parent layer groups - var treeMap = buildTreeMap(uniqueLayers) + var treeMap = buildTreeMap(layers) treeMap.forEach(function(layer){ updatePaddingAndSpacingForLayer(layer) }) @@ -303,7 +348,7 @@ function selectionChanged(context) { * Update all padding for Background layers relevant to a layer */ function updatePaddingAndSpacingForLayer(layer) { - if (!layer || layer.isMemberOfClass(MSPage)) return + if (!layer) return log('Updating for layer: ' + layer.name(), layer) // GROUPS = Spacing @@ -369,6 +414,7 @@ function updatePaddingAndSpacingForLayer(layer) { * based on the padding stored in its name */ function updatePaddingForLayerBG(bg) { + log(1, 'Updating padding with BG', bg) if (!bg) return var padding = getPaddingFromLayer(bg) diff --git a/Paddy.sketchplugin/Contents/Sketch/manifest.json b/Paddy.sketchplugin/Contents/Sketch/manifest.json index d305b7c..a0730be 100644 --- a/Paddy.sketchplugin/Contents/Sketch/manifest.json +++ b/Paddy.sketchplugin/Contents/Sketch/manifest.json @@ -4,18 +4,27 @@ "author": "David Williames", "homepage": "https://github.com/DWilliames/paddy-sketch-plugin", "appcast": "https://raw.githubusercontent.com/DWilliames/paddy-sketch-plugin/master/.appcast.xml", - "version": "1.0.2", + "version": "1.0.3", "identifier": "com.davidwilliames.sketch-plugin.paddy", "compatibleVersion": 47, "bundleVersion": 1, "commands": [ { - "name": "Padding for selection", - "identifier": "applyPadding", + "name": "Enter padding for selection", + "identifier": "promptToApplyPadding", "shortcut": "control alt p", "script": "main.js", - "handler": "applyPadding", - "description": "Apply padding to the currently selected layers", + "handler": "promptToApplyPadding", + "description": "Enter padding amount to the currently selected layers", + "icon": "Icons/padding.png" + }, + { + "name": "Imply padding for selection", + "identifier": "autoApplyPadding", + "shortcut": "command p", + "script": "main.js", + "handler": "autoApplyPadding", + "description": "Apply implied padding to the currently selected layers", "icon": "Icons/padding.png" }, { @@ -39,7 +48,8 @@ ], "menu": { "items": [ - "applyPadding", + "promptToApplyPadding", + "autoApplyPadding", "-", "applySpacing" ] diff --git a/Paddy.sketchplugin/Contents/Sketch/padding.js b/Paddy.sketchplugin/Contents/Sketch/padding.js index 53f909a..6f81067 100644 --- a/Paddy.sketchplugin/Contents/Sketch/padding.js +++ b/Paddy.sketchplugin/Contents/Sketch/padding.js @@ -50,6 +50,7 @@ function getPaddingFromLayer(layer) { function savePaddingToLayer(padding, layer) { if (!canLayerHavePadding(layer)) return + var name = layer.name().split('[')[0] if (!padding) { @@ -81,7 +82,7 @@ function layerHasPadding(layer) { function layerPaddingString(layer) { if (!layer) return - var regex = /\[(.*)\]/g + var regex = /.*\[(.*)\]/g return firstRegexMatch(regex, layer.name()) } @@ -303,3 +304,17 @@ function applyPaddingToLayerWithContainerRect(padding, layer, containerRect) { layer.layerDidEndResize() } + + +function getAssumedPaddingForBackgroundLayer(layer) { + var containerRect = getContainerFrameForBGLayer(layer) + + var padding = {} + + padding.left = containerRect.x() - layer.frame().x() + padding.right = (layer.frame().x() + layer.frame().width()) - (containerRect.x() + containerRect.width()) + padding.top = containerRect.y() - layer.frame().y() + padding.bottom = (layer.frame().y() + layer.frame().height()) - (containerRect.y() + containerRect.height()) + + return padding +} diff --git a/Paddy.sketchplugin/Contents/Sketch/spacing.js b/Paddy.sketchplugin/Contents/Sketch/spacing.js index 6df758f..78083ba 100644 --- a/Paddy.sketchplugin/Contents/Sketch/spacing.js +++ b/Paddy.sketchplugin/Contents/Sketch/spacing.js @@ -1,11 +1,12 @@ @import 'utils.js' /* -Example spacing object: +Example spacing object: [20v] { - vertical: 10, - horizontal: 20 + layout: vertical, + space: 20, + align: [c, m] } [left] @@ -75,7 +76,7 @@ function layerHasSpacing(layer) { function layerSpacingString(layer) { if (!layer) return - var regex = /\[(.*)\]/g + var regex = /.*\[(.*)\]/g return firstRegexMatch(regex, layer.name()) } @@ -105,7 +106,7 @@ function spacingToString(spacing) { spacingString += ' ' } - spacingString += spacing.align + spacingString += spacing.align.join(' ') } return spacingString @@ -121,63 +122,37 @@ function spacingFromString(string) { if (!string || string == '') return null - var values = string.split(' ') + var spacing = {} - // Possibly alignment first... - var alignment = simplifyAlignment(values[0]) - if (alignment) { - return spacing = { - align: alignment - } - } + var values = string.split(' ') + // Check if first value is 'spacing' var spacingLayout = values[0] var layout = spacingLayout.slice(-1) - if (!(layout == 'v' || layout == 'h')) { - return - } + if (layout == 'v' || layout == 'h') { + // Spacing exists + var space = spacingLayout.substring(0, spacingLayout.length - 1) - var space = spacingLayout.substring(0, spacingLayout.length - 1) + log(1, 'Layout', layout) + log(1, 'Spacing', space) - log(1, 'Layout', layout) - log(1, 'Spacing', space) + spacing.layout = layout + spacing.space = space - var spacing = { - layout: layout, - space: space, + values.shift() // Remove the first element } - log(1, 'Values', values) - log(1, 'Values count', values.length) - - if (values.length > 1) { - var alignment = values[1] - - if (alignment == "left") { - alignment = "l" - } - if (alignment == "right") { - alignment = "r" - } - if (alignment == "top") { - alignment = "t" - } - if (alignment == "bottom") { - alignment = "b" - } - if (alignment == "center" || alignment == "centre" || alignment == "h" || alignment == "horizontally") { - alignment = "c" - } - if (alignment == "middle" || alignment == "v" || alignment == "vertically") { - alignment = "m" - } - - log(1, 'Alignment', alignment) - + var alignments = [] + values.forEach(function(value) { + var alignment = simplifyAlignment(value) if (alignment) { - spacing.align = alignment + alignments.push(alignment) } + }) + + if (alignments.length > 0) { + spacing.align = alignments } return spacing @@ -244,6 +219,11 @@ function applySpacingToGroup(spacing, groupLayer) { return true }) + if (layers.length == 0) { + log(2, 'No layers to space') + return + } + log(2, 'Will space layers', layers.map(function(layer) { return layer.name() + "\n" })) @@ -256,9 +236,10 @@ function applySpacingToGroup(spacing, groupLayer) { } }) - var previous = sortedLayers[0] + var previous = sortedLayers[0] var previousFrame = frameForLayer(previous) + var minX = previousFrame.minX() var minY = previousFrame.minY() var maxX = previousFrame.maxX() @@ -269,7 +250,6 @@ function applySpacingToGroup(spacing, groupLayer) { var previousFrame = frameForLayer(previous) var frame = frameForLayer(layer) - // The amount to offset the layer var x = 0 var y = 0 @@ -304,18 +284,18 @@ function applySpacingToGroup(spacing, groupLayer) { })) // if (spacing.layout == "v") { layers.forEach(function(layer) { - if (spacing.align == "l") { + if (arrayContains(spacing.align, 'l')) { layer.frame().setX(minX) - } else if (spacing.align == "r") { + } if (arrayContains(spacing.align, 'r')) { layer.frame().setMaxX(maxX) - } else if (spacing.align == "c") { + } if (arrayContains(spacing.align, 'c')) { var mid = minX + (maxX - minX) / 2.0 layer.frame().setMidX(mid) - } else if (spacing.align == "t") { + } if (arrayContains(spacing.align, 't')) { layer.frame().setY(minY) - } else if (spacing.align == "b") { + } if (arrayContains(spacing.align, 'b')) { layer.frame().setMaxY(maxY) - } else if (spacing.align == "m") { + } if (arrayContains(spacing.align, 'm')) { var mid = minY + (maxY - minY) / 2.0 layer.frame().setMidY(mid) } @@ -333,7 +313,15 @@ function applySpacingToGroup(spacing, groupLayer) { groupLayer.layerDidEndResize() } + +function arrayContains(array, value) { + return array.indexOf(value) != -1 +} + + function frameForLayer(layer) { + if (!layer) return + return MSRect.rectWithRect(layer.frameForTransforms()) } diff --git a/Paddy.sketchplugin/Contents/Sketch/symbols.js b/Paddy.sketchplugin/Contents/Sketch/symbols.js index 9dd42cb..498a271 100644 --- a/Paddy.sketchplugin/Contents/Sketch/symbols.js +++ b/Paddy.sketchplugin/Contents/Sketch/symbols.js @@ -91,7 +91,10 @@ function updateForSymbolInstance(symbol) { var id = layer.objectID() if (!(layer.hasFixedLeft() || layer.hasFixedRight())) { - if (layer.isMemberOfClass(MSTextLayer) && layer.textAlignment() == 1) { + if (layer.textAlignment() == 1) { + layer.hasFixedRight = true + } else if (layer.textAlignment() == 2) { + layer.hasFixedLeft = true layer.hasFixedRight = true } else { layer.hasFixedLeft = true diff --git a/Paddy.sketchplugin/Contents/Sketch/utils.js b/Paddy.sketchplugin/Contents/Sketch/utils.js index 8b894cb..d27794e 100644 --- a/Paddy.sketchplugin/Contents/Sketch/utils.js +++ b/Paddy.sketchplugin/Contents/Sketch/utils.js @@ -1,7 +1,8 @@ // Whether to show logging or not -var DEBUG = false -var TIMER = false +var DEBUG = false // FALSE for prod +var TIMER = false // FALSE for prod +var PERSISTENT = true // TRUE for prod /** * Log a bunch of values diff --git a/README.md b/README.md index 9faef2f..3c95145 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ There are some similar plugins out there that do some of the features of this pl * **Automatic:** Most other plugins require a keyboard shortcut for them to be applied, or re-applied. This is all done automatically as you manipulate your layers; simply deselect everything, and will be updated. * **Visible properties:** It's easy to scan your layer list to see what padding / spacing is applied to your layers and groups; without the need to select them individually. This is because all the properties are set via the layer's name. -* **No special data manipulation:** This simply resizes and moves your layers to optimal positions – it doesn't turn your groups into a special 'Stack group' or something; so other people or programs can read the Sketch file perfectly fine, without the need for this plugin too. +* **No special data manipulation:** This simply resizes and moves your layers to optimal positions – it doesn't turn your groups into a special 'Stack group' or something; so other people or programs such as Zeplin, can read the Sketch file perfectly fine, without the need for this plugin too. * **Lightweight:** It just does a couple of core things – it's not bloated with a bunch of unused features. --- @@ -78,7 +78,9 @@ Examples: * bottom padding is `15` * left padding is `20` -Run the plugin command **'Apply padding to selection'** or **`Control + Alt + p`** to bring up an input field, to save padding to all your selected layers easier. +Run the plugin command **'Enter padding for selection'** or **`Control + Alt + p`** to bring up an input field, to save padding to all your selected layers easier. + +If you don't want to be prompted to enter the specified padding, and instead want it to be automatically inferred from what it already is – run the command **'Imply padding for selection'** or **`Command + p`**. ![Padding example](.images/GIFs/padding.gif) @@ -159,6 +161,12 @@ Run the plugin command **'Apply spacing to selection'** or **`Control + Alt + Co ![Spacing example](.images/GIFs/spacing.gif) +### Spacing in symbols + +Let's talk about spacing in symbols. You can layout your elements with 'spacing' within your Master symbol artboard, if that makes things easier... however, it will not maintain the spacing after overriding the symbol. This is simply not possible without doing some weird data manipulation, which would break viewing it in other programs such as Zeplin. + +If however, you do not care about being compatible with programs such as Zeplin; you can you [Anima's AutoLayout plugin](https://animaapp.github.io) with 'Stack groups'. Paddy will take Stack groups into account when resizing a symbol; even with its overrides. Which in most cases, is probably what you want to do when applying spacing within your Symbol. + --- Auto alignment @@ -186,6 +194,7 @@ Similar to spacing / padding, it is by naming the group something specific, with ![Alignment with spacing example](.images/GIFs/spacing-alignment.gif) +You can even specify more than one alignment value, if you wish. For example: **`[c m]`** will align the layers in the middle and center. Or **`[t c]`** to align on the top + center. ---