Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop

How to Create A GNOME Extension

5.00/5 (6 votes)
20 Jun 2020Public Domain10 min read 54.3K  
In this article, you will learn about the GNOME Shell extension basics, schema, gettext and more.
In this article, you will learn How to create a GNOME Shell extension. All examples are tested on GNOME Shell 3.36.1.

Image 1

What is a GNOME Extension?

GNOME extensions can be installed on GNOME desktop and extend the GNOME shell functionality.

What Do You Need to Know Before Starting?

You need to know some JavaScript and CSS before starting.

Can I Write My Extension in Other Languages?

GJS (Javascript Bindings for GNOME) is using JavaScript. Technically, you can write your back-end in any language you want but to show the end result in GNOME Shell, you need to use JavaScript.

Read and Watch

This article is a textual document for this video series.

If you need to understand the examples better, you can also watch the video while you are reading.

Extension Folder and Files

This is the main GNOME Shell extension folder:

~/.local/share/gnome-shell/extensions/

Inside this folder, you need to create a folder. Folder name should be the same as UUID (You will learn about that soon).

Here, we named our folder example1@example.com.

Inside example1@example.com folder, create these files:

plaintext
example1@example.com
├── metadata.json
├── extension.js
├── prefs.js [optional]
└── stylesheet.css [optional]

You can open these files with any text editor you like.

metadata.json

This file contains the extension information. You can create it like this:

JSON
{
    "name" : "Example#1",
    "description" : "Hello World",
    "shell-version" : [
        "3.36"
    ],
    "url" : "",
    "uuid" : "example1@example.com",
    "version" : 1.0
}
  • name string Extension Name
  • description string Extension Description
  • shell-version array Shell versions that Extension supports
  • url string GitLab or GitHub URL
  • uuid string Universally Unique Identifier.
  • version float Extension Version

extension.js

This is the main extension file and contains three main functions:

JavaScript
function init () {}
function enable () {}
function disable() {}
  • init will be called first to initiate your extension.
  • enable will be called when you enable the extension.
  • disable will be called when you disable the extension.

prefs.js

This is the main preferences file that loads a GTK window as your extension settings. Without this file, your extension won't have any settings dialogue.

We will talk about this file later.

stylesheet.css

This file contains CSS classes to style your elements.

Hello World

In this example, I’ll add a button to the top panel that shows a very simple text.

Inside extension.js file:

JavaScript
// 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);
}
  • We can have access to the GNOME shell elements such as panel by importing Main.

  • We can have access to the Elements such as Bin container by Importing St.

  • We can have access to the Clutter constants by importing Clutter.

  • In init function, we are simply creating a container with a Label in it.

  • On enable, we are adding the Bin container to the top panel (right side).

  • On disable, we are removing the Bin container from top panel.

Enable Your Extension

To see your newly extension in the extension list or if you modified the code and want to see the result:

  • X11 Press alt-f2, type r, press Enter to restart the GNOME shell.

  • Wayland Logout and re-login.

Now you can enable your extension and see the result.

Debug Your Extension

  • To Debug the Extension (extension.js), use this in terminal:
    journalctl -f -o cat /usr/bin/gnome-shell

  • To Debug the Extension Preferences (prefs), use this in terminal:
    journalctl -f -o cat /usr/bin/gnome-shell-extension-prefs

  • To log a message, use log:

    JavaScript
    log('Message');
  • To log a message with stack trace, use logError:

    JavaScript
    try {
      throw new Error('Message');
    } catch (e) {
      logError(e, 'ExtensionErrorType');
    }
  • To print your message in Stdout, use Print:

    JavaScript
    print('message');
  • To print your message in Stderr, use Printerr:

    JavaScript
    printerr('message');
  • To test, run gjs-console in terminal:
    gjs-console
    Please note, this is a separate process from GNOME shell and you cannot access the live code here.

  • To test and inspect use looking glass by pressing alt-f2, type lg and press enter. You can slow down the animation by running this (10 is slower than 1):
    St.set_slow_down_factor(10)

Documentation

Some Useful Basics

Folder and File path:

JavaScript
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);

Getting information from meta.json:

JavaScript
let extensionName = Me.metadata.name;
let extensionUUID = Me.metadata.uuid;

Import another js file:

JavaScript
const Me = imports.misc.extensionUtils.getCurrentExtension();
const OtherFile = Me.imports.otherfile;
let result = OtherFile.functionNameInsideOtherFile();

Send Notification:

JavaScript
const Main = imports.ui.main;
Main.notify('Message Title', 'Message Body');

Mainloop:

JavaScript
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);

Date and time with GLib:

JavaScript
const GLib = imports.gi.GLib;
let now = GLib.DateTime.new_now_local();
let nowString = now.format("%Y-%m-%d %H:%M:%S");

Gettext Translation

All strings that user can see should be translated through gettext.

Create these folders inside your extension folder:

plaintext
example@example.com
└── locale
    └── fr
        └── LC_MESSAGES
  • fr indicates to the French language (ISO 639-1 Code).

  • LC_MESSAGES holds the translation files.

JavaScript
// 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-like operation systems using gettext to translate. Here, we use gettext to translate.

  • We have created locale folder before. We can bind a text domain for that.

  • You can select the default text domain by textdomain().

  • When you use _ (gettext), the translated string will be returned. If the translation text doesn't exist in translation domain, the exact string will be returned.

  • For translating a string that can accept plural and singular form, use ngettext.

Now you need to extract all translatable strings from all js files. To create a sample file, you need to open terminal and use these commands:

Shell
cd PATH_TO_EXTENSION_FOLDER
xgettext --output=locale/example.pot *.js

Now you should have the example.pot file inside locale folder and you need to create a French translation from that sample file:

Shell
msginit --locale fr --input locale/example.pot --output locale/fr/LC_MESSAGES/example.po

Now you should have example.po file inside LC_MESSAGES folder. Open the file with a text editor and write your translated texts as msgstr. Don't forget to use the placeholder for plural form.

Save the file and go to the terminal:

Shell
cd PATH_TO_LC_MESSAGES_FOLDER
msgfmt example.po --output-file=example.mo

As you can see, filename is the same as text domain.

Now you have .mo file which is the translation compiled file and all translation should work as expected.

Schema

GSettings is an interface with back-end storage. You can consider it as database for your application.

If you open the dconf-editor and browse org/gnome/shell/extensions, you can see extensions are saving some data there. To save your data in that path, you need to create schema xml file.

Inside extension folder, create schemas folder. Inside that folder, create a file. Name it org.gnome.shell.extensions.example.gschema.xml. The file name should start with gsettings path and end with gschema.xml.

plaintext
example@example.com
└── schemas
    └── org.gnome.shell.extensions.example.gschema.xml

Inside xml file:

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 should start and end with slash.

  • Key indicates to gsettings key.

  • To define a type for each key, you can use these:

    • i Integer

    • d Double

    • b Boolean

    • s String

    • as Array of String

  • For enum, you need to create it with enum id first. The enum id should start with schema id and ends with enum name. use dash instead of white space for enum name. To define an enum key, instead of using type use enum id.

To compile the xml file, open terminal in your extension folder and do this:

Shell
glib-compile-schemas schemas/

Now you should have compiled file, inside schemas folder.

plaintext
example@example.com
└── schemas
    └── org.gnome.shell.extensions.example.gschema.xml
    └── gschemas.compiled

Now, inside extension.js file, you can have access to the schema like this:

JavaScript
// 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 () {}
  • On getSettings() function, we simply get gsettings object with the file we have created before.

  • We can get the values based on the key type. You can also set the value based on the key type.

  • For enum value, you should set the real value, not the nick.

Prefs

Create prefs.js file inside your extension folder.

plaintext
example@example.com
├── metadata.json
├── extension.js
└── prefs.js

Inside this file, you have two main functions:

  • init will be called first to initiate your preferences dialog.

  • buildPrefsWidget will create preferences widget and it should return a proper GTK wdiget like GTK box.

JavaScript
// 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);
  }

});
  • On MyPrefsWidget class, we simply create a box with a label and spin button.

  • On destroy, it is important to quit the main.

  • widget.show_all() shows all elements inside the main box.

  • With connect, you can connect the signals to a GTK object to monitor events.

Now if you go to the GNOME extension app. You should be able to see the settings button and your preferences dialog should load correctly.

Open Prefs Dialog from Terminal

You can open the extension window from terminal with UUID:

gnome-extensions prefs example@example.com

Load Prefs with Glade File

Glade can make the design process easier for you. Create the glade file and save it inside your extension folder (prefs.ui):

plaintext
example@example.com
├── metadata.json
├── extension.js
├── prefs.js
└── prefs.ui

Inside extension.js file:

JavaScript
// 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') );
  }
});
  • Instead of Gtk.Box, we use Gtk.ScrolledWindow as a wrapper.

  • To load a glade file, we use Gtk.Builder().

  • You can set the default translation domain for your glade file with builder.set_translation_domain().

  • To connect all of your signals to the handlers, you just need to use builder.connect_signals_full();

Performance Measurement

  • To measure the GNOME Shell performance, use Sysprof with /usr/bin/gnome-shell process.

  • If you want to measure the performance of specific part of your code, you can use the code I wrote. Download the performance.js file and save it inside your extension folder.

    Now you can import it to your project and measure the performance like this:

    JavaScript
    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 () {}

Create Another Panel

Here, I want to create a simple Bin container and add it to the stage.

Inside stylesheet.css file:

CSS
.bg-color {
  background-color : gold;
}

We use bg-color in our extension.js file to change the Bin color.

Inside extension.js file:

JavaScript
// 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);
}
  • To get the primary monitor resolution, you can use Main.layoutManager.primaryMonitor.

  • To add the Bin container to the GNOME shell stage, you can use addChrome(). You also have Main.uiGroup.add_child(container) and Main.uiGroup.remove_child(container) but you have more options with addChrome().

  • affectsInputRegion allows the container be on top when another object is under it.

  • affectsStruts can make the container just like top panel (snapping behavior, ...).

  • trackFullscreen allows the container hide when a window goes to full-screen.

Animation

You can easily animate an object with ease.

Inside extension.js file:

JavaScript
// 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);
}
  • You can use ease to animate the container.

  • Use opacity to change and animate the container opacity.

  • Use x and y to change the container position.

  • Use duration to specify animation duration.

  • Use mode to specify animation mode. You can find more animation modes in clutter documentation.

Panel Menu

To create a panel menu, you need to use PanelMenu from ui and add it with Main.panel.addToStatusArea() to the top panel.

Inside extension.js file:

JavaScript
// 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();
}
  • You have two options for creating an icon:

    • icon_name: Using icon from system icon theme

    • gicon: Loading the icon from icon file

  • You can create a menu item with PopupMenu.PopupMenuItem. The menu text is a label itself. You can also add another child to the menu item.

  • To add the menu item, use addMenuItem that you have inside PanelMenu.Button.menu.

  • To create separator, use PopupMenu.PopupSeparatorMenuItem.

  • Set reactive to false if you want to disable the menu item.

  • You can monitor the open and close menu event with open-state-changed.

  • addMenuItem accepts menu item order.

  • To create a section use PopupMenu.PopupMenuSection.

  • To add an item with icon, use PopupMenu.PopupImageMenuItem. The second parameter can be string (icon name) or gicon.

Key Binding

You can register a shortcut key with your extension. To do that, you need to create schema like this:

XML
<?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>
  • We stored super+g as array of string for our shortcut.

  • If you want to allow users to change the shortcut, you can get the shortcut key in prefs.

Inside extension.js file:

JavaScript
// 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");
}
  • We get the settings from our schema file.

  • To add a key binding, we need to send the settings that holds the shortcut key.

  • With Action Mode, you can specify where the shortcut should work.

  • With Key Binding Flags, you can specify how the shortcut key should assign. Usually we use NONE here.

  • You need to remove key binding on extension remove.

Drag and Drop

Here, I want to create two containers. First one is draggable and the second one is droppable.

JavaScript
// 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);
}
  • You should use this._delegate because DND needs it.

  • We have two types of drop: Copy and Move. In copy mode, it is good that you use restoreOnSuccess because even at success, the dragged item goes back to its first place. On Move, if you don't remove the dragged item and you set restoreOnSuccess to false, it will be removed automatically.

  • For drag monitor, we have two main events. dragMotion and dragDrop.

  • dragMotion should return a proper mouse cursor.

  • dragDrop can accept the drop with success. On fail or success, you need to release the drag manually.

  • acceptDrop returns Boolean.

  • I made this example as simple as possible but for adding or removing with chrome, you should check whether the container1 is in the container2 or not. In case it is inside container2, you only need to add or remove container2.


You can download the entire article (in MD Format) and all examples source code from my GitLab Repository.

Disclaimer: I’m not a GNOME developer. I just did this article to help the community.

History

  • 20th June, 2020: Initial version

 

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication