如何创建 GNOME 扩展。
在本文中,您将了解 GNOME Shell 扩展基础、模式、gettext 等。
什么是 GNOME 扩展?
GNOME 扩展可以安装在 GNOME 桌面上,并扩展 GNOME Shell 的功能。
开始之前需要了解什么?
在开始之前,您需要了解一些 JavaScript 和 CSS。
我可以用其他语言编写我的扩展吗?
GJS (GNOME 的 JavaScript 绑定) 使用 JavaScript。严格来说,您可以使用任何您想要的语言编写后端,但要将最终结果显示在 GNOME Shell 中,您需要使用 JavaScript。
阅读与观看
本文是关于这个视频系列的文本文档。
如果您需要更好地理解示例,也可以在阅读文章的同时观看视频。
扩展文件夹和文件
这是主要的 GNOME Shell 扩展文件夹
~/.local/share/gnome-shell/extensions/
在此文件夹内,您需要创建一个文件夹。文件夹名称应与*UUID*相同(您很快就会了解)。
在这里,我们将文件夹命名为*example1@example.com*。
在*example1@example.com*文件夹内,创建这些文件
example1@example.com
├── metadata.json
├── extension.js
├── prefs.js [optional]
└── stylesheet.css [optional]
您可以使用任何喜欢的文本编辑器打开这些文件。
metadata.json
此文件包含扩展信息。您可以这样创建它
{
"name" : "Example#1",
"description" : "Hello World",
"shell-version" : [
"3.36"
],
"url" : "",
"uuid" : "example1@example.com",
"version" : 1.0
}
name string
扩展名称description string
扩展描述shell-version array
扩展支持的 Shell 版本url string
GitLab 或 GitHub URLuuid string
通用唯一标识符。version float
扩展版本
extension.js
这是主要的扩展文件,包含三个主要函数
function init () {}
function enable () {}
function disable() {}
init
将首先被调用以初始化您的扩展。enable
将在您启用扩展时被调用。disable
将在您禁用扩展时被调用。
prefs.js
这是主要的首选项文件,它加载一个 GTK 窗口作为您的扩展设置。没有这个文件,您的扩展将没有任何设置对话框。
我们稍后将讨论这个文件。
stylesheet.css
此文件包含 CSS 类,用于设置元素的样式。
Hello World
在此示例中,我将向顶部面板添加一个显示非常简单文本的按钮。
在*extension.js*文件中
// Example #1
const {St, Clutter} = imports.gi;
const Main = imports.ui.main;
let panelButton;
function init () {
// Create a Button with "Hello World" text
panelButton = new St.Bin({
style_class : "panel-button",
});
let panelButtonText = new St.Label({
text : "Hello World",
y_align: Clutter.ActorAlign.CENTER,
});
panelButton.set_child(panelButtonText);
}
function enable () {
// Add the button to the panel
Main.panel._rightBox.insert_child_at_index(panelButton, 0);
}
function disable () {
// Remove the added button from panel
Main.panel._rightBox.remove_child(panelButton);
}
-
通过导入
Main
,我们可以访问 GNOME Shell 元素,例如面板。 -
通过导入 St,我们可以访问*Bin*容器等元素。
-
通过导入
Clutter
,我们可以访问 Clutter 常量。 -
在 init 函数中,我们只是创建一个带有
Label
的容器。 -
在
enable
中,我们将*Bin*容器添加到顶部面板(右侧)。 -
在
disable
中,我们从顶部面板中移除*Bin*容器。
启用您的扩展
要查看您新创建的扩展出现在扩展列表中,或者如果您修改了代码并想看到结果
-
X11 按 alt-f2,输入
r
,按 Enter 键重启 GNOME Shell。 -
Wayland 登出并重新登录。
现在您可以启用您的扩展并查看结果了。
调试您的扩展
-
要调试扩展(extension.js),请在终端中使用此命令
journalctl -f -o cat /usr/bin/gnome-shell -
要调试扩展首选项(prefs),请在终端中使用此命令
journalctl -f -o cat /usr/bin/gnome-shell-extension-prefs -
要记录一条消息,请使用
log
log('Message');
-
要记录带有堆栈跟踪的消息,请使用
logError
try { throw new Error('Message'); } catch (e) { logError(e, 'ExtensionErrorType'); }
-
要在
Stdout
中打印您的消息,请使用Print
print('message');
-
要在
Stderr
中打印您的消息,请使用Printerr
printerr('message');
-
要进行测试,请在终端中运行
gjs-console
gjs-console
请注意,这是与 GNOME Shell 分开的进程,您无法在此处访问实时代码。 -
要进行测试和检查,请按 alt-f2,输入
lg
,然后按enter键,使用looking glass。您可以运行此命令来减慢动画(10 比 1 慢)
St.set_slow_down_factor(10)
文档
-
目前,对于
ui
(在示例 #1 中使用)没有文档。您需要阅读源代码。
一些有用的基础知识
文件夹和文件路径
const Me = imports.misc.extensionUtils.getCurrentExtension();
let extensionFolderPath = Me.path();
let folderPath = Me.dir.get_child('folder').get_path();
let folderExists = Me.dir.get_child('folder').query_exists(null);
let fileExists = Me.dir.get_child('myfile.js').query_exists(null);
从*meta.json*获取信息
let extensionName = Me.metadata.name;
let extensionUUID = Me.metadata.uuid;
导入另一个 js 文件
const Me = imports.misc.extensionUtils.getCurrentExtension();
const OtherFile = Me.imports.otherfile;
let result = OtherFile.functionNameInsideOtherFile();
发送通知
const Main = imports.ui.main;
Main.notify('Message Title', 'Message Body');
主循环
const Mainloop = imports.mainloop;
let timeout = Mainloop.timeout_add_seconds(2.5, () => {
// this function will be called every 2.5 seconds
});
// remove mainloop
Mainloop.source_remove(timeout);
使用 GLib 获取日期和时间
const GLib = imports.gi.GLib;
let now = GLib.DateTime.new_now_local();
let nowString = now.format("%Y-%m-%d %H:%M:%S");
Gettext 翻译
所有用户可以看到的string
都应该通过gettext
进行翻译。
在您的扩展文件夹内创建这些文件夹
example@example.com
└── locale
└── fr
└── LC_MESSAGES
-
fr
指的是法语(ISO 639-1 代码)。 -
LC_MESSAGES
包含翻译文件。
// Example#2
const Me = imports.misc.extensionUtils.getCurrentExtension();
const Gettext = imports.gettext;
Gettext.bindtextdomain("example", Me.dir.get_child("locale").get_path());
Gettext.textdomain("example");
const _ = Gettext.gettext;
function init () {}
function enable () {
log(_("Hello My Friend"));
log(Gettext.ngettext("%d item", "%d items", 10).replace("%d", 10));
}
function disable () {}
-
类 Unix 操作系统使用
gettext
进行翻译。在这里,我们使用gettext
进行翻译。 -
我们之前已经创建了*locale*文件夹。我们可以为它绑定一个文本域。
-
您可以通过
textdomain()
选择默认文本域。 -
当您使用_(
gettext
)时,将返回翻译后的string
。如果翻译文本在翻译域中不存在,将返回确切的string
。 -
要翻译一个可以接受复数和单数形式的
string
,请使用ngettext
。
现在您需要提取所有 js 文件中可翻译的string
。要创建样本文件,您需要打开终端并使用这些命令
cd PATH_TO_EXTENSION_FOLDER
xgettext --output=locale/example.pot *.js
现在您应该在*locale*文件夹中有一个*example.pot*文件,您需要从该样本文件创建一个法语翻译。
msginit --locale fr --input locale/example.pot --output locale/fr/LC_MESSAGES/example.po
现在您应该在*LC_MESSAGES*文件夹中有一个*example.po*文件。用文本编辑器打开文件,并在msgstr
中写下您的翻译文本。不要忘记为复数形式使用占位符。
保存文件并在终端中执行
cd PATH_TO_LC_MESSAGES_FOLDER
msgfmt example.po --output-file=example.mo
如您所见,文件名与文本域相同。
现在您有了一个编译后的翻译文件*。mo*,所有翻译都应该按预期工作。
架构
GSettings
是一个与后端存储的接口。您可以将其视为应用程序的数据库。
如果您打开dconf-editor
并浏览org/gnome/shell/extensions,您可以看到扩展在那里保存了一些数据。要将您的数据保存在该路径中,您需要创建一个 schema xml 文件。
在扩展文件夹内,创建*schemas*文件夹。在该文件夹内,创建一个文件。将其命名为org.gnome.shell.extensions.example.gschema.xml。文件名应以*gsettings*路径开头,以*gschema.xml*结尾。
example@example.com
└── schemas
└── org.gnome.shell.extensions.example.gschema.xml
在 xml 文件内
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<enum id="org.gnome.shell.extensions.example.enum-sample">
<value value="0" nick="TOP"/>
<value value="1" nick="BOTTOM"/>
<value value="2" nick="RIGHT"/>
<value value="3" nick="LEFT"/>
</enum>
<schema id="org.gnome.shell.extensions.example"
path="/org/gnome/shell/extensions/example/">
<key type="i" name="my-integer">
<default>100</default>
<summary>summary</summary>
<description>description</description>
</key>
<key type="d" name="my-double">
<default>0.25</default>
<summary>summary</summary>
<description>description</description>
</key>
<key type="b" name="my-boolean">
<default>false</default>
<summary>summary</summary>
<description>description</description>
</key>
<key type="s" name="my-string">
<default>"My String Value"</default>
<summary>summary</summary>
<description>description</description>
</key>
<key type="as" name="my-array">
<default>['first', 'second']</default>
<summary>summary</summary>
<description>description</description>
</key>
<key name="my-position"
enum="org.gnome.shell.extensions.example.enum-sample">
<default>'LEFT'</default>
<summary>summary</summary>
<description>description</description>
</key>
</schema>
</schemalist>
-
Schema path 应以斜杠开头和结尾。
-
Key 指的是
gsettings
键。 -
要为每个键定义type,您可以使用这些
-
i Integer
-
d Double
-
b Boolean
-
s String
-
as Array of String
-
-
对于
enum
,您需要先使用 enum id 创建它。enum id 应以 schema id 开头并以 enum name 结尾。在 enum name 中使用连字符代替空格。要定义一个enum
键,而不是使用 type,而是使用 enum id。
要编译 xml 文件,请在您的扩展文件夹中打开终端并执行此操作
glib-compile-schemas schemas/
现在您应该在*schemas*文件夹中有了编译后的文件。
example@example.com
└── schemas
└── org.gnome.shell.extensions.example.gschema.xml
└── gschemas.compiled
现在,在*extension.js*文件中,您可以这样访问 schema
// Example#3
const Gio = imports.gi.Gio;
const Me = imports.misc.extensionUtils.getCurrentExtension();
function getSettings () {
let GioSSS = Gio.SettingsSchemaSource;
let schemaSource = GioSSS.new_from_directory(
Me.dir.get_child("schemas").get_path(),
GioSSS.get_default(),
false
);
let schemaObj = schemaSource.lookup(
'org.gnome.shell.extensions.example', true);
if (!schemaObj) {
throw new Error('cannot find schemas');
}
return new Gio.Settings({ settings_schema : schemaObj });
}
function init () {}
function enable () {
let settings = getSettings();
// my integer
//settings.set_int('my-integer', 200);
log("my integer:" + settings.get_int('my-integer'));
// my double
//settings.set_double('my-double', 2.1);
log("my double:" + settings.get_double('my-double'));
// my boolean
//settings.set_boolean('my-boolean', true);
log("my boolean:" + settings.get_boolean('my-boolean'));
// my string
//settings.set_string('my-string', 'new string');
log("my string:" + settings.get_string('my-string'));
// my array
//settings.set_strv('my-array', ['new', 'new2']);
let arr = settings.get_strv('my-array');
log("my array:" + arr[1]);
// my position
//settings.set_enum('my-position', 2);
log("my position:" + settings.get_enum('my-position'));
}
function disable () {}
-
在
getSettings()
函数中,我们使用之前创建的文件获取gsettings
对象。 -
我们可以根据键类型获取
values
。您也可以根据键类型设置值。 -
对于
enum
值,您应该设置实际值,而不是昵称。
首选项
在您的扩展文件夹内创建*prefs.js*文件。
example@example.com
├── metadata.json
├── extension.js
└── prefs.js
在此文件中,您有两个主要函数
-
init
将首先被调用以初始化您的首选项对话框。 -
buildPrefsWidget
将创建首选项小部件,并应返回一个合适的 GTK 小部件,如 GTK box。
// Example#4
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
function init () {}
function buildPrefsWidget () {
let widget = new MyPrefsWidget();
widget.show_all();
return widget;
}
const MyPrefsWidget = GObject.registerClass(
class MyPrefsWidget extends Gtk.Box {
_init (params) {
super._init(params);
this.margin = 20;
this.set_spacing(15);
this.set_orientation(Gtk.Orientation.VERTICAL);
this.connect('destroy', Gtk.main_quit);
let myLabel = new Gtk.Label({
label : "Translated Text"
});
let spinButton = new Gtk.SpinButton();
spinButton.set_sensitive(true);
spinButton.set_range(-60, 60);
spinButton.set_value(0);
spinButton.set_increments(1, 2);
spinButton.connect("value-changed", function (w) {
log(w.get_value_as_int());
});
let hBox = new Gtk.Box();
hBox.set_orientation(Gtk.Orientation.HORIZONTAL);
hBox.pack_start(myLabel, false, false, 0);
hBox.pack_end(spinButton, false, false, 0);
this.add(hBox);
}
});
-
在
MyPrefsWidget
类中,我们只是创建一个带有标签和 spin button 的 box。 -
在
destroy
中,退出主循环很重要。 -
widget.show_all()
显示主 box 内的所有元素。 -
使用
connect
,您可以将信号连接到 GTK 对象以监视事件。
现在,如果您转到 GNOME 扩展应用程序,您应该能看到设置按钮,并且您的首选项对话框应该能正确加载。
从终端打开首选项对话框
您可以使用 UUID 从终端打开扩展窗口
gnome-extensions prefs example@example.com
使用 Glade 文件加载首选项
Glade 可以让您的设计过程更轻松。创建 glade 文件并将其保存在您的扩展文件夹中(prefs.ui)
example@example.com
├── metadata.json
├── extension.js
├── prefs.js
└── prefs.ui
在*extension.js*文件中
// Example#5
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Me = imports.misc.extensionUtils.getCurrentExtension();
function init () {}
function buildPrefsWidget () {
let widget = new MyPrefsWidget();
widget.show_all();
return widget;
}
const MyPrefsWidget = GObject.registerClass(
class MyPrefsWidget extends Gtk.ScrolledWindow {
_init (params) {
super._init(params);
let builder = new Gtk.Builder();
builder.set_translation_domain('example');
builder.add_from_file(Me.path + '/prefs.ui');
this.connect("destroy", Gtk.main_quit);
let SignalHandler = {
on_my_spinbutton_value_changed (w) {
log(w.get_value_as_int());
},
on_my_switch_state_set (w) {
log(w.get_active());
}
};
builder.connect_signals_full((builder, object, signal, handler) => {
object.connect(signal, SignalHandler[handler].bind(this));
});
this.add( builder.get_object('main_prefs') );
}
});
-
我们使用
Gtk.ScrolledWindow
作为包装器,而不是Gtk.Box
。 -
要加载 glade 文件,我们使用
Gtk.Builder()
。 -
您可以使用
builder.set_translation_domain()
为您的 glade 文件设置默认翻译域。 -
要连接所有信号到处理程序,您只需使用
builder.connect_signals_full()
;
性能测量
-
要测量 GNOME Shell 的性能,请使用
Sysprof
以及/usr/bin/gnome-shell进程。 -
如果您想测量代码特定部分的性能,可以使用我编写的代码。下载performance.js文件并将其保存在您的扩展文件夹中。
现在您可以将其导入您的项目并像这样测量性能
const Me = imports.misc.extensionUtils.getCurrentExtension(); const performance = Me.imports.performance; function init () {} function enable () { Performance.start('Test1'); // code to measure Performance.end(); } function disable () {}
创建另一个面板
在这里,我想创建一个简单的*Bin*容器并将其添加到 stage。
在*stylesheet.css*文件中
.bg-color {
background-color : gold;
}
我们在*extension.js*文件中使用bg-color
来改变*Bin*的颜色。
在*extension.js*文件中
// Example#6
const Main = imports.ui.main;
const St = imports.gi.St;
let container;
function init () {
let pMonitor = Main.layoutManager.primaryMonitor;
container = new St.Bin({
style_class : 'bg-color',
reactive : true,
can_focus : true,
track_hover : true,
height : 30,
width : pMonitor.width,
});
container.set_position(0, pMonitor.height - 30);
container.connect("enter-event", () => {
log('entered');
});
container.connect("leave-event", () => {
log('left');
});
container.connect("button-press-event", () => {
log('clicked');
});
}
function enable () {
Main.layoutManager.addChrome(container, {
affectsInputRegion : true,
affectsStruts : true,
trackFullscreen : true,
});
}
function disable () {
Main.layoutManager.removeChrome(container);
}
-
要获取主显示器的分辨率,您可以使用
Main.layoutManager.primaryMonitor
。 -
要将*Bin*容器添加到 GNOME Shell stage,您可以使用
addChrome()
。您还可以使用Main.uiGroup.add_child(container)
和Main.uiGroup.remove_child(container)
,但使用addChrome()
有更多选项。 -
affectsInputRegion
允许当另一个对象在其下方时,容器保持在顶部。 -
affectsStruts
可以使容器像顶部面板一样(吸附行为等)。 -
trackFullscreen
允许当窗口进入全屏时隐藏容器。
动画
您可以使用ease轻松地动画一个对象。
在*extension.js*文件中
// Example#7
const Main = imports.ui.main;
const St = imports.gi.St;
const Clutter = imports.gi.Clutter;
let container;
function init () {
let monitor = Main.layoutManager.primaryMonitor;
let size = 100;
container = new St.Bin({
style: 'background-color: gold',
reactive : true,
can_focus : true,
track_hover : true,
width: size,
height: size,
});
container.set_position(monitor.width-size, monitor.height-size);
container.connect("button-press-event", () => {
let [xPos, yPos] = container.get_position();
let newX = (xPos === 0) ? monitor.width-size : 0;
container.ease({
x: newX,
//y: 10,
//opacity: 100,
duration: 2000,
mode: Clutter.AnimationMode.EASE_OUT_BOUNCE,
onComplete: () => {
log('Animation is finished');
}
});
});
}
function enable () {
Main.layoutManager.addChrome(container, {
affectsInputRegion : true,
trackFullscreen : true,
});
}
function disable () {
Main.layoutManager.removeChrome(container);
}
-
您可以使用
ease
来动画容器。 -
使用
opacity
来更改和动画容器的不透明度。 -
使用
x
和y
来更改容器的位置。 -
使用
duration
来指定动画持续时间。 -
使用
mode
来指定动画模式。您可以在clutter 文档中找到更多动画模式。
面板菜单
要创建面板菜单,您需要使用ui
中的PanelMenu
,并使用Main.panel.addToStatusArea()
将其添加到顶部面板。
在*extension.js*文件中
// Example#8
const Main = imports.ui.main;
const St = imports.gi.St;
const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Me = imports.misc.extensionUtils.getCurrentExtension();
let myPopup;
const MyPopup = GObject.registerClass(
class MyPopup extends PanelMenu.Button {
_init () {
super._init(0);
let icon = new St.Icon({
//icon_name : 'security-low-symbolic',
gicon : Gio.icon_new_for_string( Me.dir.get_path() + '/icon.svg' ),
style_class : 'system-status-icon',
});
this.add_child(icon);
let pmItem = new PopupMenu.PopupMenuItem('Normal Menu Item');
pmItem.add_child(new St.Label({text : 'Label added to the end'}));
this.menu.addMenuItem(pmItem);
pmItem.connect('activate', () => {
log('clicked');
});
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addMenuItem(
new PopupMenu.PopupMenuItem(
"User cannot click on this item",
{reactive : false},
)
);
this.menu.connect('open-state-changed', (menu, open) => {
if (open) {
log('opened');
} else {
log('closed');
}
});
// sub menu
let subItem = new PopupMenu.PopupSubMenuMenuItem('sub menu item');
this.menu.addMenuItem(subItem);
subItem.menu.addMenuItem(new PopupMenu.PopupMenuItem('item 1'));
subItem.menu.addMenuItem(new PopupMenu.PopupMenuItem('item 2'), 0);
// section
let popupMenuSection = new PopupMenu.PopupMenuSection();
popupMenuSection.actor.add_child(new PopupMenu.PopupMenuItem('section'));
this.menu.addMenuItem(popupMenuSection);
// image item
let popupImageMenuItem = new PopupMenu.PopupImageMenuItem(
'Menu Item with Icon',
'security-high-symbolic',
);
this.menu.addMenuItem(popupImageMenuItem);
// you can close, open and toggle the menu with
// this.menu.close();
// this.menu.open();
// this.menu.toggle();
}
});
function init() {
}
function enable() {
myPopup = new MyPopup();
Main.panel.addToStatusArea('myPopup', myPopup, 1);
}
function disable() {
myPopup.destroy();
}
-
创建图标有两种选择
-
icon_name
:使用系统图标主题中的图标 -
gicon
:从图标文件加载图标
-
-
您可以使用
PopupMenu.PopupMenuItem
创建菜单项。菜单文本本身就是一个标签。您也可以向菜单项添加另一个子项。 -
要添加菜单项,请使用
PanelMenu.Button.menu
中的addMenuItem
。 -
要创建分隔符,请使用
PopupMenu.PopupSeparatorMenuItem
。 -
如果您想禁用菜单项,请将
reactive
设置为false
。 -
您可以使用
open-state-changed
来监视菜单的打开和关闭事件。 -
addMenuItem
接受菜单项的顺序。 -
要创建分区,请使用
PopupMenu.PopupMenuSection
。 -
要添加带有图标的项目,请使用
PopupMenu.PopupImageMenuItem
。第二个参数可以是string
(*图标名称*)或*gicon*。
键盘绑定
您可以为您的扩展注册一个快捷键。要做到这一点,您需要创建一个像这样的schema
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.example9"
path="/org/gnome/shell/extensions/example9/">
<key type="as" name="my-shortcut">
<default><![CDATA[['<Super>g']]]></default>
<summary>The shortcut key</summary>
<description>
You can create GTK entry in prefs and set it as CDATA.
</description>
</key>
</schema>
</schemalist>
-
我们将
super+g
存储为快捷键的字符串数组。 -
如果您想允许用户更改快捷键,您可以在
prefs
中获取快捷键。
在*extension.js*文件中
// Example#9
const {Gio, Shell, Meta} = imports.gi;
const Main = imports.ui.main;
const Me = imports.misc.extensionUtils.getCurrentExtension();
function getSettings () {
let GioSSS = Gio.SettingsSchemaSource;
let schemaSource = GioSSS.new_from_directory(
Me.dir.get_child("schemas").get_path(),
GioSSS.get_default(),
false
);
let schemaObj = schemaSource.lookup(
'org.gnome.shell.extensions.example9', true);
if (!schemaObj) {
throw new Error('cannot find schemas');
}
return new Gio.Settings({ settings_schema : schemaObj });
}
function init () {}
function enable () {
// Shell.ActionMode.NORMAL
// Shell.ActionMode.OVERVIEW
// Shell.ActionMode.LOCK_SCREEN
// Shell.ActionMode.ALL
let mode = Shell.ActionMode.ALL;
// Meta.KeyBindingFlags.NONE
// Meta.KeyBindingFlags.PER_WINDOW
// Meta.KeyBindingFlags.BUILTIN
// Meta.KeyBindingFlags.IGNORE_AUTOREPEAT
let flag = Meta.KeyBindingFlags.NONE;
let settings = getSettings();
Main.wm.addKeybinding("my-shortcut", settings, flag, mode, () => {
log('shortcut is working');
});
}
function disable () {
Main.wm.removeKeybinding("my-shortcut");
}
-
我们从
schema
文件中获取设置。 -
要添加键盘绑定,我们需要发送包含快捷键的
settings
。 -
使用Action Mode,您可以指定快捷键应该在哪里工作。
-
使用Key Binding Flags,您可以指定如何分配快捷键。通常我们在这里使用 NONE。
-
您需要在扩展移除时移除键盘绑定。
拖放
在这里,我想创建两个容器。第一个是可拖动的,第二个是可放置的。
// Example#10
const {St, GObject} = imports.gi;
const Main = imports.ui.main;
const DND = imports.ui.dnd;
let container1, container2;
const MyContainer1 = GObject.registerClass(
class MyContainer1 extends St.Bin {
_init () {
super._init({
style : 'background-color : gold',
reactive : true,
can_focus : true,
track_hover : true,
width : 120,
height : 120,
x : 0,
y : 0,
});
this._delegate = this;
this._draggable = DND.makeDraggable(this, {
//restoreOnSuccess : true,
//manualMode : false,
//dragActorMaxSize : 80,
//dragActorOpacity : 200,
});
this._draggable.connect("drag-begin", () => {
log("DRAG BEGIN");
this._setDragMonitor(true);
});
this._draggable.connect("drag-end", () => {
log("DRAG END");
this._setDragMonitor(false);
});
this._draggable.connect("drag-cancelled", () => {
log("DRAG CANCELLED");
this._setDragMonitor(false);
});
this.connect("destroy", () => {
this._setDragMonitor(false);
});
}
_setDragMonitor (add) {
if (add) {
this._dragMonitor = {
dragMotion : this._onDragMotion.bind(this),
//dragDrop : this._onDragDrop.bind(this),
};
DND.addDragMonitor(this._dragMonitor);
} else if (this._dragMonitor) {
DND.removeDragMonitor(this._dragMonitor);
}
}
_onDragMotion (dragEvent) {
if (dragEvent.targetActor instanceof MyContainer2) {
return DND.DragMotionResult.MOVE_DROP;
}
// DND.DragMotionResult.COPY_DROP
// DND.DragMotionResult.MOVE_DROP
// DND.DragMotionResult.NO_DROP
// DND.DragMotionResult.CONTINUE
return DND.DragMotionResult.CONTINUE;
}
_onDragDrop (dropEvent) {
// DND.DragDropResult.FAILURE
// DND.DragDropResult.SUCCESS
// DND.DragDropResult.CONTINUE
return DND.DragDropResult.CONTINUE;
}
});
const MyContainer2 = GObject.registerClass(
class MyContainer2 extends St.Bin {
_init () {
super._init({
style : 'background-color : lime',
reactive : true,
can_focus : true,
track_hover : true,
width : 120,
height : 120,
x : 0,
y : 750,
});
this._delegate = this;
}
acceptDrop (source, actor, x, y, time) {
if (!source instanceof MyContainer1) {
return false;
}
source.get_parent().remove_child(source);
this.set_child(source);
log('Drop has been accepted');
return true;
}
});
function init () {
container1 = new MyContainer1();
container2 = new MyContainer2();
}
function enable () {
let chromeSettings = {
affectsInputRegion : true,
trackFullscreen : true,
};
Main.layoutManager.addChrome(container1, chromeSettings);
Main.layoutManager.addChrome(container2, chromeSettings);
}
function disable () {
Main.layoutManager.removeChrome(container1);
Main.layoutManager.removeChrome(container2);
}
-
您应该使用
this._delegate
,因为 DND 需要它。 -
我们有两种类型的放置:复制和移动。在复制模式下,最好使用
restoreOnSuccess
,因为即使成功,拖动的项目也会回到它的初始位置。在移动模式下,如果您不移除拖动的项目并将restoreOnSuccess
设置为false
,它将被自动移除。 -
对于拖动监视器,我们有两个主要事件。
dragMotion
和dragDrop
。 -
dragMotion
应该返回一个合适的光标。 -
dragDrop
可以成功接受放置。在失败或成功时,您需要手动释放拖动。 -
acceptDrop
返回布尔值。 -
我尽可能简化了这个例子,但对于使用chrome添加或删除,您应该检查
container1
是否在container2
中。如果它在container2
中,您只需要添加或删除container2
。
您可以从我的GitLab 仓库下载完整的文章(MD 格式)和所有示例源代码。
免责声明:我不是 GNOME 开发者。我写这篇文章只是为了帮助社区。
历史
- 2020 年 6 月 20 日:初始版本