如何使用 JavaScript、HTML 和 CSS 构建 Sketch 插件(第 1 部分)

已发表: 2022-03-10
快速总结 ↬如果你曾经使用过 Sketch,很可能有很多时候你会想,“如果 Sketch 能做到这一点,我就能完成手头的任务更快、更容易、更好。” 好了,别再烦恼了! 在这篇由两部分组成的文章中,您将学习如何从头开始构建您自己的 Sketch 插件——为您提供解决这类问题所需的技能。

本教程适用于了解和使用 Sketch 应用程序并且不怕涉足代码的人。 要从中获得最大收益,您至少需要具备一些编写 JavaScript(以及可选的 HTML/CSS)的基本经验。

我们将要创建的插件称为“Mosaic”。 在第一部分,我们将了解构成 Sketch 插件的基本文件; 我们将编写一些 JavaScript 并在一些 HTML 和 CSS 的帮助下为我们的插件创建一个用户界面。 下一篇文章将介绍如何将用户界面连接到核心插件代码,如何实现插件的主要功能,最后,您还将学习如何优化代码以及插件的工作方式。

我还将分享插件的代码(JS、HTML、CSS)和文件,您将能够检查和用于学习目的。

什么是 Sketch 插件,它们是如何工作的?

在 Sketch 中,插件是一种“开箱即用”添加 Sketch 中不存在的特性和功能的方法。 考虑到任何给定程序中几乎总是会缺少一些功能或集成(特别是考虑到任何个人设计师可能有的大量需求!),人们可以开始想象插件如何特别有用和强大。 Sketch 插件几乎可以做所有你期望的事情,比如操纵图层的颜色、形状、大小、顺序、样式、分组和效果,还可以做一些事情,比如向互联网资源发出请求,向用户展示界面,还有更多!

在编程方面,所有 Sketch 插件都是用 JavaScript 代码编写的。 嗯,实际上,这并不完全正确。 更准确地说,大多数Sketch 插件都是用 JavaScript 编写的,因为也可以用 Apple 的一种编程语言 Objective-C 和 Swift 编写 Sketch 插件,尽管它们需要少量的 JavaScript 知识。

不过不用担心。 在本文中,我们将重点介绍如何单独使用 JavaScript、HTML 和 CSS 构建 Sketch 插件。 我们不会讨论 HTML、CSS 或 JavaScript 的基础知识——本文假设至少对这三者有一定的了解和经验。 MDN 开发者网站提供了一个了解 Web 开发更多信息的好地方。

跳跃后更多! 继续往下看↓

让我们开始吧!

首先,我们在做什么?

在本教程中,我将教您如何构建一个基本的、对初学者友好的插件,该插件将能够创建、复制和修改图层,并为用户提供一个漂亮的用户界面。 通过这样做,我的目标是建立一个基础知识,您可以在此基础上构建并使用它来创建自己的插件。

我们将要构建的插件称为Mosaic,它实际上是一个“模式生成器”。 喂它你的图层,调整一些设置,它会创建一个图案:

图片显示了 Mosaic 插件的 UI,以及一些示例模式。
Mosaic 的 UI,以及一些用它制作的模式示例。 (大预览)

如果您想安装和使用 Mosaic,可以从 GitHub 下载完整的插件。

一点历史:Mosaic 的灵感很大程度上来自一个名为Twist-and-Fade的老式 Adob​​e Fireworks 插件。 Twist-and-Fade 非常强大,能够在调整其色调、位置、旋转、大小和不透明度的同时任意多次复制图层。 该插件甚至能够生成动画 GIF,比如这个,它为盒式磁带中的两个旋转元素创建了帧:

显示带有旋转鼓的音乐盒式磁带的图像
动画盒式磁带(来源)。 (大预览)

(如果您有兴趣了解它的工作原理,这里有一段演示 Twist and Fade 的视频。)

出于本教程的目的,我们将为 Sketch 构建一个有点类似的插件,但有意简化以使教程尽可能易于访问。 具体来说,我们的插件将能够:

  • 复制任何 Sketch 图层(位图或矢量)并调整副本图层的位置、旋转和不透明度。 这将向我们介绍使用 Sketch 的 JavaScript API 操作图层。
  • 显示使用 HTML、CSS 和 JS 创建的用户界面,它将教您如何使用您可能已经熟悉的 Web 技术轻松地为插件创建界面。 插件界面非常重要,因为它是我们收集用户关于用户希望生成的马赛克图像外观的输入的方式。

在十秒内创建我们的基础插件

首先,我们将为我们要构建的插件创建“基础”(或模板)。 我们可以手动创建构成插件的所有必要文件和文件夹,但幸运的是我们不必这样做——因为 Sketch 可以为我们完成。 在我们生成模板插件之后,我们将能够根据我们认为合适的方式对其进行自定义。

我们可以使用一种非常快速和简单的技术来创建模板插件,当我需要将插件组合在一起以解决我在特定时刻处理的任何问题时,这几乎是我的首选方法。 以下是它的工作原理:

打开 Sketch,检查屏幕顶部的菜单栏,然后​​单击Plugins -> Run Script 。 这将打开一个对话框,我们可以使用它来测试和运行代码。 我们还可以将我们输入的任何代码保存为插件,这是我们现在特别感兴趣的部分。

清除此对话框中已有的任何代码,并将其替换为以下演示代码:

 const UI = require("sketch/ui"); UI.message(" Hey there, you fantastic plugin developer you! This is your plugin! Talking to you from the digital computer screen! In Sketch! Simply stupendous!");

接下来,点击窗口左下角的Save Script as Plugin ,输入您希望该插件具有的任何名称(在我们的例子中,这是“马赛克”),然后再次Save Script as Plugin

按窗口左下角的“保存”,然后输入您希望此插件具有的任何名称。 (大预览)

信不信由你,我们已经完成了——剩下的就是吃我们刚烤好的蛋糕。 有趣的部分来了。 再次打开插件菜单,您应该会看到如下内容:您的全新插件被列为“Mosaic”! 点击它!

(大预览)

恭喜,你刚刚编写了你的​​第一个 Sketch 插件!

点击“Mosaic”后你应该看到的应该和上面的短视频一样,屏幕底部会出现一条不显眼的工具提示消息,以“Hey there...”开头——这正是我们粘贴的代码告诉它的内容去做。 这就是它使这项技术如此出色的原因:它可以轻松粘贴、修改和测试代码,而无需从头开始构建插件。 如果您熟悉或曾经使用过浏览器的 Web 控制台,基本上就是这样。 在构建和测试代码时,必须拥有这个工具。

让我们快速了解一下您添加的代码的作用:

首先导入Sketch内置JS库的sketch/ui模块,赋值给UI变量。 该模块包含几个有用的与接口相关的方法,我们将使用其中一个:

 const UI = require("sketch/ui");

接下来,它使用我们希望在我们看到的工具提示中显示的文本字符串调用message方法(它是sketch/ui模块的一部分):

 UI.message(" Hey there, you fantastic plugin developer you! This is your plugin! Talking to you from the digital computer screen! In Sketch! Simply stupendous!");

message()方法提供了一种向用户呈现不显眼的消息的好方法; 它非常适合您不需要窃取焦点(非模态)并且不需要任何花哨的按钮或文本字段的情况。 还有其他方法可以呈现常见的 UI 元素,例如警报、提示等,其中一些我们将在构建 Mosaic 时使用。

自定义我们插件的元数据

我们现在有一个基本的插件可以开始,但我们仍然需要进一步调整它并使其真正成为我们的。 我们的下一步将是更改插件的元数据。

对于这一步,我们需要查看所谓的插件包。 当您在“运行脚本”窗口中点击保存时,Sketch 将您的插件保存为名为Mosaic.sketchplugin的文件夹,您可以在~/Library/Application Support/com.bohemiancoding.sketch3/Plugins目录中找到该文件夹​​。 记起来有点长而且烦人。 作为快捷方式,您还可以通过Plugins -> Manage Plugins -> (right-click your plugin) -> Reveal Plugins Folder将其拉起。 尽管它在 Finder 中显示为单个文件,但它实际上是一个文件夹,其中包含我们的插件运行 Sketch 所需的所有内容。 尽管它是一个文件夹,但它显示为单个文件的原因是,当您第一次安装 Sketch 时,Sketch 将.sketchplugin扩展名注册为“捆绑包”(一种显示为文件的特殊文件夹)并要求它自动打开打开时在草图中。

让我们看看里面。 右键单击Mosaic.sketchplugin ,然后单击“显示包内容”。 在内部,您应该看到以下目录结构:

 Contents/ └ Resources/ └ Sketch/ └ manifest.json └ script.cocoascript

您可能想知道为什么其中有一个扩展名为.cocoascript的文件。 别担心——它只是一个普通的 JavaScript 文件,只包含我们之前输入的代码。 继续将此文件重命名为index.js ,这会将目录结构更改为如下所示:

 Contents/ └ Resources/ └ Sketch/ └ manifest.json └ index.js

在插件包中组织文件的最常见方法如下:您的代码 (JavaScript) 和manifest.json属于Sketch/ ,资源(例如图像、音频文件、文本文件等)属于Resources/

让我们从调整名为manifest.json的文件开始。 在您喜欢的代码编辑器中打开它,例如 Visual Studio Code 或 Atom。

您会看到目前这里的内容相对较少,但我们很快会添加更多内容。 插件清单主要有两个目的:

  1. 首先,它向用户提供描述插件的元数据——比如它的名称、版本、作者姓名等。 Sketch 在Sketch -> Preferences -> Plugins对话框中使用此信息来为您的插件创建列表和描述。
  2. 其次,它还告诉 Sketch 如何开始你的业务; 也就是说,它告诉 Sketch 您希望插件菜单的外观、分配给插件的热键以及插件代码所在的位置(以便 Sketch 可以运行它)。

考虑到目的 #1,向用户描述插件,您可能会注意到现在没有给出描述作者,这会使用户感到困惑并使插件难以识别。 让我们通过将相关键的值调整为:

 { "description": "Generate awesome designs and repeating patterns from your layers!", "author": "=> Your name here <=" }

接下来,让我们调整插件的标识符。 此标识符使用所谓的“反向域表示法”,这是一种非常简洁(或无聊,随您选择)的方式,表示“获取您网站的域,颠倒顺序,然后将您的产品名称放在最后。” 这将出现如下内容: com.your-company-or-your-name-its-not-that-big-a-deal.yourproduct

你不必遵守这个命名约定——你可以在这里放任何你想要的东西,只要它足够独特以避免与其他插件发生冲突(尽管坚持 RDN 格式可能是一个好主意,特别是因为它提供一个简单的、可重用的插件标识符系统)。

为此,请将您的标识符更改为com.your-name.mosaic

 { "identifier": "com.your-name.mosaic" }

我个人喜欢获取所有与元数据相关的键(标题、作者、标识符等)并将它们分组到清单顶部附近,这样它们就不会散布在各处,并在我需要找到它们时帮助我保持理智.

接下来,我们来看看menucommands键。 这两个负责告诉 Sketch 调用什么代码以及响应什么。

如果您查看menu键,您会看到它包含一个title键,其值是我们的插件将在Plugins菜单中显示的名称。 它还有一个items键,它是一个命令标识符列表:

 { "menu": { "title": "Mosaic", "items": [ "com.bohemiancoding.sketch.runscriptidentifier" ] } }

现在这个列表中只有一个命令标识符, "com.bohemiancoding.sketch.runscriptidentifier" 。 命令标识符始终指向commands列表中的命令。 现在我们的插件只有一个命令,就是带有这个标识符的那个:

 { "commands": [ { "script" : "script.cocoascript", "name" : "Mosaic", "handlers" : { "run" : "onRun" }, "identifier" : "com.bohemiancoding.sketch.runscriptidentifier" } ] }

每当您将命令标识符添加到menu条目时,Sketch 将查找具有该标识符的命令条目并显示其name键的值(在本例中为“Mosaic”)并将其显示在插件的菜单中的标识符。

至于命令所扮演的角色,我们可以将命令条目视为一种方式来告诉 Sketch 在调用该命令时我们想要运行插件 JavaScript 代码中的哪个函数,“调用”通常是用户点击相关菜单物品。 命令条目本身不做任何事情,它只是 JSON——它只是向 Sketch 提供了一个描述,说明在调用命令时在哪里寻找它需要运行的 JavaScript。

到目前为止,我们已经讨论了命令的nameidentifier键的作用,但是命令中还有另外两个键需要处理: scripthandlers

script键告诉 Sketch 它应该运行的 JavaScript 文件在哪里。 请注意 Sketch 是如何假设有问题的脚本文件位于Sketch/文件夹中的,这就是为什么为了简单起见,您需要确保所有 JavaScript 代码都位于Sketch/文件夹下的某个位置。 在我们从这个键开始之前,重要的是你确保你将这个键的值更改为index.js ,就像我们之前重命名文件一样。 否则,Sketch 将无法找到并运行您的 JavaScript 文件。

handlers键的值是 Sketch 用来确定 JavaScript 中要调用的函数的值。 在这里,我们只有一个处理程序集: run ,其值为onRunrun是预定义的内置 Sketch动作的名称。 当用户单击引用此命令的菜单项时,将始终调用此run操作。 onRun是自动生成的script.cocoascript文件(我们将其重命名为index.js )中的函数名称,以及我们希望在run事件发生时调用的函数,即当用户单击菜单项时。

在我们目前的例子中,这个过程是这样的:

  1. 用户点击我们的菜单项。
  2. Sketch 查找与该菜单项关联的命令。
  3. Sketch 找到命令引用的脚本文件并运行它(在这种情况下,这意味着它执行index.js中的 JavaScript)。
  4. 由于此命令是由单击菜单项调用的,因此它被视为run操作。 这意味着 Sketch 将查看命令的handlers.run值以供下一个调用的函数,在本例中为onRun
  5. Sketch 调用onRun函数。

命令最常被调用以响应用户单击您的菜单项之一,但也可以调用它们以响应其他用户操作,例如用户更改选择或图层上的属性。 但是,对于这个插件,我们不会使用任何其他操作。 (您可以在 Action API 帮助页面中了解有关操作及其工作原理的更多信息。)

在我们从这个清单继续之前,我们需要做另外两个调整。 现在,我们的菜单结构如下:

 Mosaic └ Mosaic 
图像显示 Mosaic 菜单项冗余嵌套在另一个名为 Mosaic 的菜单中
相当多余,对吧? (大预览)

…这有点多余,因为我们的插件只有一个菜单项。 它还为我们的用户增加了一些不必要的摩擦,因为我们的插件现在需要两次点击才能调用,而不是一次。 我们可以通过添加isRoot: true到我们的menu来解决这个问题:

 { "menu": { "title" : "Mosaic", "items" : [ "com.bohemiancoding.sketch.runscriptidentifier" ], "isRoot": true } }

这告诉 Sketch 将第一级菜单项直接放在Plugins菜单下,而不是将它们嵌套在菜单的title下。

点击保存并返回到 Sketch。 您应该看到现在Mosaic -> Mosaic已被Mosaic取代——完美!

显示 Mosaic 插件 UI 的图像
马赛克的用户界面。 (大预览)

至于我们的第二个调整,让我们继续并将这个命令标识符重命名为不那么笨重的东西。 由于命令标识符只需要在单个插件的上下文中是唯一的,我们可以安全地将其重命名为更简洁明了的名称,例如"open"

 { "commands": [ { ... "identifier" : "open" } ], "menu": { ... "items" : [ "open" ] } }

在我们继续之前,请注意菜单也可以包含其他菜单。 您可以通过在另一个菜单的items列表中嵌套另一个{ title: ..., items: ... }条目来轻松创建子菜单:

 { "menu": { "title" : "Mosaic", "items" : [ "open", { "title" : "I'm a sub-menu!", "items" : [ "another-command-identifier" ] } ] } }

构建插件的用户界面

到目前为止,我们已经编写了一些演示代码并自定义了我们插件的清单。 我们现在将继续创建它的用户界面,它本质上是一个嵌入在窗口中的网页(类似于您熟悉使用的浏览器):

插件的窗口。 (大预览)
显示组成我们插件界面的组件的图像:窗口和 Web 视图
组成我们插件的组件。 (大预览)

窗户

Mosaic的用户界面设计有自己的窗口,我们可以认为是最基本的组件; 我们将从它开始。 为了创建和显示一个窗口,我们必须使用一个默认内置在 macOS 中的类,称为NSWindow 。 在本教程的其余部分,我们实际上会做很多事情(使用内置的 API,如NSWindow ),如果你不熟悉它,这可能看起来有点令人生畏,但别担心——我会解释一路上的一切!

注意:当我们谈论内置 API 时,我们能够使用这个类原因是由于 Sketch 插件使用的 JavaScript 运行时中存在的桥接器。 此桥会自动导入这些通常仅适用于本机应用程序的内置类、方法和函数。

在代码编辑器中打开Sketch/index.js ,删除已经存在的内容,然后粘贴以下内容:

 function onRun(context){ const window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( NSMakeRect(0, 0, 145, 500), NSWindowStyleMaskClosable | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable, NSBackingStoreBuffered, false ); window.releasedWhenClosed = false; window.makeKeyAndOrderFront(nil); };

让我们看一下第一段代码的作用:

 function onRun(context){

还记得之前我们谈到命令及其功能的时候,我们告诉 Sketch 响应菜单点击调用onRun吗? (如果您需要复习,请重新访问上面的那部分,然后再回来。)这部分所做的就是创建该函数。 您还会注意到我们的onRun函数带有一个context参数。 这是 Sketch 将调用您的命令处理程序的参数,可以为我们提供某些信息。 稍后,我们将使用它来获取用户计算机上插件包的 URL。

 const window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer( NSMakeRect(0, 0, 145, 500), NSWindowStyleMaskClosable | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable, NSBackingStoreBuffered, false );

在这里,我们实际上正在做一些事情:

  1. 首先,我们在NSWindow上调用alloc() ; 这基本上意味着“为 NSWindow 的实例留出一些内存”。 知道您必须为要创建的本地类的每个实例执行此操作就足够了。 alloc方法在每个本机类中都可用。
  2. 接下来,我们调用NSWindow的初始化方法(即实际创建NSWindow实例的方法),该方法名为initWithContentRect:styleMask:backing:defer: 。 你会注意到这与我们在上面的代码中调用的不同——每个参数之间都有一堆冒号( : 。 由于我们不能在 JavaScript 中使用该语法,Sketch 方便地将其重命名为我们可以实际使用的名称,方法是用下划线替换冒号,这就是我们获得其 JS 名称的方式: initWithContentRect_styleMask_backing_defer
  3. 接下来,我们传入该方法需要的每个参数。 对于第一个参数contentRect ,我们提供了一个大小足以容纳我们的用户界面的矩形。
  4. 对于styleMask ,我们使用一个位掩码,表示我们希望我们的窗口有一个关闭按钮、一个标题栏,并且可以调整大小。
  5. 接下来的两个参数backingdefer总是会设置为NSBackingStoreBufferedfalse ,所以我们真的不需要担心它们。 (此方法的文档更详细地说明了为什么会这样。)
 window.releasedWhenClosed = false; window.makeKeyAndOrderFront(null);

这里我们将NSWindowreleasedWhenClosed属性设置为false ,这意味着:“嘿! 不要仅仅因为用户关闭它就从内存中删除这个窗口。” 然后我们调用makeKeyAndOrderFront (null) ,意思是:“把这个窗口移到最前面,并给它键盘焦点。”

Web 视图:界面

为了让事情变得更简单,我已经编写了我们将要使用的插件 Web 用户界面的 HTML 和 CSS 代码; 我们需要添加的唯一剩余代码将确保我们能够在它和我们的 Sketch 插件代码之间进行通信。

接下来,下载 HTML 和 CSS 代码。 下载后,解压缩,然后将名为“web-ui”的文件夹移动到我们插件的 Resources 文件夹中。

注意编写和优化实际的 HTML/CSS 代码超出了本教程的范围,因为它的重点是支持插件核心功能的 JavaScript; 但是如果您想了解更多信息,网络上有大量关于此主题的教程。

如果你现在运行我们的插件,你会看到它显示了一个窗口——是的,进步! 但它是空的,没有标题,也不是特别有用。 我们需要让它显示我们的网络界面。 为此,我们需要使用另一个原生类WKWebView ,这是一个专门用于显示 Web 内容的视图。

我们将在为窗口编写的代码下方添加创建WKWebView所需的代码:

 function onRun(context){ // Create window const window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer( NSMakeRect(0, 0, 145, 500), NSWindowStyleMaskClosable | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable, NSBackingStoreBuffered, false ); window.releasedWhenClosed = false; // Create web view, and set it as the view for our window to display const webView = WKWebView.alloc().init(); window.contentView = webView; // Load our UI into the web view const webUIFolderURL = context.scriptURL .URLByDeletingLastPathComponent() .URLByAppendingPathComponent("../Resources/web-ui/"); const indexURL = webUIFolderURL.URLByAppendingPathComponent("index.html"); webView.loadFileURL_allowingReadAccessToURL(indexURL, webUIFolderURL); // Make window key and move to front window.makeKeyAndOrderFront(nil); };

如果我们现在运行我们的插件,我们会看到现在我们打开了一个窗口,显示我们的 Web 用户界面。 成功!

同样,在继续之前,让我们检查一下我们添加的代码的作用:

 const webView = WKWebView.alloc().init();

这看起来应该很熟悉——它与我们在创建NSWindow时所做的基本相同:为 Web 视图分配内存,然后对其进行初始化。

 window.contentView = webView;

这行代码告诉我们的窗口显示我们刚刚创建的 web 视图。

 const webUIFolderURL = context.scriptURL .URLByDeletingLastPathComponent() .URLByAppendingPathComponent("../Resources/web-ui/");

这里我们的目标是创建一个指向我们之前创建的web-ui文件夹的 URL。 为了获取该 URL,我们需要一些方法来确定我们的插件包在用户文件系统中的位置。 这里我们使用context.scriptURL属性,它为我们提供了当前正在运行的脚本的 URL。 但是,这并没有像您期望的那样给我们一个 JavaScript String ,而是一个本机类的实例NSURL ,它上面有一些方法可以使操作 URL 字符串更容易。

我们需要转换context.scriptURL给我们的东西——

 file://path-to-your-plugin/Contents/Sketch/index.js

- 进入:

 file://path-to-your-plugin/Contents/Resources/web-ui/

一步步:

  1. 第一次调用URLByDeletingLastPathComponent()会给我们file://path-to-your-plugin/Contents/Sketch/
  2. 再次调用URLByDeletingLastPathComponent()会给我们file://path-to-your-plugin/Contents/
  3. 最后,使用URLByAppendingPathComponent ("Resources/web-ui/")Resources/web-ui/添加到末尾会给我们file://path-to-your-plugin/Contents/Resources/web-ui/

我们还需要创建第二个直接指向index.html文件的 URL:

 const indexURL = webUIFolderURL.URLByAppendingPathComponent("index.html");

最后,我们告诉我们的 web 视图加载index.html并允许它访问web-ui文件夹的内容:

 webView.loadFileURL_allowingReadAccessToURL(indexURL, webUIFolderURL);

好的。 到目前为止,我们有一个窗口可以显示我们的 Web 用户界面,就像我们想要的那样。 然而,它还没有完全完成——我们的原始设计没有标题栏(或“chrome”),但我们当前的窗口有。 还有一个事实是,当我们在 Sketch 文档中单击时,该文档会移动到我们的窗口前面,这不是我们想要的——我们希望用户能够与插件窗口Sketch 文档进行交互,而不必不断地从一个窗口重新聚焦到另一个窗口。

为了解决这个问题,我们首先需要摆脱默认的窗口镶边,只保留按钮。 添加下面的两行代码将摆脱标题栏。

注意:和以前一样,我们在下面使用的所有属性和方法都记录在NSWindow的文档页面中。

 window.titlebarAppearsTransparent = true; window.titleVisibility = NSWindowTitleHidden;

接下来的两行代码将删除我们不需要的窗口按钮(在 MacOS 术语中也称为“交通灯”)——“缩放”和“最小化”——只留下“关闭”按钮:

 window.standardWindowButton(NSWindowZoomButton).hidden = true; window.standardWindowButton(NSWindowMiniaturizeButton).hidden = true;

在此过程中,让我们继续更改窗口的背景颜色以匹配我们的 Web UI:

 window.backgroundColor = NSColor.colorWithRed_green_blue_alpha(1, 0.98, 0.98, 1);

接下来,我们需要做一些事情来让我们的浮动插件窗口保持在其他窗口之上,这样用户就可以与他们的 Sketch 文档进行交互,而不必担心 Mosaic 的窗口消失。 我们可以为此使用一种特殊类型的NSWindow ,称为NSPanel ,它能够“保持在”其他窗口的顶部。 只需将NSWindow更改为NSPanel ,这是一个单行代码更改:

 const window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer(

现在我们告诉我们的面板窗口浮动(保持在所有其他窗口之上),并且仅在必要时获取键盘/鼠标焦点:

 window.floatingPanel = true; window.becomesKeyOnlyIfNeeded = true;

我们还可以调整我们的窗口,以便它自动重新打开它的最后一个位置:

 window.frameAutosaveName = "mosaic-panel-frame";

这条线基本上是说“记住这个窗口的位置,用 Sketch 的偏好将它保存在关键的mosaic-panel-frame下”。

总之,我们现在有以下代码:

 function onRun(context){ // Create window const window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer( NSMakeRect(0, 0, 145, 500), NSWindowStyleMaskClosable | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable, NSBackingStoreBuffered, false ); window.becomesKeyOnlyIfNeeded = true; window.floatingPanel = true; window.frameAutosaveName = "mosaic-panel-frame"; window.releasedWhenClosed = false; window.standardWindowButton(NSWindowZoomButton).hidden = true; window.standardWindowButton(NSWindowMiniaturizeButton).hidden = true; window.titlebarAppearsTransparent = true; window.titleVisibility = NSWindowTitleHidden; window.backgroundColor = NSColor.colorWithRed_green_blue_alpha(1, 0.98, 0.98, 1); // Create web view, and set it as the view for our window to display const webView = WKWebView.alloc().init(); window.contentView = webView; // Load our UI into the webview const webUIFolderURL = context.scriptURL .URLByDeletingLastPathComponent() .URLByAppendingPathComponent("../Resources/web-ui/"); const indexURL = webUIFolderURL.URLByAppendingPathComponent("index.html"); webView.loadFileURL_allowingReadAccessToURL(indexURL, webUIFolderURL); // Make window key and move to front window.makeKeyAndOrderFront(nil); };

组织代码

在我们进入下一部分之前,组织我们的代码是一个好主意,以便更容易导航和调整。 由于我们还有很多代码要添加,并且我们希望避免index.js成为我们所有代码的混乱垃圾场,让我们将它们拆分一下并将我们的 UI 特定代码移动到一个名为ui.js的文件中,在Sketch文件夹下。 我们还将提取我们所做的一些 UI 任务,例如创建 Web 视图和窗口,并将其提取到它们自己的函数中。

创建一个名为ui.js的新文件,并在其中插入以下代码:

 // Private var _window; function createWebView(pageURL){ const webView = WKWebView.alloc().init(); webView.loadFileURL_allowingReadAccessToURL(pageURL, pageURL.URLByDeletingLastPathComponent()); return webView; }; function createWindow(){ const window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer( NSMakeRect(0, 0, 420, 646), NSWindowStyleMaskClosable | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable, NSBackingStoreBuffered, false ); window.becomesKeyOnlyIfNeeded = true; window.floatingPanel = true; window.frameAutosaveName = "mosaic-panel-frame"; window.releasedWhenClosed = false; window.standardWindowButton(NSWindowZoomButton).hidden = true; window.standardWindowButton(NSWindowMiniaturizeButton).hidden = true; window.titlebarAppearsTransparent = true; window.titleVisibility = NSWindowTitleHidden; window.backgroundColor = NSColor.colorWithRed_green_blue_alpha(1, 0.98, 0.98, 1); return window; }; function showWindow(window){ window.makeKeyAndOrderFront(nil); }; // Public function loadAndShow(baseURL){ if(_window){ showWindow(_window); return; } const pageURL = baseURL .URLByDeletingLastPathComponent() .URLByAppendingPathComponent("../Resources/web-ui/index.html"); const window = createWindow(); const webView = createWebView(pageURL); window.contentView = webView; _window = window; showWindow(_window); }; function cleanup(){ if(_window){ _window.orderOut(nil); _window = null; } }; // Export module.exports = { loadAndShow, cleanup };

我们在此处进行了一些重要的更改,值得注意。 除了我们已经为创建、隐藏和显示我们的窗口及其 Web 视图创建了特定函数这一事实之外,我们还模块化了我们的用户界面代码。

注意底部的module.exports = { loadAndShow, cleanup }行吗? 这是一种让我们准确指定导入此 UI 代码的脚本可以使用哪些对象和函数的方法(并隐藏我们不想让他们担心的那些),这意味着我们现在有一个更有条理的 API 可以与之交互,显示和破坏我们的 UI。

推荐阅读在 Sketch 中释放符号的全部潜力

让我们看看这在实践中是什么样子的。 返回index.js ,删除旧代码并添加以下内容:

 const UI = require("./ui"); function onRun(context){ UI.loadAndShow(context.scriptURL); };

我们正在使用 Sketch 自动提供给我们的特殊功能require来导入我们的ui.js代码并将返回的模块分配给UI变量。 这让我们可以访问一个简化的 API 来触发我们的用户界面。 现在东西更整洁了,很容易找到!

结论

干得好——你已经走了很远! In the next part of this tutorial, we'll give our web UI the ability to send us a message when the “Apply” button is clicked, and we'll focus on the main plugin functionality: actually generating layer mosaics!