From fa7bfe4aa82590e446b3b2db8a36a6a5aaddbbae Mon Sep 17 00:00:00 2001 From: Aby N Date: Sun, 13 Nov 2016 15:23:12 +0800 Subject: [PATCH 01/10] Fixes for Sketch 41 --- UserFlows.sketchplugin/Contents/Sketch/common.js | 5 +++-- UserFlows.sketchplugin/Contents/Sketch/userflows.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/common.js b/UserFlows.sketchplugin/Contents/Sketch/common.js index 0dbb363..5fa7e5f 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/common.js +++ b/UserFlows.sketchplugin/Contents/Sketch/common.js @@ -605,13 +605,14 @@ function exportLayerToPath(layer, path, scale, format, suffix) { if(sketchVersionNumber >= 350) { var rect = layer.absoluteRect().rect(), - slice = [MSExportRequest requestWithRect:rect scale:scale], + slice = MSExportRequest.alloc().init(), layerName = layer.name() + ((typeof suffix !== 'undefined') ? suffix : ""), format = (typeof format !== 'undefined') ? format : "png"; + slice.setRect(rect) + slice.setScale(scale) slice.setShouldTrim(0) slice.setSaveForWeb(1) - slice.configureForLayer(layer) slice.setName(layerName) slice.setFormat(format) doc.saveArtboardOrSlice_toFile(slice, path) diff --git a/UserFlows.sketchplugin/Contents/Sketch/userflows.js b/UserFlows.sketchplugin/Contents/Sketch/userflows.js index 44aa69e..7a007f6 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/userflows.js +++ b/UserFlows.sketchplugin/Contents/Sketch/userflows.js @@ -12,7 +12,7 @@ var userDefaults = { } var scaleOptions = ['1x', '2x']; -var formatOptions = [NSArray arrayWithObjects:"PNG", "JPG", "TIFF", nil] +var formatOptions = [NSArray arrayWithObjects:"PDF", "PNG", "JPG", "TIFF", nil] iconName = "icon.png" var askForFlowDetails = function() { From d43311cc741c741cf6cee37ecec0e13a218838a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Thu, 8 Dec 2016 11:43:17 -0500 Subject: [PATCH 02/10] basic linking and unlinking between layers --- .../Contents/Sketch/script.js | 131 +++++++++++------- 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index a0d8cf5..41fba76 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -19,31 +19,35 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var destArtboard, linkLayer; + var dest, linkLayer; if (selection.count() != 2) { validSelection = false; } else { if (selection.firstObject().className() == "MSArtboardGroup") { - destArtboard = selection.firstObject(); + dest = selection.firstObject(); linkLayer = selection.lastObject(); } else if(selection.lastObject().className() == "MSArtboardGroup") { - destArtboard = selection.lastObject(); + dest = selection.lastObject(); linkLayer = selection.firstObject(); } + else { + dest = selection.firstObject(); + linkLayer = selection.lastObject(); + } - if (!destArtboard || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == destArtboard) { + if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == dest) { validSelection = false; } } if (!validSelection) { - showAlert("Invalid selection", "Select a layer or group to define as a Link, select the destination artboard, then run this command again."); + showAlert("Invalid selection", "Select a layer or group to define as a Link, select the destination artboard or layer, then run this command again."); return; } - context.command.setValue_forKey_onLayer_forPluginIdentifier(destArtboard.objectID(), "destinationArtboardID", linkLayer, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(dest.objectID(), "destinationID", linkLayer, kPluginDomain); var doc = context.document; var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -51,7 +55,7 @@ var defineLink = function(context) { if (showingConnections == 1) { redrawConnections(context); } else { - doc.showMessage("Link defined: " + linkLayer.name() + " → " + destArtboard.name()); + doc.showMessage("Link defined: " + linkLayer.name() + " → " + dest.name()); } } @@ -65,11 +69,11 @@ var removeLink = function(context) { } var loop = context.selection.objectEnumerator(), - linkLayer, destinationArtboardID; + linkLayer, destinationID; while (linkLayer = loop.nextObject()) { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); - if (!destinationArtboardID) { continue; } - context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { continue; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); } var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -151,7 +155,7 @@ var addCondition = function(context) { settingsWindow.addAccessoryView(conditionView); conditionFields.push(conditionField); conditionChecks.push(checkbox); - conditionLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", conditionLayer.parentGroup(), kPluginDomain) || 0; + conditionLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", conditionLayer.parentGroup(), kPluginDomain) || 0; conditionLinks.push(conditionLink); } } @@ -159,7 +163,7 @@ var addCondition = function(context) { predicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isElse != nil", kPluginDomain); var elseLabel = currentArtboard.children().filteredArrayUsingPredicate(predicate).firstObject(); if (elseLabel) { - elseLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", elseLabel.parentGroup(), kPluginDomain) || 0; + elseLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", elseLabel.parentGroup(), kPluginDomain) || 0; } } @@ -269,7 +273,7 @@ var addCondition = function(context) { conditionLink = conditionLinks[i]; if (conditionLink != 0) { - context.command.setValue_forKey_onLayer_forPluginIdentifier(conditionLink, "destinationArtboardID", conditionGroup, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(conditionLink, "destinationID", conditionGroup, kPluginDomain); } } @@ -290,18 +294,18 @@ var addCondition = function(context) { } -var gotoDestinationArtboard = function(context) { +var gotoDestination = function(context) { parseContext(context); var linkLayer = context.selection.firstObject(), validSelection = true, - destinationArtboardID; + destinationID; if (!linkLayer) { validSelection = false; } else { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); - if (!destinationArtboardID) { + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { validSelection = false; } } @@ -312,8 +316,8 @@ var gotoDestinationArtboard = function(context) { } var doc = context.document, - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - if (destinationArtboard) { + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + if (destination) { var cRect = doc.currentView().visibleContentRect(), contentRect = { x : cRect.origin.x, @@ -331,8 +335,30 @@ var gotoDestinationArtboard = function(context) { context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); - doc.currentView().centerRect(destinationArtboard.absoluteRect().rect()); - destinationArtboard.select_byExpandingSelection(true, false); + doc.currentView().centerRect(destination.absoluteRect().rect()); + destination.select_byExpandingSelection(true, false); + }else{ + destination = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() + + var cRect = doc.currentView().visibleContentRect(), + contentRect = { + x : cRect.origin.x, + y : cRect.origin.y, + width : cRect.size.width, + height : cRect.size.height, + linkLayerID : linkLayer.objectID() + }, + rects = context.command.valueForKey_onDocument_forPluginIdentifier("contentRectsHistory", doc.documentData(), kPluginDomain); + + if (!rects) { + rects = NSArray.array(); + } + rects = rects.arrayByAddingObject(contentRect); + + context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); + + doc.currentView().centerRect(destination.absoluteRect().rect()); + destination.select_byExpandingSelection(true, false); } } @@ -374,7 +400,7 @@ var generateFlow = function(context) { settingsWindow.addTextLabelWithValue("Start from:"); - linkLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain); + linkLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationID != nil", kPluginDomain); var linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayerPredicate); if (linkLayers.count() == 0) { @@ -460,7 +486,7 @@ var generateFlow = function(context) { artboardsToExport = [initialArtboard], screenShadowColor = MSImmutableColor.colorWithSVGString("#00000").newMutableCounterpart(), tempFolderURL = NSFileManager.defaultManager().URLsForDirectory_inDomains(NSCachesDirectory, NSUserDomainMask).lastObject().URLByAppendingPathComponent(kPluginDomain), - artboard, artboardID, linkLayers, linkLayersCount, destinationArtboard, destinationArtboardID, linkLayer, screenLayer, exportRequest, exportURL, screenShadow, connection, artboardNameLabel, primaryTextColor, secondaryTextColor, flowBackgroundColor, artboardIsConditional, isCondition, destinationArtboardIsConditional; + artboard, artboardID, linkLayers, linkLayersCount, destination, destinationID, linkLayer, screenLayer, exportRequest, exportURL, screenShadow, connection, artboardNameLabel, primaryTextColor, secondaryTextColor, flowBackgroundColor, artboardIsConditional, isCondition, destinationIsConditional; context.command.setValue_forKey_onLayer_forPluginIdentifier(initialArtboard.objectID(), "homeScreenID", doc.currentPage(), kPluginDomain); screenShadowColor.setAlpha(.2); @@ -539,25 +565,25 @@ var generateFlow = function(context) { linkLayersCount = linkLayers.count(); for (var i=0; i < linkLayersCount; i++) { linkLayer = linkLayers.objectAtIndex(i); - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - destinationArtboardIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, destinationArtboard, kPluginDomain) || 0; - if (destinationArtboard) { + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + destinationIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, destination, kPluginDomain) || 0; + if (destination) { connection = { linkRect : linkLayer.absoluteRect().rect(), linkIsCondition : isCondition, - destinationIsConditional : destinationArtboardIsConditional, + destinationIsConditional : destinationIsConditional, dropPoint : { - x : destinationArtboard.absoluteRect().x() - (10*exportScale), - y : destinationArtboard.absoluteRect().y() - (10*exportScale) + x : destination.absoluteRect().x() - (10*exportScale), + y : destination.absoluteRect().y() - (10*exportScale) } } connections.push(connection); - artboardsToExport.push(destinationArtboard); + artboardsToExport.push(destination); } } @@ -729,6 +755,7 @@ var showConnections = function(context) { } var redrawConnections = function(context) { + var doc = context.document || context.actionContext.document; var selectedLayers = doc.findSelectedLayers(); var connectionsGroup = getConnectionsGroupInPage(doc.currentPage()); @@ -737,31 +764,33 @@ var redrawConnections = function(context) { connectionsGroup.removeFromParent(); } - var linkLayersPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain), + var linkLayersPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationID != nil", kPluginDomain), linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayersPredicate), loop = linkLayers.objectEnumerator(), connections = [], - linkLayer, destinationArtboardID, destinationArtboard, isCondition; + linkLayer, destinationID, destination, isCondition; while (linkLayer = loop.nextObject()) { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - if (destinationArtboard) { - - connection = { - linkRect : linkLayer.absoluteRect().rect(), - linkIsCondition : isCondition, - dropPoint : { - x : destinationArtboard.absoluteRect().x() - 10, - y : destinationArtboard.absoluteRect().y() - } - } - connections.push(connection); + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + + if (!destination) { + destination = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() + } + + connection = { + linkRect : linkLayer.absoluteRect().rect(), + linkIsCondition : isCondition, + dropPoint : { + x : destination.absoluteRect().x() - 10, + y : destination.absoluteRect().y() + } } + connections.push(connection); } var connectionLayers = MSLayerArray.arrayWithLayers(drawConnections(connections, doc.currentPage(), 1)); @@ -788,13 +817,13 @@ var drawConnections = function(connections, parent, exportScale) { hitAreaBorderColor = MSImmutableColor.colorWithSVGString(flowIndicatorColor).newMutableCounterpart(), arrowRotation = 0, arrowOffsetX = 0, - path, hitAreaLayer, linkRect, dropPoint, hitAreaBorder, startPoint, controlPoint1, controlPoint1Offset, controlPoint2OffsetX, controlPoint2OffsetY, linePath, lineLayer, destinationArtboardIsConditional; + path, hitAreaLayer, linkRect, dropPoint, hitAreaBorder, startPoint, controlPoint1, controlPoint1Offset, controlPoint2OffsetX, controlPoint2OffsetY, linePath, lineLayer, destinationIsConditional; hitAreaColor.setAlpha(0); for (var i=0; i < connectionsCount; i++) { connection = connections[i]; linkRect = connection.linkRect; - destinationArtboardIsConditional = connection.destinationIsConditional == 1; + destinationIsConditional = connection.destinationIsConditional == 1; if (linkRect.size.width < minimumTapArea) { linkRect = NSInsetRect(linkRect, (linkRect.size.width-minimumTapArea)/2, 0); } @@ -814,7 +843,7 @@ var drawConnections = function(connections, parent, exportScale) { connectionLayers.push(hitAreaLayer); } - dropPoint = destinationArtboardIsConditional ? NSMakePoint(connection.dropPoint.x+(5*exportScale), connection.dropPoint.y + (10*exportScale)) : NSMakePoint(connection.dropPoint.x, connection.dropPoint.y); + dropPoint = destinationIsConditional ? NSMakePoint(connection.dropPoint.x+(5*exportScale), connection.dropPoint.y + (10*exportScale)) : NSMakePoint(connection.dropPoint.x, connection.dropPoint.y); if (dropPoint.x < CGRectGetMinX(linkRect)) { dropPoint = NSMakePoint(dropPoint.x + 18, dropPoint.y - (30/exportScale) ); arrowRotation = 90; @@ -1172,5 +1201,5 @@ var logEvent = function(event, props) { base64 = json.base64EncodedStringWithOptions(0), url = NSURL.URLWithString(NSString.stringWithFormat("https://api.mixpanel.com/track/?data=%@&ip=1", base64)); - if (url) NSURLSession.sharedSession().dataTaskWithURL(url).resume(); + //if (url) NSURLSession.sharedSession().dataTaskWithURL(url).resume(); } From c572b56cd3d8b482cc7fbbcad6dfac7f11e51edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Thu, 8 Dec 2016 15:56:00 -0500 Subject: [PATCH 03/10] start reversing links between layers --- .../Contents/Sketch/manifest.json | 12 +++++++++ .../Contents/Sketch/script.js | 26 +++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/manifest.json b/UserFlows.sketchplugin/Contents/Sketch/manifest.json index 0748358..f537c60 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/manifest.json +++ b/UserFlows.sketchplugin/Contents/Sketch/manifest.json @@ -23,6 +23,17 @@ "icon" : "icon.png", "name" : "Remove Link" }, + { + "script" : "script.js", + "shortcut" : "cmd shift i", + "handlers" : { + "run" : "reverseLink" + }, + "identifier" : "reverseLink", + "description" : "Reverse a Link.", + "icon" : "icon.png", + "name" : "Reverse Link" + }, { "script" : "script.js", "shortcut" : "cmd shift d", @@ -148,6 +159,7 @@ "items" : [ "defineLink", "removeLink", + "reverseLink", "addCondition", "-", "generateFlow", diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index 41fba76..39dc341 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -19,7 +19,9 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var dest, linkLayer; + var dest, linkLayer, reversal; + + reversal = context.reverse; if (selection.count() != 2) { validSelection = false; @@ -33,8 +35,8 @@ var defineLink = function(context) { linkLayer = selection.firstObject(); } else { - dest = selection.firstObject(); - linkLayer = selection.lastObject(); + dest = context.reverse ? selection.lastObject() : selection.firstObject(); + linkLayer = context.reverse ? selection.firstObject() : selection.lastObject(); } if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == dest) { @@ -57,7 +59,6 @@ var defineLink = function(context) { } else { doc.showMessage("Link defined: " + linkLayer.name() + " → " + dest.name()); } - } var removeLink = function(context) { @@ -70,9 +71,12 @@ var removeLink = function(context) { var loop = context.selection.objectEnumerator(), linkLayer, destinationID; + while (linkLayer = loop.nextObject()) { destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { continue; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); } @@ -85,6 +89,16 @@ var removeLink = function(context) { } } +var reverseLink = function(context) { + var currentSelection = context.selection; + + removeLink(context); + + context.reverse = true; + + defineLink(context); +} + var editArtboardDescription = function(context) { parseContext(context); @@ -290,10 +304,8 @@ var addCondition = function(context) { redrawConnections(context); } } - } - var gotoDestination = function(context) { parseContext(context); @@ -731,7 +743,6 @@ var generateFlow = function(context) { } var updateFlow = function(context) { - } var hideConnections = function(context) { @@ -1136,7 +1147,6 @@ var checkForUpdates = function(context) { var websiteURL = NSURL.URLWithString(json.valueForKey("websiteURL")); NSWorkspace.sharedWorkspace().openURL(websiteURL); } - } var showAlert = function(message, info, primaryButtonText, secondaryButtonText) { From 5aa376152b52c503520d592f319844f2854f31e0 Mon Sep 17 00:00:00 2001 From: Aby N Date: Tue, 29 Nov 2016 00:26:53 +0800 Subject: [PATCH 04/10] Published V2 --- LICENSE | 2 +- README.md | 46 +- .../Contents/Sketch/_handlers.cocoascript | 16 - .../Contents/Sketch/manifest.json | 163 ++- .../Contents/Sketch/script.js | 1137 +++++++++++++++++ 5 files changed, 1281 insertions(+), 83 deletions(-) delete mode 100644 UserFlows.sketchplugin/Contents/Sketch/_handlers.cocoascript create mode 100644 UserFlows.sketchplugin/Contents/Sketch/script.js diff --git a/LICENSE b/LICENSE index f836f91..2a42c20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Aby Nimbalkar +Copyright (c) 2017 Aby Nimbalkar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dfae6e4..24b69f6 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,8 @@ -# User Flows -A plugin for generating user walkthroughs from Artboards in [Sketch](http://www.bohemiancoding.com/sketch/). +# User Flows V2 +A plugin for generating flow diagrams from Artboards in [Sketch](http://www.bohemiancoding.com/sketch/). -## Demo -[![Video](http://silverux.com/sketchplugins/userflows/assets/video_title_1.png)](https://vimeo.com/abynim/userflows) -[Watch on Vimeo](https://vimeo.com/abynim/userflows) - -##### Example Output -![UserFlows Example](http://silverux.com/sketchplugins/userflows/assets/exampleflow.jpg) - -## Supported Versions -UserFlows is only supported in Sketch 3.3 or later. To check what version you have installed, go to `Sketch` > `About Sketch`. -![About Sketch](http://silverux.com/ig-auth/assets/sketchsquares-8a.png) - -## Installation -[Download](https://github.com/abynim/UserFlows/archive/master.zip) and extract the contents of this repository. Then double-click the UserFlows.sketchplugin bundle to install the plugin. Remember, this will only work if you are working with Sketch 3.3 or later. - -## Usage - -#### Generate Flows -1. Select Layers that you want to use as hotspots (one Layer or Group per Artboard). -2. If you don't need hotspots, select the Artboard itself. -3. Run the plugin from `Plugins` > `User Flows` > `Generate a Flow`. -![Generate Flow Dialog](http://silverux.com/sketchplugins/userflows/assets/generate_flow_dialog1.png) -4. Enter details for the flow and hit `Generate`. - -_Note: The sequence of screens in a flow is based on the `x` position of your Artboards. Sketch does not allow event-tracking (yet) so it's not possible to determine the order in which layers or Artboards were selected, although that would be ideal._ - -#### Artboard Description -1. To enter a description for an artboard, select it and go to `Plugins` > `User Flows` > `Edit Artboard Description`. -2. The description entered here will be added to the exported flow, and positioned below the artboard image. -![Description Dialog](http://silverux.com/sketchplugins/userflows/assets/artboard_description.png) - -#### Settings -To manage settings for the plugin, go to `Plugins` > `User Flows` > `Settings`. -![Settings Dialog](http://silverux.com/sketchplugins/userflows/assets/settings_dialog1.png) - -## Shortcut -Once you've installed the plugin you can trigger it using: `control` + `shift` + `f`. - -## Bugs, Features and Feedback -If you encounter bugs, or think of some awesome enhancements, please create an Issue on Github or send a message on Twitter [@abynim](http://twitter.com/abynim). +Please check the project page for installation, usage and other details: https://abynim.github.io/UserFlows --- -MIT License © Aby Nimbalkar. I'm on [Twitter](http://twitter.com/abynim) and [LinkedIn](http://tw.linkedin.com/in/abynim/). +MIT License © Aby Nimbalkar | [@abynim](http://twitter.com/abynim). diff --git a/UserFlows.sketchplugin/Contents/Sketch/_handlers.cocoascript b/UserFlows.sketchplugin/Contents/Sketch/_handlers.cocoascript deleted file mode 100644 index 6d9bb54..0000000 --- a/UserFlows.sketchplugin/Contents/Sketch/_handlers.cocoascript +++ /dev/null @@ -1,16 +0,0 @@ -@import 'userflows.js' - -var generateFlow = function(context) { - parseContext(context) - askForFlowDetails() -} - -var editSettings = function(context) { - parseContext(context) - showSettingsDialog() -} - -var editArtboardDescription = function(context) { - parseContext(context) - showArtboardDescription() -} \ No newline at end of file diff --git a/UserFlows.sketchplugin/Contents/Sketch/manifest.json b/UserFlows.sketchplugin/Contents/Sketch/manifest.json index d5755f2..eaed0e7 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/manifest.json +++ b/UserFlows.sketchplugin/Contents/Sketch/manifest.json @@ -2,47 +2,162 @@ "author" : "Aby Nimbalkar", "commands" : [ { - "script" : "_handlers.cocoascript", - "handler" : "generateFlow", - "shortcut" : "ctrl shift f", - "name" : "Generate Flow", + "script" : "script.js", + "shortcut" : "cmd shift k", + "handlers" : { + "run" : "defineLink" + }, + "identifier" : "defineLink", + "description" : "Select a layer\/group, select an artboard, then create a link between the two.", + "icon" : "icon.png", + "name" : "Create Link" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift u", + "handlers" : { + "run" : "removeLink" + }, + "identifier" : "removeLink", + "description" : "Remove a Link.", + "icon" : "icon.png", + "name" : "Remove Link" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift d", + "handlers" : { + "run" : "addCondition" + }, + "identifier" : "addCondition", + "description" : "Add or Edit a condition in the current flow.", + "icon" : "icon.png", + "name" : "Add/Edit Conditions" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift f", + "handlers" : { + "run" : "generateFlow" + }, "identifier" : "generateFlow", + "description" : "Generate a flow diagram starting from a specific artboard.", "icon" : "icon.png", - "description" : "Generate a User Flow with selected Layers and Artboards." + "name" : "Generate Flow Diagram" }, - { - "script" : "_handlers.cocoascript", - "handler" : "editArtboardDescription", - "shortcut" : "ctrl shift d", - "name" : "Edit Artboard Description", + { + "script" : "script.js", + "shortcut" : "cmd shift r", + "handlers" : { + "run" : "updateFlow" + }, + "identifier" : "updateFlow", + "description" : "Update the selected User Flow artboard.", + "icon" : "icon.png", + "name" : "Update Flow Diagram" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift 9", + "handlers" : { + "run" : "showConnections" + }, + "identifier" : "showConnections", + "description" : "Show or redraw connections between link layers and destination artboards.", + "icon" : "icon.png", + "name" : "Show or Redraw Connections" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift 0", + "handlers" : { + "run" : "hideConnections" + }, + "identifier" : "hideConnections", + "description" : "Hide connections between link layers and destination artboards.", + "icon" : "icon.png", + "name" : "Hide Connections" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift .", + "handlers" : { + "run" : "gotoDestinationArtboard" + }, + "identifier" : "gotoDestinationArtboard", + "description" : "Go to the artboard linked to this layer.", + "icon" : "icon.png", + "name" : "Go to Linked Artboard" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift ,", + "handlers" : { + "run" : "goBackToLink" + }, + "identifier" : "goBackToLink", + "description" : "Navigate back to the link layer.", + "icon" : "icon.png", + "name" : "Go Back to Link Layer" + }, + { + "script" : "script.js", + "shortcut" : "cmd shift b", + "handlers" : { + "run" : "editArtboardDescription" + }, "identifier" : "editArtboardDescription", + "description" : "Edit the description of the selected Artboard.", "icon" : "icon.png", - "description" : "Edit the description of the selected Artboard." + "name" : "Edit Artboard Description" }, - { - "script" : "_handlers.cocoascript", - "handler" : "editSettings", + { + "script" : "script.js", "shortcut" : "", "name" : "Settings", "identifier" : "editSettings", + "description" : "Edit settings for User Flows.", + "icon" : "icon.png", + "handlers" : { + "run" : "editSettings" + } + }, + { + "script" : "script.js", + "shortcut" : "", + "name" : "Keyboard Shortcuts", + "identifier" : "editShortcuts", + "description" : "Edit keyboard shortcuts for User Flows.", "icon" : "icon.png", - "description" : "Edit settings for User Flows." + "handlers" : { + "run" : "editShortcuts" + } } ], - "bundleVersion" : 1, - "identifier" : "com.silverux.sketchplugins.user-flow", - "version" : "1.0", - "homepage" : "https:\/\/github.com\/abynim\/UserFlow.sketchplugin", - "description" : "Generate user-flow diagrams from artboards in Sketch", "menu" : { "items" : [ + "defineLink", + "removeLink", + "addCondition", + "-", "generateFlow", - "editArtboardDescription", "-", - "editSettings" + "showConnections", + "hideConnections", + "-", + "gotoDestinationArtboard", + "goBackToLink", + "-", + "editSettings", + "editShortcuts" ] }, - "compatibleVersion" : 3.3, - "authorEmail" : "aby@silverux.com", + "compatibleVersion" : 4.1, + "disableCocoaScriptPreprocessor" : true, + "identifier" : "com.abynim.sketchplugins.userflows", + "version" : "2.0", + "description" : "Generate user-flow diagrams from artboards in Sketch", + "homepage" : "https:\/\/github.com\/abynim\/UserFlow.sketchplugin", + "authorEmail" : "abynimbalkar@gmail.com", "name" : "User Flows" } \ No newline at end of file diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js new file mode 100644 index 0000000..5c0074d --- /dev/null +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -0,0 +1,1137 @@ +var kPluginDomain = "com.abynim.sketchplugins.userflows"; +var kKeepOrganizedKey = "com.abynim.userflows.keepOrganized"; +var kExportScaleKey = "com.abynim.userflows.exportScale"; +var kExportFormatKey = "com.abynim.userflows.exportFormat"; +var kShowModifiedDateKey = "com.abynim.userflows.showModifiedDate"; +var kFlowIndicatorColorKey = "com.abynim.userflows.flowIndicatorColor"; +var kFlowBackgroundKey = "com.abynim.userflows.backgroundColor"; +var kMinTapAreaKey = "com.abynim.userflows.minTapArea"; +var kFullNameKey = "com.abynim.userflows.fullName"; +var kUUIDKey = "com.abynim.userflows.uuid"; +var kShowConnectionsKey = "com.abynim.userflows.showConnections"; +var kConditionalArtboardKey = "com.abynim.userflows.conditionalArtboard"; +var linkLayerPredicate; +var iconImage; + +var defineLink = function(context) { + + parseContext(context); + + var selection = context.selection; + var validSelection = true; + var destArtboard, linkLayer; + + if (selection.count() != 2) { + validSelection = false; + } else { + if (selection.firstObject().className() == "MSArtboardGroup") { + destArtboard = selection.firstObject(); + linkLayer = selection.lastObject(); + } + else if(selection.lastObject().className() == "MSArtboardGroup") { + destArtboard = selection.lastObject(); + linkLayer = selection.firstObject(); + } + + if (!destArtboard || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == destArtboard) { + validSelection = false; + } + } + + if (!validSelection) { + showAlert("Invalid selection", "Select a layer or group to define as a Link, select the destination artboard, then run this command again."); + return; + } + + context.command.setValue_forKey_onLayer_forPluginIdentifier(destArtboard.objectID(), "destinationArtboardID", linkLayer, kPluginDomain); + + var doc = context.document; + var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; + + if (showingConnections == 1) { + redrawConnections(context); + } else { + doc.showMessage("Link defined: " + linkLayer.name() + " → " + destArtboard.name()); + } + +} + +var removeLink = function(context) { + var doc = context.document, + selection = context.selection; + if (selection.count() == 0) { + doc.showMessage("Select a layer already defined as a link, to remove its link attributes."); + return; + } + + var loop = context.selection.objectEnumerator(), + linkLayer, destinationArtboardID; + while (linkLayer = loop.nextObject()) { + destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + if (!destinationArtboardID) { continue; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationArtboardID", linkLayer, kPluginDomain); + } + + var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; + if (showingConnections == 1) { + redrawConnections(context); + } else { + var plural = context.selection.count() == 1 ? "Link" : "Links"; + doc.showMessage(plural + " removed."); + } +} + +var editArtboardDescription = function(context) { + + parseContext(context); + + var currentArtboard = context.document.currentPage().currentArtboard(); + if (!currentArtboard) { + showAlert("Select an artboard"); + return; + } + + var settingsWindow = getAlertWindow(); + settingsWindow.addButtonWithTitle("Save"); + settingsWindow.addButtonWithTitle("Cancel"); + + settingsWindow.setMessageText("Artboard: " + currentArtboard.name()); + + settingsWindow.addTextLabelWithValue("Description"); + var descriptionField = NSTextField.alloc().initWithFrame(NSMakeRect(0,0,300,100)); + settingsWindow.addAccessoryView(descriptionField); + + if (settingsWindow.runModal() == "1000") { + context.command.setValue_forKey_onLayer_forPluginIdentifier(descriptionField.stringValue(), "artboardDescription", currentArtboard, kPluginDomain); + context.document.showMessage("Artboard description saved"); + } +} + +var addCondition = function(context) { + + parseContext(context); + + var settingsWindow = getAlertWindow(); + settingsWindow.addButtonWithTitle("Save"); + settingsWindow.addButtonWithTitle("Cancel"); + + settingsWindow.setMessageText("Add or Edit Conditions"); + // settingsWindow.setInformativeText(""); + + var parentArtboards = context.selection.valueForKeyPath("@distinctUnionOfObjects.parentArtboard"), + currentArtboard = parentArtboards.firstObject(), + artboardIsConditional = parentArtboards.count() == 1 ? (context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, currentArtboard, kPluginDomain) || 0) : 0, + hasElse = artboardIsConditional ? (context.command.valueForKey_onLayer_forPluginIdentifier("hasElse", currentArtboard, kPluginDomain) || 1) : 1, + conditionFields = [], + conditionChecks = [], + conditionLinks = [], + elseLink = 0, + conditionField, conditionCheck, conditionLink, conditionView, checkbox, elseCheckbox; + if (artboardIsConditional == 1) { + + var predicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isCondition != nil", kPluginDomain), + conditionLayers = currentArtboard.children().filteredArrayUsingPredicate(predicate); + + if (conditionLayers.count() != 0) { + + var loop = conditionLayers.objectEnumerator(), conditionLayer; + while (conditionLayer = loop.nextObject()) { + conditionView = NSView.alloc().initWithFrame(NSMakeRect(0,0,300,30)); + checkbox = NSButton.alloc().initWithFrame(NSMakeRect(0,0,23,23)); + checkbox.state = NSOnState; + checkbox.setButtonType(NSSwitchButton); + checkbox.setBezelStyle(0); + conditionView.addSubview(checkbox); + conditionField = NSTextField.alloc().initWithFrame(NSMakeRect(21,0,250,22)); + conditionField.cell().setWraps(false); + conditionField.cell().setScrollable(true); + conditionField.setPlaceholderString("Ex: If user is logged in"); + conditionField.setStringValue(conditionLayer.stringValue()); + conditionView.addSubview(conditionField); + settingsWindow.addAccessoryView(conditionView); + conditionFields.push(conditionField); + conditionChecks.push(checkbox); + conditionLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", conditionLayer.parentGroup(), kPluginDomain) || 0; + conditionLinks.push(conditionLink); + } + } + + predicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isElse != nil", kPluginDomain); + var elseLabel = currentArtboard.children().filteredArrayUsingPredicate(predicate).firstObject(); + if (elseLabel) { + elseLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", elseLabel.parentGroup(), kPluginDomain) || 0; + } + } + + conditionView = NSView.alloc().initWithFrame(NSMakeRect(0,0,300,30)); + checkbox = NSButton.alloc().initWithFrame(NSMakeRect(0,0,23,23)); + checkbox.state = NSOnState; + checkbox.setButtonType(NSSwitchButton); + checkbox.setBezelStyle(0); + conditionView.addSubview(checkbox); + conditionField = NSTextField.alloc().initWithFrame(NSMakeRect(21,0,250,22)); + conditionField.setPlaceholderString("Ex: If user is logged in"); + conditionView.addSubview(conditionField); + settingsWindow.addAccessoryView(conditionView); + conditionFields.push(conditionField); + conditionChecks.push(checkbox); + conditionLinks.push(0); + + elseCheckbox = NSButton.alloc().initWithFrame(NSMakeRect(0,0,300,23)); + elseCheckbox.title = "Else.."; + elseCheckbox.state = hasElse; + elseCheckbox.setButtonType(NSSwitchButton); + elseCheckbox.setBezelStyle(0); + settingsWindow.addAccessoryView(elseCheckbox); + conditionChecks.push(elseCheckbox); + conditionLinks.push(elseLink); + + var numConditionFields = conditionFields.length; + for (var i = 0; i < numConditionFields; i++) { + if (i == numConditionFields-1) break; + conditionFields[i].setNextKeyView(conditionFields[i+1]); + }; + settingsWindow.alert().window().setInitialFirstResponder(conditionField); + + if (settingsWindow.runModal() == "1000") { + + var conditionBoard; + + if (artboardIsConditional) { + conditionBoard = currentArtboard; + conditionBoard.removeAllLayers(); + } else { + conditionBoard = MSArtboardGroup.new(); + conditionBoard.setName("⤙ Conditions"); + conditionBoard.setHasBackgroundColor(1); + conditionBoard.frame().setWidth(280); + context.command.setValue_forKey_onLayer_forPluginIdentifier(1, kConditionalArtboardKey, conditionBoard, kPluginDomain); + context.document.currentPage().addLayers([conditionBoard]); + } + conditionBoard.setConstrainProportions(false); + context.command.setValue_forKey_onLayer_forPluginIdentifier(elseCheckbox.state(), "hasElse", conditionBoard, kPluginDomain); + + var numConditions = conditionChecks.length, + conditionSpacing = 16, + listY = conditionSpacing, + flowIndicatorColor = NSUserDefaults.standardUserDefaults().objectForKey(kFlowIndicatorColorKey) || "#F5A623", + conditionBorderColor = MSImmutableColor.colorWithSVGString(flowIndicatorColor).newMutableCounterpart(), + conditionBoardWidth = conditionBoard.frame().width(), + count = 0, + conditionLabel, conditionValue, conditionBox, conditionBorder, conditionBoxHeight, layersArray, conditionGroup, isElse; + + for (var i = 0; i < numConditions; i++) { + + checkbox = conditionChecks[i]; + + if (checkbox.state() == NSOffState) continue; + + isElse = checkbox == elseCheckbox; + if (isElse) { + conditionValue = "Else"; + } else { + conditionField = conditionFields[i]; + conditionValue = conditionField.stringValue(); + if (!conditionValue || conditionValue == "") continue; + } + + count++; + + conditionLabel = MSTextLayer.new(); + conditionLabel.frame().setX(conditionSpacing + 8); + conditionLabel.frame().setY(listY + 8); + conditionLabel.frame().setWidth(conditionBoardWidth - ((conditionSpacing+8)*2)); + conditionLabel.setTextBehaviour(1); + conditionLabel.setStringValue(conditionValue); + conditionLabel.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("HelveticaNeue", 16)); + conditionLabel.setLineHeight(16*1.4); + conditionLabel.setTextColor(MSImmutableColor.colorWithSVGString("#121212").newMutableCounterpart()); + conditionLabel.adjustFrameToFit(); + context.command.setValue_forKey_onLayer_forPluginIdentifier(1, (isElse ? "isElse" : "isCondition"), conditionLabel, kPluginDomain); + + conditionBoxHeight = conditionLabel.frame().height() + 16; + conditionBox = MSShapeGroup.shapeWithPath(MSRectangleShape.alloc().initWithFrame(NSMakeRect(conditionSpacing, listY, conditionBoardWidth-(conditionSpacing*2), conditionBoxHeight))); + conditionBox.firstLayer().setCornerRadiusFloat(5); + conditionBox.style().addStylePartOfType(0).setColor(MSImmutableColor.colorWithSVGString("#f9f9f9").newMutableCounterpart()); + conditionBorder = conditionBox.style().addStylePartOfType(1); + conditionBorder.setColor(conditionBorderColor); + conditionBorder.setPosition(2); + conditionBorder.setThickness(2); + + listY += conditionBoxHeight + conditionSpacing; + + conditionBoard.addLayers([conditionBox, conditionLabel]); + layersArray = MSLayerArray.arrayWithLayers([conditionBox, conditionLabel]); + conditionGroup = MSLayerGroup.groupFromLayers(layersArray); + conditionGroup.setName("Condition " + count); + + context.command.setValue_forKey_onLayer_forPluginIdentifier(1, "isConditionGroup", conditionGroup, kPluginDomain); + + conditionLink = conditionLinks[i]; + if (conditionLink != 0) { + context.command.setValue_forKey_onLayer_forPluginIdentifier(conditionLink, "destinationArtboardID", conditionGroup, kPluginDomain); + } + + } + + conditionBoard.frame().setHeight(listY); + if (artboardIsConditional != 1) { + var vcr = context.document.currentView().visibleContentRect(), + absPosition = NSMakePoint(CGRectGetMidX(vcr)-CGRectGetMidX(conditionBoard.absoluteRect().rect()), CGRectGetMidY(vcr)-CGRectGetMidY(conditionBoard.absoluteRect().rect())); + conditionBoard.setAbsolutePosition(absPosition); + } + + var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; + if (showingConnections == 1) { + redrawConnections(context); + } + } + +} + + +var gotoDestinationArtboard = function(context) { + + parseContext(context); + + var linkLayer = context.selection.firstObject(), + validSelection = true, + destinationArtboardID; + if (!linkLayer) { + validSelection = false; + } else { + destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + if (!destinationArtboardID) { + validSelection = false; + } + } + + if (!validSelection) { + showAlert("Invalid selection", "Select a layer that you have defined as a Link, then run this command again."); + return; + } + + var doc = context.document, + destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); + if (destinationArtboard) { + var cRect = doc.currentView().visibleContentRect(), + contentRect = { + x : cRect.origin.x, + y : cRect.origin.y, + width : cRect.size.width, + height : cRect.size.height, + linkLayerID : linkLayer.objectID() + }, + rects = context.command.valueForKey_onDocument_forPluginIdentifier("contentRectsHistory", doc.documentData(), kPluginDomain); + + if (!rects) { + rects = NSArray.array(); + } + rects = rects.arrayByAddingObject(contentRect); + + context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); + + doc.currentView().centerRect(destinationArtboard.absoluteRect().rect()); + destinationArtboard.select_byExpandingSelection(true, false); + } +} + +var goBackToLink = function(context) { + + var doc = context.document, + rects = context.command.valueForKey_onDocument_forPluginIdentifier("contentRectsHistory", doc.documentData(), kPluginDomain).mutableCopy(); + + if (rects) { + var contentRect = rects.lastObject(); + + if (!contentRect) { return; } + + if (contentRect.linkLayerID ) { + var layer = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", contentRect.linkLayerID)).firstObject(); + if(layer) { + layer.select_byExpandingSelection(true, false); + } + } + + var cRect = NSMakeRect(contentRect.x, contentRect.y, contentRect.width, contentRect.height); + doc.currentView().centerRect(cRect); + + rects.removeLastObject(); + context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); + } +} + +var generateFlow = function(context) { + + parseContext(context); + + var doc = context.document; + var settingsWindow = getAlertWindow(); + settingsWindow.addButtonWithTitle("Generate Diagram"); + settingsWindow.addButtonWithTitle("Cancel"); + + settingsWindow.setMessageText("Generate a User Flow"); + + settingsWindow.addTextLabelWithValue("Start from:"); + + linkLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain); + var linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayerPredicate); + + if (linkLayers.count() == 0) { + howAlert("No links defined", "This page has no link layers defined. Define at least one link layer by going to `Plugins > User Flows > Define Link`"); + return; + } + + var artboardsWithLinks = linkLayers.valueForKeyPath("@distinctUnionOfObjects.parentArtboard"); + var artboardNames = artboardsWithLinks.valueForKeyPath("@unionOfObjects.name"); + var artboardsDropdown = NSPopUpButton.alloc().initWithFrame(NSMakeRect(0,0,300,25)); + artboardsDropdown.addItemsWithTitles(artboardNames); + + var homeScreenID = context.command.valueForKey_onLayer_forPluginIdentifier("homeScreenID", doc.currentPage(), kPluginDomain); + if (homeScreenID) { + var artboardIDs = artboardsWithLinks.valueForKeyPath("@unionOfObjects.objectID"); + var homeScreenIndex = Math.max(0, artboardIDs.indexOfObject(homeScreenID)); + artboardsDropdown.selectItemAtIndex(homeScreenIndex); + } + settingsWindow.addAccessoryView(artboardsDropdown); + + settingsWindow.addTextLabelWithValue("Flow Name"); + var nameField = NSTextField.alloc().initWithFrame(NSMakeRect(0,0,300,23)); + settingsWindow.addAccessoryView(nameField); + + settingsWindow.addTextLabelWithValue("Flow Description"); + var descriptionField = NSTextField.alloc().initWithFrame(NSMakeRect(0,0,300,100)); + settingsWindow.addAccessoryView(descriptionField); + + var separator = NSBox.alloc().initWithFrame(NSMakeRect(0,0,300,10)); + separator.setBoxType(2); + settingsWindow.addAccessoryView(separator); + + var pageNames = doc.valueForKeyPath("pages.@unionOfObjects.name") + if (!pageNames.containsObject("_Flows")) { + pageNames = NSArray.arrayWithObject("_Flows").arrayByAddingObjectsFromArray(pageNames); + } + pageNames = pageNames.arrayByAddingObject("[New Page]"); + var pagesDropdown = NSPopUpButton.alloc().initWithFrame(NSMakeRect(0,0,300,25)); + pagesDropdown.addItemsWithTitles(pageNames); + var lastUsedPageName = context.command.valueForKey_onDocument_forPluginIdentifier("lastUsedFlowPage", doc.documentData(), kPluginDomain); + if (lastUsedPageName && pageNames.containsObject(lastUsedPageName)) { + pagesDropdown.selectItemWithTitle(lastUsedPageName); + } + settingsWindow.addAccessoryView(pagesDropdown); + + var keepOrganized = NSUserDefaults.standardUserDefaults().objectForKey(kKeepOrganizedKey) || 1; + var keepOrganizedCheckbox = NSButton.alloc().initWithFrame(NSMakeRect(0,0,300,22)); + keepOrganizedCheckbox.setButtonType(NSSwitchButton); + keepOrganizedCheckbox.setBezelStyle(0); + keepOrganizedCheckbox.setTitle("Keep _Flows Page Organized"); + keepOrganizedCheckbox.setState(keepOrganized); + settingsWindow.addAccessoryView(keepOrganizedCheckbox); + + settingsWindow.alert().window().setInitialFirstResponder(nameField); + nameField.setNextKeyView(descriptionField); + descriptionField.setNextKeyView(nameField); + + var response = settingsWindow.runModal(); + if (response == "1000") { + + var connectionsOverlayPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isConnectionsContainer == true", kPluginDomain), + connectionsOverlay = doc.currentPage().children().filteredArrayUsingPredicate(connectionsOverlayPredicate).firstObject(), + connectionsGroupVisible; + if (connectionsOverlay) { + connectionsOverlayVisible = connectionsOverlay.isVisible(); + connectionsOverlay.setIsVisible(0); + } + + var exportScale = NSUserDefaults.standardUserDefaults().objectForKey(kExportScaleKey) || 1, + exportFormat = NSUserDefaults.standardUserDefaults().objectForKey(kExportFormatKey) || "pdf", + modifiedBy = NSUserDefaults.standardUserDefaults().objectForKey(kFullNameKey), + showModifiedDate = NSUserDefaults.standardUserDefaults().objectForKey(kShowModifiedDateKey) || false, + flowBackground = NSUserDefaults.standardUserDefaults().objectForKey(kFlowBackgroundKey) || "Light", + flowName = nameField.stringValue(), + flowDescription = descriptionField.stringValue(), + artboardBitmapLayers = [], + connections = [], + exportedArtboardIDs = {}, + outerPadding = 40*exportScale, + spacing = 50*exportScale, + screenNumber = 1, + initialArtboard = artboardsWithLinks.objectAtIndex(artboardsDropdown.indexOfSelectedItem()), + artboardsToExport = [initialArtboard], + screenShadowColor = MSImmutableColor.colorWithSVGString("#00000").newMutableCounterpart(), + tempFolderURL = NSFileManager.defaultManager().URLsForDirectory_inDomains(NSCachesDirectory, NSUserDomainMask).lastObject().URLByAppendingPathComponent(kPluginDomain), + artboard, artboardID, linkLayers, linkLayersCount, destinationArtboard, destinationArtboardID, linkLayer, screenLayer, exportRequest, exportURL, screenShadow, connection, artboardNameLabel, primaryTextColor, secondaryTextColor, flowBackgroundColor, artboardIsConditional, isCondition, destinationArtboardIsConditional; + + context.command.setValue_forKey_onLayer_forPluginIdentifier(initialArtboard.objectID(), "homeScreenID", doc.currentPage(), kPluginDomain); + screenShadowColor.setAlpha(.2); + exportFormat = exportFormat.toLowerCase(); + + if (flowBackground == "Dark") { + flowBackgroundColor = MSImmutableColor.colorWithSVGString("#1E1D1C").newMutableCounterpart(); + primaryTextColor = MSImmutableColor.colorWithSVGString("#FFFFFF").newMutableCounterpart(); + secondaryTextColor = MSImmutableColor.colorWithSVGString("#9B9B9B").newMutableCounterpart(); + } else { + flowBackgroundColor = MSImmutableColor.colorWithSVGString("#FFFFFF").newMutableCounterpart(); + primaryTextColor = MSImmutableColor.colorWithSVGString("#121212").newMutableCounterpart(); + secondaryTextColor = MSImmutableColor.colorWithSVGString("#999999").newMutableCounterpart(); + } + + while(artboardsToExport.length) { + artboard = artboardsToExport.shift(); + artboardID = artboard.objectID(); + if (exportedArtboardIDs[artboardID] == 1) { + continue; + } + exportedArtboardIDs[artboardID] = 1; + + artboardIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, artboard, kPluginDomain) || 0; + + exportRequest = MSExportRequest.alloc().init(); + exportRequest.setRect(artboard.absoluteRect().rect()); + exportRequest.setScale(exportScale); + exportRequest.setShouldTrim(0); + exportRequest.setSaveForWeb(1); + exportRequest.setBackgroundColor(( artboard.hasBackgroundColor() ? artboard.backgroundColor() : MSImmutableColor.colorWithSVGString("#FFFFFF").newMutableCounterpart() )); + exportRequest.setIncludeArtboardBackground(1); + exportRequest.setName(artboard.objectID()); + exportRequest.setFormat(exportFormat); + exportURL = tempFolderURL.URLByAppendingPathComponent(artboard.objectID()).URLByAppendingPathExtension(exportFormat); + doc.saveArtboardOrSlice_toFile(exportRequest, exportURL.path()); + + screenLayer = MSBitmapLayer.bitmapLayerWithImageFromPath(exportURL); + doc.currentPage().addLayers([screenLayer]); + screenLayer.absoluteRect().setX(artboard.absoluteRect().x()); + screenLayer.absoluteRect().setY(artboard.absoluteRect().y()); + screenLayer.absoluteRect().setWidth(artboard.absoluteRect().width()); + screenLayer.absoluteRect().setHeight(artboard.absoluteRect().height()); + + screenShadow = screenLayer.style().addStylePartOfType(1); + screenShadow.setColor(screenShadowColor); + screenShadow.setPosition(2); + screenShadow.setThickness(1/exportScale); + + artboardBitmapLayers.push(screenLayer); + NSFileManager.defaultManager().removeItemAtURL_error(exportURL, nil); + + if (artboardIsConditional == 0) { + artboardNameLabel = MSTextLayer.new(); + doc.currentPage().addLayers([artboardNameLabel]); + artboardNameLabel.setName(artboard.name()); + artboardNameLabel.absoluteRect().setX(artboard.absoluteRect().x()); + artboardNameLabel.absoluteRect().setY(artboard.absoluteRect().y()); + artboardNameLabel.frame().setWidth(artboard.frame().width()); + artboardNameLabel.setTextBehaviour(0); + artboardNameLabel.setStringValue(screenNumber + ": " + artboard.name()); + artboardNameLabel.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("HelveticaNeue", 12*exportScale)); + artboardNameLabel.setTextColor(primaryTextColor); + artboardNameLabel.adjustFrameToFit(); + artboardNameLabel.absoluteRect().setY(artboard.absoluteRect().y() - (artboardNameLabel.absoluteRect().height()/exportScale) - (6*exportScale)); + + artboardBitmapLayers.push(artboardNameLabel); + + screenNumber++; + } + + + linkLayers = artboard.children().filteredArrayUsingPredicate(linkLayerPredicate).sortedArrayUsingDescriptors([ + NSSortDescriptor.sortDescriptorWithKey_ascending("absoluteRect.rulerY", true) + ]); + linkLayersCount = linkLayers.count(); + for (var i=0; i < linkLayersCount; i++) { + linkLayer = linkLayers.objectAtIndex(i); + destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + + isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; + + destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); + destinationArtboardIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, destinationArtboard, kPluginDomain) || 0; + if (destinationArtboard) { + + connection = { + linkRect : linkLayer.absoluteRect().rect(), + linkIsCondition : isCondition, + destinationIsConditional : destinationArtboardIsConditional, + dropPoint : { + x : destinationArtboard.absoluteRect().x() - (10*exportScale), + y : destinationArtboard.absoluteRect().y() - (10*exportScale) + } + } + connections.push(connection); + artboardsToExport.push(destinationArtboard); + + } + } + } + + if (connectionsOverlay) { + connectionsOverlay.setIsVisible(connectionsOverlayVisible); + } + + var connectionLayers = MSLayerArray.arrayWithLayers(drawConnections(connections, doc.currentPage(), exportScale)); + var connectionsGroup = MSLayerGroup.groupFromLayers(connectionLayers); + connectionsGroup.setName("Connections"); + artboardBitmapLayers.push(connectionsGroup); + connectionsGroup.setIsLocked(1); + + var groupBounds = CGRectZero; + for (var i = 0; i < artboardBitmapLayers.length; i++) { + groupBounds = CGRectUnion(groupBounds, artboardBitmapLayers[i].absoluteRect().rect()); + } + var layers = MSLayerArray.arrayWithLayers(artboardBitmapLayers); + var newGroup = MSLayerGroup.groupFromLayers(layers); + newGroup.setName("Flow Group"); + newGroup.resizeToFitChildrenWithOption(1); + + var flowBoard = MSArtboardGroup.new(); + flowBoard.setName(flowName); + flowBoard.setHasBackgroundColor(1); + flowBoard.setBackgroundColor(flowBackgroundColor); + + var flowNameLabel = MSTextLayer.new(); + flowNameLabel.setName(flowName); + flowNameLabel.frame().setX(outerPadding); + flowNameLabel.frame().setY(outerPadding); + flowNameLabel.frame().setWidth(groupBounds.size.width); + flowNameLabel.setTextBehaviour(1); + flowNameLabel.setStringValue(flowName); + flowNameLabel.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("HelveticaNeue", 18*exportScale)); + flowNameLabel.setTextColor(primaryTextColor); + flowNameLabel.adjustFrameToFit(); + flowNameLabel.setIsLocked(1); + flowBoard.addLayers([flowNameLabel]); + + var yPos = outerPadding + flowNameLabel.frame().height() + 14; + + if (flowDescription && flowDescription != "") { + var flowDescriptionLabel = MSTextLayer.new(); + flowDescriptionLabel.setName("Flow Description"); + flowDescriptionLabel.frame().setX(outerPadding); + flowDescriptionLabel.frame().setY(yPos); + flowDescriptionLabel.frame().setWidth(groupBounds.size.width); + flowDescriptionLabel.setTextBehaviour(1); + flowDescriptionLabel.setStringValue(flowDescription); + flowDescriptionLabel.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("HelveticaNeue", 12*exportScale)); + flowDescriptionLabel.setTextColor(secondaryTextColor); + flowDescriptionLabel.adjustFrameToFit(); + flowDescriptionLabel.setIsLocked(1); + flowBoard.addLayers([flowDescriptionLabel]); + yPos = flowDescriptionLabel.frame().y() + flowDescriptionLabel.frame().height(); + } + + if (showModifiedDate == 1) { + + var formatter = NSDateFormatter.alloc().init(); + formatter.setTimeStyle(NSDateFormatterNoStyle); + formatter.setDateStyle(NSDateFormatterMediumStyle); + + var modifiedDateLabel = MSTextLayer.new(); + var modifiedOnText = "Modified on " + formatter.stringFromDate(NSDate.date()); + if (modifiedBy && modifiedBy != "") { + modifiedOnText += " by " + modifiedBy; + } + modifiedDateLabel.setName("Modified Date"); + modifiedDateLabel.frame().setX(outerPadding); + modifiedDateLabel.frame().setY(yPos + 12); + modifiedDateLabel.frame().setWidth(groupBounds.size.width - (outerPadding*2)); + modifiedDateLabel.setTextBehaviour(1); + modifiedDateLabel.setStringValue(modifiedOnText); + modifiedDateLabel.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("HelveticaNeue", 12*exportScale)); + modifiedDateLabel.setTextColor(secondaryTextColor); + modifiedDateLabel.adjustFrameToFit(); + modifiedDateLabel.setIsLocked(1); + flowBoard.addLayers([modifiedDateLabel]); + } + + yPos += 60; + + newGroup.removeFromParent(); + flowBoard.addLayers([newGroup]); + newGroup.frame().scaleBy(exportScale); + newGroup.frame().setX(outerPadding); + newGroup.frame().setY(yPos); + flowBoard.frame().setWidth(newGroup.frame().width() + (outerPadding*2)); + flowBoard.frame().setHeight(yPos + newGroup.frame().height() + outerPadding); + newGroup.ungroup(); + + var flowPageName = pagesDropdown.titleOfSelectedItem(), + flowPage = doc.pages().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("name == %@", flowPageName)).firstObject();; + if (!flowPage) { + flowPage = doc.addBlankPage(); + if (flowPageName == "[New Page]") { + flowPageName = "Page " + doc.pages().count(); + } + flowPage.setName(flowPageName); + } + + flowPage.addLayers([flowBoard]); + var shouldOrganize = keepOrganizedCheckbox.state(); + if (shouldOrganize && flowPageName == "_Flows") { + + var loop = flowPage.artboards().objectEnumerator(), + i = 0, newX = 0, newY = 0, maxHeight = 0, + spacing = 160, artboard; + while (artboard = loop.nextObject()) { + artboard.frame().setX(newX); + artboard.frame().setY(newY); + newX += artboard.frame().width() + spacing; + maxHeight = Math.max(artboard.frame().height(), maxHeight); + if (++i == 6) { + i = 0 + newY += maxHeight + (spacing * 2); + newX = maxHeight = 0; + } + } + } else { + var originForNewArtboard = flowPage.originForNewArtboard(); + flowBoard.absoluteRect().setX(originForNewArtboard.x); + flowBoard.absoluteRect().setY(originForNewArtboard.y); + } + + context.command.setValue_forKey_onDocument_forPluginIdentifier(flowPageName, "lastUsedFlowPage", doc.documentData(), kPluginDomain); + + flowBoard.setConstrainProportions(false); + flowBoard.resizeToFitChildrenWithOption(0); + flowBoard.exportOptions().addExportFormat(); + + doc.setCurrentPage(flowPage); + flowBoard.select_byExpandingSelection(true, false); + doc.currentView().zoomToFitRect(NSInsetRect(flowBoard.absoluteRect().rect(), -60, -60)); + + // update defaults + NSUserDefaults.standardUserDefaults().setObject_forKey(shouldOrganize, kKeepOrganizedKey); + + logEvent("generatedFlow", {numberOfScreens : screenNumber, format : exportFormat, exportScale : exportScale}); + } +} + +var updateFlow = function(context) { + +} + +var hideConnections = function(context) { + + var doc = context.document; + var connectionsLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isConnectionsContainer == true", kPluginDomain); + var connectionsGroup = doc.currentPage().children().filteredArrayUsingPredicate(connectionsLayerPredicate).firstObject(); + + if (connectionsGroup) { + connectionsGroup.removeFromParent(); + } + + NSUserDefaults.standardUserDefaults().setObject_forKey(0, kShowConnectionsKey); +} + +var showConnections = function(context) { + + NSUserDefaults.standardUserDefaults().setObject_forKey(1, kShowConnectionsKey); + + redrawConnections(context); +} + +var redrawConnections = function(context) { + var doc = context.document || context.actionContext.document; + var selectedLayers = doc.findSelectedLayers(); + var connectionsGroup = getConnectionsGroupInPage(doc.currentPage()); + + if (connectionsGroup) { + connectionsGroup.removeFromParent(); + } + + var linkLayersPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain), + linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayersPredicate), + loop = linkLayers.objectEnumerator(), + connections = [], + linkLayer, destinationArtboardID, destinationArtboard, isCondition; + + while (linkLayer = loop.nextObject()) { + + destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + + isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; + + destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); + if (destinationArtboard) { + + connection = { + linkRect : linkLayer.absoluteRect().rect(), + linkIsCondition : isCondition, + dropPoint : { + x : destinationArtboard.absoluteRect().x() - 10, + y : destinationArtboard.absoluteRect().y() + } + } + connections.push(connection); + } + } + + var connectionLayers = MSLayerArray.arrayWithLayers(drawConnections(connections, doc.currentPage(), 1)); + connectionsGroup = MSLayerGroup.groupFromLayers(connectionLayers); + connectionsGroup.setName("Connections"); + connectionsGroup.setIsLocked(1); + context.command.setValue_forKey_onLayer_forPluginIdentifier(true, "isConnectionsContainer", connectionsGroup, kPluginDomain); + doc.currentPage().deselectAllLayers(); + + var loop = selectedLayers.objectEnumerator(), selectedLayer; + while (selectedLayer = loop.nextObject()) { + selectedLayer.select_byExpandingSelection(true, true); + } + + return connectionsGroup; +} + +var drawConnections = function(connections, parent, exportScale) { + var connectionsCount = connections.length, + flowIndicatorColor = NSUserDefaults.standardUserDefaults().objectForKey(kFlowIndicatorColorKey) || "#F5A623", + minimumTapArea = NSUserDefaults.standardUserDefaults().objectForKey(kMinTapAreaKey) || 44, + connectionLayers = [], + hitAreaColor = MSImmutableColor.colorWithSVGString("#000000").newMutableCounterpart(), + hitAreaBorderColor = MSImmutableColor.colorWithSVGString(flowIndicatorColor).newMutableCounterpart(), + arrowRotation = 0, + arrowOffsetX = 0, + path, hitAreaLayer, linkRect, dropPoint, hitAreaBorder, startPoint, controlPoint1, controlPoint1Offset, controlPoint2OffsetX, controlPoint2OffsetY, linePath, lineLayer, destinationArtboardIsConditional; + hitAreaColor.setAlpha(0); + + for (var i=0; i < connectionsCount; i++) { + connection = connections[i]; + linkRect = connection.linkRect; + destinationArtboardIsConditional = connection.destinationIsConditional == 1; + if (linkRect.size.width < minimumTapArea) { + linkRect = NSInsetRect(linkRect, (linkRect.size.width-minimumTapArea)/2, 0); + } + if (linkRect.size.height < minimumTapArea) { + linkRect = NSInsetRect(linkRect, 0, (linkRect.size.height-minimumTapArea)/2); + } + + if (connection.linkIsCondition != 1) { + path = NSBezierPath.bezierPathWithRect(linkRect); + hitAreaLayer = MSShapeGroup.shapeWithBezierPath(path); + hitAreaLayer.style().addStylePartOfType(0).setColor(hitAreaColor); + hitAreaBorder = hitAreaLayer.style().addStylePartOfType(1); + hitAreaBorder.setColor(hitAreaBorderColor); + hitAreaBorder.setPosition(2); + hitAreaBorder.setThickness(2*exportScale); + parent.addLayers([hitAreaLayer]); + connectionLayers.push(hitAreaLayer); + } + + dropPoint = destinationArtboardIsConditional ? NSMakePoint(connection.dropPoint.x+(5*exportScale), connection.dropPoint.y + (10*exportScale)) : NSMakePoint(connection.dropPoint.x, connection.dropPoint.y); + if (dropPoint.x < CGRectGetMinX(linkRect)) { + dropPoint = NSMakePoint(dropPoint.x + 18, dropPoint.y - (30/exportScale) ); + arrowRotation = 90; + arrowOffsetX = 2; + if (dropPoint.y < CGRectGetMinY(linkRect)) { + startPoint = NSMakePoint(CGRectGetMidX(linkRect), CGRectGetMinY(linkRect) + 5); + controlPoint1Offset = Math.max(Math.abs(dropPoint.y - startPoint.y)/2, 200); + controlPoint1 = NSMakePoint(startPoint.x, startPoint.y - controlPoint1Offset); + } else { + startPoint = NSMakePoint(CGRectGetMidX(linkRect), CGRectGetMaxY(linkRect) - 5); + controlPoint1Offset = Math.max(Math.abs(dropPoint.y - startPoint.y)/2, 200); + controlPoint1 = NSMakePoint(startPoint.x, startPoint.y + controlPoint1Offset); + } + controlPoint2OffsetX = 0; + controlPoint2OffsetY = -160; + + } else { + startPoint = NSMakePoint(CGRectGetMaxX(linkRect) - 5, CGRectGetMidY(linkRect)); + controlPoint1Offset = Math.max(Math.abs(dropPoint.x - startPoint.x)/2, 100); + controlPoint1 = NSMakePoint(startPoint.x + controlPoint1Offset, startPoint.y); + controlPoint2OffsetY = 0; + controlPoint2OffsetX = Math.max(Math.abs(dropPoint.x - startPoint.x)/2, 100); + arrowRotation = 0; + } + + linkRect = NSInsetRect(NSMakeRect(startPoint.x, startPoint.y, 0, 0), -5, -5); + path = NSBezierPath.bezierPathWithOvalInRect(linkRect); + hitAreaLayer = MSShapeGroup.shapeWithBezierPath(path); + hitAreaLayer.style().addStylePartOfType(0).setColor(hitAreaBorderColor); + parent.addLayers([hitAreaLayer]); + connectionLayers.push(hitAreaLayer); + + linePath = NSBezierPath.bezierPath(); + linePath.moveToPoint(startPoint); + linePath.curveToPoint_controlPoint1_controlPoint2(dropPoint, controlPoint1, NSMakePoint(dropPoint.x - controlPoint2OffsetX, dropPoint.y + controlPoint2OffsetY)); + + lineLayer = MSShapeGroup.shapeWithBezierPath(linePath); + lineLayer.setName("Flow arrow"); + hitAreaBorder = lineLayer.style().addStylePartOfType(1); + hitAreaBorder.setColor(hitAreaBorderColor); + hitAreaBorder.setThickness(3*exportScale); + hitAreaBorder.setPosition(0); + parent.addLayers([lineLayer]); + connectionLayers.push(lineLayer); + + var arrowSize = 12; + path = NSBezierPath.bezierPath(); + path.moveToPoint(NSMakePoint(dropPoint.x+(arrowSize*0.6), dropPoint.y)); + path.lineToPoint(NSMakePoint(dropPoint.x-arrowSize, dropPoint.y+(arrowSize*0.6))); + path.lineToPoint(NSMakePoint(dropPoint.x-(arrowSize*0.6), dropPoint.y)); + path.lineToPoint(NSMakePoint(dropPoint.x-arrowSize, dropPoint.y-(arrowSize*0.6))); + path.closePath(); + var arrow = MSShapeGroup.shapeWithBezierPath(path); + arrow.style().addStylePartOfType(0).setColor(hitAreaBorderColor); + arrow.setRotation(-arrowRotation); + arrow.absoluteRect().setX(arrow.absoluteRect().x() + arrowOffsetX); + parent.addLayers([arrow]); + connectionLayers.push(arrow); + + } + + return connectionLayers; +} + +var editSettings = function(context) { + + parseContext(context); + var settingsWindow = getAlertWindow(); + settingsWindow.addButtonWithTitle("Save"); + settingsWindow.addButtonWithTitle("Cancel"); + settingsWindow.addButtonWithTitle("Reset Defaults"); + + settingsWindow.setMessageText("User Flows Settings"); + settingsWindow.setInformativeText("v" + context.plugin.version() + " | © Aby Nimbalkar | @abynim"); + + settingsWindow.addTextLabelWithValue("Artboard Export Options"); + var scaleOptions = [1, 2]; + var numOptions = scaleOptions.length; + var exportScale = NSUserDefaults.standardUserDefaults().objectForKey(kExportScaleKey) || 1; + var buttonCell = NSButtonCell.new(); + buttonCell.setTitle("Scale Options"); + buttonCell.setButtonType(NSRadioButton); + + var scaleOptionsMatrix = NSMatrix.alloc().initWithFrame_mode_prototype_numberOfRows_numberOfColumns(NSMakeRect(0, 0, 300, 22), NSRadioModeMatrix, buttonCell, 1, numOptions); + scaleOptionsMatrix.setAutorecalculatesCellSize(true); + scaleOptionsMatrix.setIntercellSpacing(NSMakeSize(10,10)); + var cells = scaleOptionsMatrix.cells(); + var scaleOption; + + for (var i = 0; i Date: Tue, 29 Nov 2016 01:44:17 +0800 Subject: [PATCH 05/10] Added check for updates --- .../Contents/Sketch/manifest.json | 13 ++++++- .../Contents/Sketch/script.js | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/manifest.json b/UserFlows.sketchplugin/Contents/Sketch/manifest.json index eaed0e7..c8f126e 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/manifest.json +++ b/UserFlows.sketchplugin/Contents/Sketch/manifest.json @@ -132,6 +132,16 @@ "handlers" : { "run" : "editShortcuts" } + }, + { + "script" : "script.js", + "shortcut" : "", + "name" : "Check for Updates...", + "identifier" : "checkForUpdates", + "icon" : "icon.png", + "handlers" : { + "run" : "checkForUpdates" + } } ], "menu" : { @@ -149,7 +159,8 @@ "goBackToLink", "-", "editSettings", - "editShortcuts" + "editShortcuts", + "checkForUpdates" ] }, "compatibleVersion" : 4.1, diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index 5c0074d..a0d8cf5 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -1078,6 +1078,37 @@ var editShortcuts = function(context) { } } +var checkForUpdates = function(context) { + + parseContext(context); + + context.document.showMessage("Checking for updates..."); + + var json = NSJSONSerialization.JSONObjectWithData_options_error(NSData.dataWithContentsOfURL(NSURL.URLWithString("https://abynim.github.io/UserFlows/version.json")), 0, nil), + currentVersion = json.valueForKey("currentVersion"), + currentVersionAsInt = getVersionNumberFromString(currentVersion), + installedVersion = context.plugin.version(), + installedVersionAsInt = getVersionNumberFromString(installedVersion), + updateAvailable = currentVersionAsInt > installedVersionAsInt, + updateAlert = getAlertWindow(); + + updateAlert.setMessageText(updateAvailable ? "An update is available." : "You're good."); + if (updateAvailable) { + updateAlert.setInformativeText("The most recent version is " + currentVersion + " and you have version " + installedVersion + ". Please download and install the plugin again from the website."); + updateAlert.addButtonWithTitle("Update Now"); + updateAlert.addButtonWithTitle("Later"); + } else { + updateAlert.setInformativeText("You have the most recent version of UserFlows installed 👍"); + updateAlert.addButtonWithTitle("Done"); + } + + var response = updateAlert.runModal(); + if (updateAvailable && response == "1000") { + var websiteURL = NSURL.URLWithString(json.valueForKey("websiteURL")); + NSWorkspace.sharedWorkspace().openURL(websiteURL); + } + +} var showAlert = function(message, info, primaryButtonText, secondaryButtonText) { var alert = getAlertWindow(); @@ -1109,6 +1140,14 @@ var getAlertWindow = function() { return alert; } +var getVersionNumberFromString = function(versionString) { + var versionNumber = versionString.stringByReplacingOccurrencesOfString_withString(".", "") + "" + while(versionNumber.length != 3) { + versionNumber += "0" + } + return parseInt(versionNumber) +} + var logEvent = function(event, props) { var uuid = NSUserDefaults.standardUserDefaults().objectForKey(kUUIDKey); if (!uuid) { From 1ea33d36f4d5a1aacd0eca3a990765138070326c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Tue, 6 Dec 2016 11:30:08 -0500 Subject: [PATCH 06/10] fixed documentation link --- UserFlows.sketchplugin/Contents/Sketch/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/manifest.json b/UserFlows.sketchplugin/Contents/Sketch/manifest.json index c8f126e..0748358 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/manifest.json +++ b/UserFlows.sketchplugin/Contents/Sketch/manifest.json @@ -168,7 +168,7 @@ "identifier" : "com.abynim.sketchplugins.userflows", "version" : "2.0", "description" : "Generate user-flow diagrams from artboards in Sketch", - "homepage" : "https:\/\/github.com\/abynim\/UserFlow.sketchplugin", + "homepage" : "https:\/\/github.com\/abynim\/UserFlows", "authorEmail" : "abynimbalkar@gmail.com", "name" : "User Flows" } \ No newline at end of file From 9a83fa2ec3b336b6fa0aa9eb21663ae94342d5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Thu, 8 Dec 2016 11:43:17 -0500 Subject: [PATCH 07/10] basic linking and unlinking between layers --- .../Contents/Sketch/script.js | 131 +++++++++++------- 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index a0d8cf5..41fba76 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -19,31 +19,35 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var destArtboard, linkLayer; + var dest, linkLayer; if (selection.count() != 2) { validSelection = false; } else { if (selection.firstObject().className() == "MSArtboardGroup") { - destArtboard = selection.firstObject(); + dest = selection.firstObject(); linkLayer = selection.lastObject(); } else if(selection.lastObject().className() == "MSArtboardGroup") { - destArtboard = selection.lastObject(); + dest = selection.lastObject(); linkLayer = selection.firstObject(); } + else { + dest = selection.firstObject(); + linkLayer = selection.lastObject(); + } - if (!destArtboard || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == destArtboard) { + if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == dest) { validSelection = false; } } if (!validSelection) { - showAlert("Invalid selection", "Select a layer or group to define as a Link, select the destination artboard, then run this command again."); + showAlert("Invalid selection", "Select a layer or group to define as a Link, select the destination artboard or layer, then run this command again."); return; } - context.command.setValue_forKey_onLayer_forPluginIdentifier(destArtboard.objectID(), "destinationArtboardID", linkLayer, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(dest.objectID(), "destinationID", linkLayer, kPluginDomain); var doc = context.document; var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -51,7 +55,7 @@ var defineLink = function(context) { if (showingConnections == 1) { redrawConnections(context); } else { - doc.showMessage("Link defined: " + linkLayer.name() + " → " + destArtboard.name()); + doc.showMessage("Link defined: " + linkLayer.name() + " → " + dest.name()); } } @@ -65,11 +69,11 @@ var removeLink = function(context) { } var loop = context.selection.objectEnumerator(), - linkLayer, destinationArtboardID; + linkLayer, destinationID; while (linkLayer = loop.nextObject()) { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); - if (!destinationArtboardID) { continue; } - context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { continue; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); } var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -151,7 +155,7 @@ var addCondition = function(context) { settingsWindow.addAccessoryView(conditionView); conditionFields.push(conditionField); conditionChecks.push(checkbox); - conditionLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", conditionLayer.parentGroup(), kPluginDomain) || 0; + conditionLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", conditionLayer.parentGroup(), kPluginDomain) || 0; conditionLinks.push(conditionLink); } } @@ -159,7 +163,7 @@ var addCondition = function(context) { predicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).isElse != nil", kPluginDomain); var elseLabel = currentArtboard.children().filteredArrayUsingPredicate(predicate).firstObject(); if (elseLabel) { - elseLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", elseLabel.parentGroup(), kPluginDomain) || 0; + elseLink = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", elseLabel.parentGroup(), kPluginDomain) || 0; } } @@ -269,7 +273,7 @@ var addCondition = function(context) { conditionLink = conditionLinks[i]; if (conditionLink != 0) { - context.command.setValue_forKey_onLayer_forPluginIdentifier(conditionLink, "destinationArtboardID", conditionGroup, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(conditionLink, "destinationID", conditionGroup, kPluginDomain); } } @@ -290,18 +294,18 @@ var addCondition = function(context) { } -var gotoDestinationArtboard = function(context) { +var gotoDestination = function(context) { parseContext(context); var linkLayer = context.selection.firstObject(), validSelection = true, - destinationArtboardID; + destinationID; if (!linkLayer) { validSelection = false; } else { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); - if (!destinationArtboardID) { + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { validSelection = false; } } @@ -312,8 +316,8 @@ var gotoDestinationArtboard = function(context) { } var doc = context.document, - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - if (destinationArtboard) { + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + if (destination) { var cRect = doc.currentView().visibleContentRect(), contentRect = { x : cRect.origin.x, @@ -331,8 +335,30 @@ var gotoDestinationArtboard = function(context) { context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); - doc.currentView().centerRect(destinationArtboard.absoluteRect().rect()); - destinationArtboard.select_byExpandingSelection(true, false); + doc.currentView().centerRect(destination.absoluteRect().rect()); + destination.select_byExpandingSelection(true, false); + }else{ + destination = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() + + var cRect = doc.currentView().visibleContentRect(), + contentRect = { + x : cRect.origin.x, + y : cRect.origin.y, + width : cRect.size.width, + height : cRect.size.height, + linkLayerID : linkLayer.objectID() + }, + rects = context.command.valueForKey_onDocument_forPluginIdentifier("contentRectsHistory", doc.documentData(), kPluginDomain); + + if (!rects) { + rects = NSArray.array(); + } + rects = rects.arrayByAddingObject(contentRect); + + context.command.setValue_forKey_onDocument_forPluginIdentifier(rects, "contentRectsHistory", doc.documentData(), kPluginDomain); + + doc.currentView().centerRect(destination.absoluteRect().rect()); + destination.select_byExpandingSelection(true, false); } } @@ -374,7 +400,7 @@ var generateFlow = function(context) { settingsWindow.addTextLabelWithValue("Start from:"); - linkLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain); + linkLayerPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationID != nil", kPluginDomain); var linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayerPredicate); if (linkLayers.count() == 0) { @@ -460,7 +486,7 @@ var generateFlow = function(context) { artboardsToExport = [initialArtboard], screenShadowColor = MSImmutableColor.colorWithSVGString("#00000").newMutableCounterpart(), tempFolderURL = NSFileManager.defaultManager().URLsForDirectory_inDomains(NSCachesDirectory, NSUserDomainMask).lastObject().URLByAppendingPathComponent(kPluginDomain), - artboard, artboardID, linkLayers, linkLayersCount, destinationArtboard, destinationArtboardID, linkLayer, screenLayer, exportRequest, exportURL, screenShadow, connection, artboardNameLabel, primaryTextColor, secondaryTextColor, flowBackgroundColor, artboardIsConditional, isCondition, destinationArtboardIsConditional; + artboard, artboardID, linkLayers, linkLayersCount, destination, destinationID, linkLayer, screenLayer, exportRequest, exportURL, screenShadow, connection, artboardNameLabel, primaryTextColor, secondaryTextColor, flowBackgroundColor, artboardIsConditional, isCondition, destinationIsConditional; context.command.setValue_forKey_onLayer_forPluginIdentifier(initialArtboard.objectID(), "homeScreenID", doc.currentPage(), kPluginDomain); screenShadowColor.setAlpha(.2); @@ -539,25 +565,25 @@ var generateFlow = function(context) { linkLayersCount = linkLayers.count(); for (var i=0; i < linkLayersCount; i++) { linkLayer = linkLayers.objectAtIndex(i); - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - destinationArtboardIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, destinationArtboard, kPluginDomain) || 0; - if (destinationArtboard) { + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + destinationIsConditional = context.command.valueForKey_onLayer_forPluginIdentifier(kConditionalArtboardKey, destination, kPluginDomain) || 0; + if (destination) { connection = { linkRect : linkLayer.absoluteRect().rect(), linkIsCondition : isCondition, - destinationIsConditional : destinationArtboardIsConditional, + destinationIsConditional : destinationIsConditional, dropPoint : { - x : destinationArtboard.absoluteRect().x() - (10*exportScale), - y : destinationArtboard.absoluteRect().y() - (10*exportScale) + x : destination.absoluteRect().x() - (10*exportScale), + y : destination.absoluteRect().y() - (10*exportScale) } } connections.push(connection); - artboardsToExport.push(destinationArtboard); + artboardsToExport.push(destination); } } @@ -729,6 +755,7 @@ var showConnections = function(context) { } var redrawConnections = function(context) { + var doc = context.document || context.actionContext.document; var selectedLayers = doc.findSelectedLayers(); var connectionsGroup = getConnectionsGroupInPage(doc.currentPage()); @@ -737,31 +764,33 @@ var redrawConnections = function(context) { connectionsGroup.removeFromParent(); } - var linkLayersPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationArtboardID != nil", kPluginDomain), + var linkLayersPredicate = NSPredicate.predicateWithFormat("userInfo != nil && function(userInfo, 'valueForKeyPath:', %@).destinationID != nil", kPluginDomain), linkLayers = doc.currentPage().children().filteredArrayUsingPredicate(linkLayersPredicate), loop = linkLayers.objectEnumerator(), connections = [], - linkLayer, destinationArtboardID, destinationArtboard, isCondition; + linkLayer, destinationID, destination, isCondition; while (linkLayer = loop.nextObject()) { - destinationArtboardID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationArtboardID", linkLayer, kPluginDomain); + destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); isCondition = context.command.valueForKey_onLayer_forPluginIdentifier("isConditionGroup", linkLayer, kPluginDomain) || 0; - destinationArtboard = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationArtboardID)).firstObject(); - if (destinationArtboard) { - - connection = { - linkRect : linkLayer.absoluteRect().rect(), - linkIsCondition : isCondition, - dropPoint : { - x : destinationArtboard.absoluteRect().x() - 10, - y : destinationArtboard.absoluteRect().y() - } - } - connections.push(connection); + destination = doc.currentPage().artboards().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject(); + + if (!destination) { + destination = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() + } + + connection = { + linkRect : linkLayer.absoluteRect().rect(), + linkIsCondition : isCondition, + dropPoint : { + x : destination.absoluteRect().x() - 10, + y : destination.absoluteRect().y() + } } + connections.push(connection); } var connectionLayers = MSLayerArray.arrayWithLayers(drawConnections(connections, doc.currentPage(), 1)); @@ -788,13 +817,13 @@ var drawConnections = function(connections, parent, exportScale) { hitAreaBorderColor = MSImmutableColor.colorWithSVGString(flowIndicatorColor).newMutableCounterpart(), arrowRotation = 0, arrowOffsetX = 0, - path, hitAreaLayer, linkRect, dropPoint, hitAreaBorder, startPoint, controlPoint1, controlPoint1Offset, controlPoint2OffsetX, controlPoint2OffsetY, linePath, lineLayer, destinationArtboardIsConditional; + path, hitAreaLayer, linkRect, dropPoint, hitAreaBorder, startPoint, controlPoint1, controlPoint1Offset, controlPoint2OffsetX, controlPoint2OffsetY, linePath, lineLayer, destinationIsConditional; hitAreaColor.setAlpha(0); for (var i=0; i < connectionsCount; i++) { connection = connections[i]; linkRect = connection.linkRect; - destinationArtboardIsConditional = connection.destinationIsConditional == 1; + destinationIsConditional = connection.destinationIsConditional == 1; if (linkRect.size.width < minimumTapArea) { linkRect = NSInsetRect(linkRect, (linkRect.size.width-minimumTapArea)/2, 0); } @@ -814,7 +843,7 @@ var drawConnections = function(connections, parent, exportScale) { connectionLayers.push(hitAreaLayer); } - dropPoint = destinationArtboardIsConditional ? NSMakePoint(connection.dropPoint.x+(5*exportScale), connection.dropPoint.y + (10*exportScale)) : NSMakePoint(connection.dropPoint.x, connection.dropPoint.y); + dropPoint = destinationIsConditional ? NSMakePoint(connection.dropPoint.x+(5*exportScale), connection.dropPoint.y + (10*exportScale)) : NSMakePoint(connection.dropPoint.x, connection.dropPoint.y); if (dropPoint.x < CGRectGetMinX(linkRect)) { dropPoint = NSMakePoint(dropPoint.x + 18, dropPoint.y - (30/exportScale) ); arrowRotation = 90; @@ -1172,5 +1201,5 @@ var logEvent = function(event, props) { base64 = json.base64EncodedStringWithOptions(0), url = NSURL.URLWithString(NSString.stringWithFormat("https://api.mixpanel.com/track/?data=%@&ip=1", base64)); - if (url) NSURLSession.sharedSession().dataTaskWithURL(url).resume(); + //if (url) NSURLSession.sharedSession().dataTaskWithURL(url).resume(); } From f362dcb19a72b2d41cc84990709976fbad54a704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Thu, 8 Dec 2016 15:56:00 -0500 Subject: [PATCH 08/10] start reversing links between layers --- .../Contents/Sketch/manifest.json | 12 +++++++++ .../Contents/Sketch/script.js | 26 +++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/manifest.json b/UserFlows.sketchplugin/Contents/Sketch/manifest.json index 0748358..f537c60 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/manifest.json +++ b/UserFlows.sketchplugin/Contents/Sketch/manifest.json @@ -23,6 +23,17 @@ "icon" : "icon.png", "name" : "Remove Link" }, + { + "script" : "script.js", + "shortcut" : "cmd shift i", + "handlers" : { + "run" : "reverseLink" + }, + "identifier" : "reverseLink", + "description" : "Reverse a Link.", + "icon" : "icon.png", + "name" : "Reverse Link" + }, { "script" : "script.js", "shortcut" : "cmd shift d", @@ -148,6 +159,7 @@ "items" : [ "defineLink", "removeLink", + "reverseLink", "addCondition", "-", "generateFlow", diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index 41fba76..39dc341 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -19,7 +19,9 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var dest, linkLayer; + var dest, linkLayer, reversal; + + reversal = context.reverse; if (selection.count() != 2) { validSelection = false; @@ -33,8 +35,8 @@ var defineLink = function(context) { linkLayer = selection.firstObject(); } else { - dest = selection.firstObject(); - linkLayer = selection.lastObject(); + dest = context.reverse ? selection.lastObject() : selection.firstObject(); + linkLayer = context.reverse ? selection.firstObject() : selection.lastObject(); } if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == dest) { @@ -57,7 +59,6 @@ var defineLink = function(context) { } else { doc.showMessage("Link defined: " + linkLayer.name() + " → " + dest.name()); } - } var removeLink = function(context) { @@ -70,9 +71,12 @@ var removeLink = function(context) { var loop = context.selection.objectEnumerator(), linkLayer, destinationID; + while (linkLayer = loop.nextObject()) { destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); + if (!destinationID) { continue; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); } @@ -85,6 +89,16 @@ var removeLink = function(context) { } } +var reverseLink = function(context) { + var currentSelection = context.selection; + + removeLink(context); + + context.reverse = true; + + defineLink(context); +} + var editArtboardDescription = function(context) { parseContext(context); @@ -290,10 +304,8 @@ var addCondition = function(context) { redrawConnections(context); } } - } - var gotoDestination = function(context) { parseContext(context); @@ -731,7 +743,6 @@ var generateFlow = function(context) { } var updateFlow = function(context) { - } var hideConnections = function(context) { @@ -1136,7 +1147,6 @@ var checkForUpdates = function(context) { var websiteURL = NSURL.URLWithString(json.valueForKey("websiteURL")); NSWorkspace.sharedWorkspace().openURL(websiteURL); } - } var showAlert = function(message, info, primaryButtonText, secondaryButtonText) { From 4265036e0decb2fc2011b362bff6d1068b39469a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Fri, 3 Feb 2017 13:19:05 -0500 Subject: [PATCH 09/10] Fixed reversing links. Also between layers, trying to link already reversed link and removing reversed link. --- .../Contents/Sketch/script.js | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index 39dc341..b33151c 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -19,13 +19,26 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var dest, linkLayer, reversal; + var dest, linkLayer, reversing, alreadyReversed; - reversal = context.reverse; + + reversing = context.reverse != nil ? context.reverse :false; + + if(context.command.valueForKey_onLayer_forPluginIdentifier("reversed-"+selection.lastObject().objectID(), selection.firstObject(), kPluginDomain) == 1 + || context.command.valueForKey_onLayer_forPluginIdentifier("reversed-"+selection.firstObject().objectID(), selection.lastObject(), kPluginDomain) == 1){ + + alreadyReversed = true; + } if (selection.count() != 2) { validSelection = false; } else { + + if(alreadyReversed && !reversing){ + return; + reversing = false + } + if (selection.firstObject().className() == "MSArtboardGroup") { dest = selection.firstObject(); linkLayer = selection.lastObject(); @@ -35,13 +48,19 @@ var defineLink = function(context) { linkLayer = selection.firstObject(); } else { - dest = context.reverse ? selection.lastObject() : selection.firstObject(); - linkLayer = context.reverse ? selection.firstObject() : selection.lastObject(); + if(alreadyReversed){ + reversing = false; + } + + dest = reversing ? selection.lastObject() : selection.firstObject(); + linkLayer = reversing ? selection.firstObject() : selection.lastObject(); } if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.parentArtboard() == dest) { validSelection = false; } + + } if (!validSelection) { @@ -49,8 +68,17 @@ var defineLink = function(context) { return; } + context.command.setValue_forKey_onLayer_forPluginIdentifier(dest.objectID(), "destinationID", linkLayer, kPluginDomain); + if(!reversing){ + context.command.setValue_forKey_onLayer_forPluginIdentifier(reversing, "reversed-"+dest.objectID(), linkLayer, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(reversing, "reversed-"+linkLayer.objectID(), dest, kPluginDomain); + } + else{ + context.command.setValue_forKey_onLayer_forPluginIdentifier(reversing, "reversed-"+dest.objectID(), linkLayer, kPluginDomain); + } + var doc = context.document; var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -70,14 +98,21 @@ var removeLink = function(context) { } var loop = context.selection.objectEnumerator(), - linkLayer, destinationID; + linkLayer, destinationID, destLayer; while (linkLayer = loop.nextObject()) { destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); if (!destinationID) { continue; } + destLayer = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); + + if(context.reverse == nil){ + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "reversed-"+destinationID, linkLayer, kPluginDomain); + context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "reversed-"+linkLayer.objectID(), destLayer, kPluginDomain); + } } var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -92,10 +127,10 @@ var removeLink = function(context) { var reverseLink = function(context) { var currentSelection = context.selection; - removeLink(context); - context.reverse = true; + removeLink(context); + defineLink(context); } From 8f2f51aece729f9e57b972f53748e5c48fe59147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Beauchamp?= Date: Fri, 3 Feb 2017 13:41:04 -0500 Subject: [PATCH 10/10] reverted to working reversing --- .../Contents/Sketch/script.js | 35 +++++-------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/UserFlows.sketchplugin/Contents/Sketch/script.js b/UserFlows.sketchplugin/Contents/Sketch/script.js index cc7b784..2c3bbad 100644 --- a/UserFlows.sketchplugin/Contents/Sketch/script.js +++ b/UserFlows.sketchplugin/Contents/Sketch/script.js @@ -36,16 +36,9 @@ var defineLink = function(context) { var selection = context.selection; var validSelection = true; - var dest, linkLayer, reversing, alreadyReversed; + var dest, linkLayer, reversal; - - reversing = context.reverse != nil ? context.reverse :false; - - if(context.command.valueForKey_onLayer_forPluginIdentifier("reversed-"+selection.lastObject().objectID(), selection.firstObject(), kPluginDomain) == 1 - || context.command.valueForKey_onLayer_forPluginIdentifier("reversed-"+selection.firstObject().objectID(), selection.lastObject(), kPluginDomain) == 1){ - - alreadyReversed = true; - } + reversal = context.reverse; if (selection.count() != 2) { validSelection = false; @@ -56,7 +49,7 @@ var defineLink = function(context) { reversing = false } - if (selection.firstObject().className() == "MSArtboardGroup" || selection.firstObject().className() == "MSSymbolMaster") { + if (selection.firstObject().className() == "MSArtboardGroup") { dest = selection.firstObject(); linkLayer = selection.lastObject(); } @@ -65,12 +58,8 @@ var defineLink = function(context) { linkLayer = selection.firstObject(); } else { - if(alreadyReversed){ - reversing = false; - } - - dest = reversing ? selection.lastObject() : selection.firstObject(); - linkLayer = reversing ? selection.firstObject() : selection.lastObject(); + dest = context.reverse ? selection.lastObject() : selection.firstObject(); + linkLayer = context.reverse ? selection.firstObject() : selection.lastObject(); } if (!dest || linkLayer.className() == "MSArtboardGroup" || linkLayer.className() == "MSSymbolMaster" || linkLayer.parentArtboard() == dest) { @@ -83,7 +72,6 @@ var defineLink = function(context) { return; } - context.command.setValue_forKey_onLayer_forPluginIdentifier(dest.objectID(), "destinationID", linkLayer, kPluginDomain); if(!reversing){ @@ -126,21 +114,14 @@ var removeLink = function(context) { } var loop = context.selection.objectEnumerator(), - linkLayer, destinationID, destLayer; + linkLayer, destinationID; while (linkLayer = loop.nextObject()) { destinationID = context.command.valueForKey_onLayer_forPluginIdentifier("destinationID", linkLayer, kPluginDomain); if (!destinationID) { continue; } - destLayer = doc.currentPage().children().filteredArrayUsingPredicate(NSPredicate.predicateWithFormat("objectID == %@", destinationID)).firstObject() - context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "destinationID", linkLayer, kPluginDomain); - - if(context.reverse == nil){ - context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "reversed-"+destinationID, linkLayer, kPluginDomain); - context.command.setValue_forKey_onLayer_forPluginIdentifier(nil, "reversed-"+linkLayer.objectID(), destLayer, kPluginDomain); - } } var showingConnections = NSUserDefaults.standardUserDefaults().objectForKey(kShowConnectionsKey) || 1; @@ -229,10 +210,10 @@ var confirmRelinkArtboards = function(context) { var reverseLink = function(context) { var currentSelection = context.selection; - context.reverse = true; - removeLink(context); + context.reverse = true; + defineLink(context); }