65.9K
CodeProject 正在变化。 阅读更多。
Home

如何创建 GNOME 扩展。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2020 年 6 月 20 日

公共领域

10分钟阅读

viewsIcon

55952

在本文中,您将了解 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 URL
  • uuid 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*容器。

启用您的扩展

要查看您新创建的扩展出现在扩展列表中,或者如果您修改了代码并想看到结果

  • X11alt-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)

文档

一些有用的基础知识

文件夹和文件路径

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(&#39;Test1&#39;);
     // 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来更改和动画容器的不透明度。

  • 使用xy来更改容器的位置。

  • 使用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,它将被自动移除。

  • 对于拖动监视器,我们有两个主要事件。dragMotiondragDrop

  • dragMotion应该返回一个合适的光标。

  • dragDrop可以成功接受放置。在失败或成功时,您需要手动释放拖动。

  • acceptDrop返回布尔值。

  • 我尽可能简化了这个例子,但对于使用chrome添加或删除,您应该检查container1是否在container2中。如果它在container2中,您只需要添加或删除container2


您可以从我的GitLab 仓库下载完整的文章(MD 格式)和所有示例源代码

免责声明:我不是 GNOME 开发者。我写这篇文章只是为了帮助社区。

历史

  • 2020 年 6 月 20 日:初始版本

 

© . All rights reserved.