【CEP教程-6】面板与宿主之间的交互

【CEP教程-6】面板与宿主之间的交互

前面的文章我们讲到了插件面板是运行在CEF这个浏览器运行时下,而需要操作Photoshop的时候,要用到运行在宿主环境下的ExtendScript(以下简称JSX),那这两个独立的运行时之间是如何进行交互通信,且有哪些通信方式的呢,这篇文章我们来扒一扒。

面板调用宿主(JS -> JSX)

当我们在面板上点击一个按钮,然后让Photshop执行一个行为,这个时候,就需要JS调用JSX来完成,这个操作通过CSInterface提供的方法来完成,该对象提供了一个叫evalScript的方法,让我们可以在JS环境下执行一段JSX的代码,这里记得是执行一段代码,它好比原生JS的eval方法,将一串字符串代码执行,如下代码执行,就会在PS上出现一个alert弹窗:

1
2
var cs = new CSInterface();
cs.evalScript(`alert("Hi there")`);

这个函数提供了一个通往JSX世界的入口,但是光执行一段字符串代码显然在实际开发中是不够用的,因为我们的JSX代码通常都会很多很多,不可能全部都塞成一段字符串,于是我们一般将工程里的所有JSX文件都预先加载进来,然后暴露出函数,再通过evalScript函数去执行函数调用的过程。比如我在JSX文件里头有一个函数,该函数获取当前图层的名称:

1
2
3
4
5
6
// main.jsx
// 获取当前选中图层的名称
function getActiveLayerName() {
var doc = app.activeDocument;
return doc.activeLayer.name;
}

main.jsx文件已经有了,它放在我们的插件的jsx目录下

main.jsx

那我们如何将这个jsx文件加载进来呢,这里就涉及到前面文章【CEP教程-3】 CEP插件面板结构介绍提到的CSXS/manifest.xml文件中的配置

1
2
3
4
5
6
7
<Resources>
<MainPath>./index.html</MainPath>
<ScriptPath>./jsx/main.jsx</ScriptPath>
<CEFCommandLine>
<Parameter>--enable-nodejs</Parameter>
</CEFCommandLine>
</Resources>

里面有一个ScriptPath的配置,它就是插件在启动的时候载入的入口JSX文件,按照我们上面的配置,它就能加载main.jsx文件了,这就好比我们在做网页开发的时候用script标签引用js文件一样。

1
<script src="./jsx/main.jsx"></script>

加载了main.jsx文件之后,我们就可以随时随地调用getActiveLayerName函数了

1
cs.evalScript(`getActiveLayerName()`);

我们注意到getActiveLayerName函数return返回了当前的图层名称,这个JSX的数据返回,我们可以在evalScript函数的回调方法中获取,该回调函数会将JSX返回结果输出

1
2
3
cs.evalScript(`getActiveLayerName()`, function(result) {
console.log(result);
});

我们就可以在控制台中看到JSX返回的图层名称

evalScript

整体evalScript函数的使用非常简单,一看就会,有几个地方需要注意:

1. 参数传递

很多时候,我们需要在JS调用JSX的时候,传递一些参数进去,由于evalScript执行的是字符串,我们需要特别关注参数的拼接

1
2
// 传递基本类型
cs.evalScript(`foo("abc", 100)`);

如果传递的是对象类型参数,调用的时候需要转换成字符串,但是在JSX接收的地方并不需要做parse,系统会自动给你转化成对象

1
2
3
4
5
6
7
8
9
// JS
var params = {a: 100, b: "hi"};
cs.evalScript(`foo(${JSON.stringify(params)})`);

// JSX
function foo(params) {
// 不需要做JSON.parse(params);
alert(params.a); // 100
}

2. 错误

当我们的JSX代码执行错误的时候,evalScript返回的result就会显示 EvalScript error. ,如果我们不做处理就会导致面板的交互出现非预期的现象,于是我们可以对返回结果做一下判断

1
2
3
4
5
6
7
8
cs.evalScript(`fool()`, function(result) {
// EvalScript_ErrMessage 是一个定义在CSInterface.js中的一个常量,值就是EvalScript error.
if (result === EvalScript_ErrMessage) {
console.error(result);
} else {
// do your code stuff
}
});

总结: 上面我们介绍了插件面板主动调用宿主完成操作并返回结果的过程,这些过程都是面板主动发起的,那如果某些场景希望宿主主动发起一些动作让面板来处理呢,这里就涉及到插件开发过程中的事件了。

宿主调用面板 JSX->JS

说到事件,Photoshop && 插件系统提供了许多种事件场景和类型,在上一篇文章插件面板的样式中,我们就第一次说到了事件:为了能够监听Ps的主题发生变化,我们监听com.adobe.csxs.events.ThemeColorChanged事件

1. CSXSEvent

1
2
3
csInterface.addEventListener('com.adobe.csxs.events.ThemeColorChanged', () => {
syncTheme();
});

这些CSXSEvent是Photoshop自身派发的事件,它们都是CSXSEvent的一部分,它也给我们提供了自定义事件的能力,这个事件可以在JSX层进行派发,然后在JS层进行监听,这样就可以实现JSX->JS这样一条通道。我们来看看实现过程,CSXSEvent是在一个叫PlugPlug插件模块提供的,我们需要先进行加载,然后通过CSXSEvent对象来实现事件派发的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JSX

// 加载plugplug模块
try {
var xLib = new ExternalObject("lib:\PlugPlugExternalObject");
} catch (e) {
// do nothing
}

// 事件派发函数
function dispatch(message) {
var eventObj = new CSXSEvent();
eventObj.type = "my_custom_event_type";
eventObj.data = '[CSXSEvent] ' + message + '';
eventObj.dispatch()
}

// 给JS层发送事件
dispatch('message from jsx');

紧接着,和监听Ps主题变化的事件一样,我们在JS层监听事件名称my_custom_event_type,然后就可以在控制台看到输出的结果了

1
2
3
4
// JS
csInterface.addEventListener('my_custom_event_type', (data) => {
console.log(data);
});

CSXSEvent

至于my_custom_event_type只是一个示例,你可以根据情况自己去定义事件的类型来满足实际需要,这个事件通道非常适合用来打log调试使用,因为JSX本身并没有提供浏览器输出的console.log这样的日志打印功能,那我们就可以通过CSXSEvent来模拟一个

严格来说,上面的说法不对,JSX提供了$.write/$.writeln方法用来输出日志,但是只能在ExtendTookit和相关的官方debug环境下才能用

1
2
3
4
5
6
7
8
9
10
11
// JSX
var console = {
log: function(message) {
var eventObj = new CSXSEvent();
eventObj.type = "console_log_event";
eventObj.data = '[JSXLog] ' + message + '';
eventObj.dispatch();
}
};

console.log('log message from jsx');

2. CSEvent

上面我们介绍了CSXSEvent,它可以通过宿主进行派发,也支持自定义事件类型,从JSX层进行派发,JS层监听。但是有了这些还不够,自定义的事件类型,只能做一些自己代码内部的通信,很多时候,我们希望监听Ps的一些行为,比如用户选中了一个图层,切换了一个工具等,然后通过这些行为我们继续做一些操作。这就需要用到CSEvent,它是CSInterface里头给JS层提供的一个事件对象,通过它我们可以监听宿主的一些操作事件。

为了监听这些事件,我们先要派发一个事件!

对,你没看错,为了监听Ps的事件,我们需要先派发一个注册事件的事件

1
2
3
4
5
6
7
8
9
10
11
// JS
var appId = csInterface.getApplicationID();
var extId = csInterface.getExtensionID();

var csEvent = new CSEvent();
csEvent.type = 'com.adobe.PhotoshopRegisterEvent';
csEvent.scope = 'APPLICATION';
csEvent.appId = appId;
csEvent.extensionId = extId;
csEvent.data = data; // data 是某个事件的ID,下文详述
csInterface.dispatchEvent(csEvent);

上面这个事件的派发,就是告诉宿主,我要开始监听data这个事件了,接着我们监听它的回调

1
2
3
4
// JS
csInterface.addEventListener('com.adobe.PhotoshopJSONCallback' + extId, function(result) {
console.log(result);
});

在CC2015之前的版本,监听的事件名叫做 PhotoshopCallback,之后的版本事件名称叫’com.adobe.PhotoshopJSONCallback’ + extId

这样,当Ps发生指定的动作时候,我们就能收到回调了。

在上面的代码中data是个什么?它指明的是我们要关心的宿主发生的事件动作,那它到底填什么呢, 下面是一个监听图层选择事件的代码

1
2
3
4
5
6
7
8
9
10
11
// JS
csInterface.evalScript(`app.stringIDToTypeID('select')`, function (data) {
var csEvent = new CSEvent();
csEvent.type = 'com.adobe.PhotoshopRegisterEvent';
csEvent.scope = 'APPLICATION';
csEvent.appId = appId;
csEvent.extensionId = extId;
csEvent.data = data;
csInterface.dispatchEvent(csEvent);
});

从我们前面学习到的知识告诉我们,JS调用了JSX的函数app.stringIDToTypeID(‘select’),得到了一个data,它就是我们需要监听的事件ID,把它赋值给csEvent.data。我们大概可以猜出来select就是选择的意思,代表了选择图层这个操作,那app.stringIDToTypeID又是什么?这里涉及到ActionManager相关的知识,我们后续会专门开篇介绍,这里不做展开,只要记住需要这么做就行了。

代码设置好之后,我们通过切换选择图层,就能在控制台里头打印输出了

CSEvent output

从上面的日志输出可以看到当前图层选择的一些数据,通过加工处理这个返回的结果,就可以拿到当前图层的信息了。 通过这种机制,我们的示例项目实现了一个实时显示用户选择图层的功能,如下图。

layer selected event

总结

这篇文章介绍了Js和JSX之间的几种交互方式,JS层通过evalScript方法来执行JSX的代码,JSX通过CSXSEvent来派发事件通知JS,JS通过注册Ps的事件来监听Ps的指定行为。今天的所有代码都可以在下面github仓库找到

https://github.com/cutterman-cn/cep-panel-start

分支: js-with-jsx

后面的教程,我们会进入JSX核心,讲述JSX DOMAction Manager的使用,敬请期待~~

【CEP教程-6】面板与宿主之间的交互

https://uiscripting.com/2021/11/13/panel-communicate-with-host/

作者

小强

发布于

2021-11-13

更新于

2023-03-04

许可协议

评论