×

Rotterdam

Benjamin Franklinstraat 513
3029 AC Rotterdam

How to create floating Sketch plugins, part I

October 12th, 2017

We recently open-sourced Alembic, a Sketch plugin that extracts colors from images. When I started working on it, I knew that using a basic dialog window wasn’t going to work with the interface I had in mind for it. So, I experimented with other ways to create a custom interface for Sketch plugins and decided to use a floating panel with a WebView inside to create the actual interface.

Unfortunately, there’s not a lot of documentation around how to advancedly use Cocoa for Sketch plugins. In this article, we'll look into how to create a sample plugin with a floating panel containing a WebView and most importantly, how to make the WebView communicate with Sketch.

If you’re not familiar with building Sketch plugins, I highly recommend starting with Sketch’s Developer Site introduction and basic examples before reading this article. If you have any trouble or questions when experimenting around new ideas for plugins, I also recommend going to the Sketch Plugins forum created by Ale from Sketch, to see if someone already encountered the same problem.

The article is divided into two parts: the first part is about creating the panel and the interface, the second part will be about the communication between Sketch and the WebView.

Creating the panel

To do something more advanced than Sketch default dialogs, we’ll have to use Cocoa frameworks provided by macOS such as AppKit for interface elements and WebKit to create a WebView.

We’ll start by creating the panel itself, but before doing so, you can download the basic plugin structure to start with. We’re going to use AppKit’s NSPanel class, a subclass of NSWindow used to create secondary windows, which is exactly what we want our panel to be.

var panelWidth = 80;
var panelHeight = 240;

// Create the panel and set its appearance
var panel = NSPanel.alloc().init();
panel.setFrame_display(NSMakeRect(0, 0, panelWidth, panelHeight), true);
panel.setStyleMask(NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask | NSFullSizeContentViewWindowMask);
panel.setBackgroundColor(NSColor.whiteColor());

// Set the panel's title and title bar appearance
panel.title = "";
panel.titlebarAppearsTransparent = true;

// Center and focus the panel
panel.center();
panel.makeKeyAndOrderFront(null);
panel.setLevel(NSFloatingWindowLevel);

// Make the plugin's code stick around (since it's a floating panel)
COScript.currentCOScript().setShouldKeepAround_(true);

// Hide the Minimize and Zoom button
panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true);
panel.standardWindowButton(NSWindowZoomButton).setHidden(true);

We have a panel! The plain white background is a little bland to make it look like a window that belongs in macOS, though. Let’s add a blurred background with AppKit’s NSVisualEffectView class.

// Create the blurred background
var vibrancy = NSVisualEffectView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight));
vibrancy.setAppearance(NSAppearance.appearanceNamed(NSAppearanceNameVibrantLight));
vibrancy.setBlendingMode(NSVisualEffectBlendingModeBehindWindow);

// Add it to the panel
panel.contentView().addSubview(vibrancy);

The panel is now visually finished, but we have some issues: we can launch as many panels as we want and we can't close any of them.

To fix the possibility to launch multiple panels, we’ll store a reference to the first panel we open with Foundation’s NSThread. Now, we can check if there’s already a panel opened before running the plugin again.

// Create an NSThread dictionary with a specific identifier
var threadDictionary = NSThread.mainThread().threadDictionary();
var identifier = "co.awkward.floatingexample";

// If there's already a panel, prevent the plugin from running
if (threadDictionary[identifier]) return;

// After creating the panel, store a reference to it
threadDictionary[identifier] = panel;

The next step is to close the panel when the Close button is clicked, we’ll also remove the reference (so we can open it again) and stop the plugin.

var closeButton = panel.standardWindowButton(NSWindowCloseButton);

// Assign a function to the Close button
closeButton.setCOSJSTargetFunction(function(sender) {
  panel.close();

  // Remove the reference to the panel
  threadDictionary.removeObjectForKey(identifier);

  // Stop the plugin
  COScript.currentCOScript().setShouldKeepAround_(false);
});

Creating the interface

Now that we have the panel, we’ll use WebKit’s WebView class to add a Web page to it, located in the plugin’s Resources folder.

// Create the WebView with a request to a Web page in Contents/Resources/
var webView = WebView.alloc().initWithFrame(NSMakeRect(0, 0, panelWidth, panelHeight - 44));
var request = NSURLRequest.requestWithURL(context.plugin.urlForResourceNamed("webView.html"));
webView.mainFrame().loadRequest(request);

// Prevent it from drawing a white background
webView.setDrawsBackground(false);

// Add it to the panel
panel.contentView().addSubview(webView);

Then, we’ll add some CSS and JavaScript to the Web page to make it feel more inline with macOS.

html {
  box-sizing: border-box;
  background: transparent;

  /* Prevent the page to be scrollable */
  overflow: hidden;

  /* Force the default cursor, even on text */
  cursor: default;
}

*, *:before, *:after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
  position: relative;

  /* Prevent the content from being selectionable */
  user-select: none;
}
// Disable the context menu
document.addEventListener("contextmenu", function(e) {
  e.preventDefault();
});

You should now have a floating panel with a local Web page inside, congratulations! If not, everything is fine, here’s the code.

In the next part, we’ll see the most important part: how to make the WebView and Sketch communicate so we can actually do something with the interface we just made. Make sure to be there!