Skip to content

The Basics of GObject

GObject is the base upon which most of the GNOME platform is built. This is a gentle introduction to using GObject in GJS, including constructing objects, using properties and connecting to signals.

GObject Construction

TIP

In rare cases, like the Gio.File interface, objects can not be constructed with the new operator and a constructor method must always be used.

The most common way to create a new GObject is using the new operator. When constructing a GObject this way, you can pass a dictionary of properties:

js
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const cancelLabel = new Gtk.Label({
    label: '_Cancel',
    use_underline: true,
});

const saveLabel = Gtk.Label.new_with_mnemonic('_Save');

Many classes also have static constructor methods you can use directly:

js
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const cancelLabel = new Gtk.Label({
    label: '_Cancel',
    use_underline: true,
});

const saveLabel = Gtk.Label.new_with_mnemonic('_Save');

Properties

GObject supports a property system that is slightly more powerful than native JavaScript properties.

Accessing Properties

GObject properties may be retrieved and set using native property style access or GObject get and set methods.

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const invisibleLabel = new Gtk.Label({
    visible: false,
});
let visible;

// Three different ways to get or set properties
visible = invisibleLabel.visible;
visible = invisibleLabel['visible'];
visible = invisibleLabel.get_visible();

invisibleLabel.visible = false;
invisibleLabel['visible'] = false;
invisibleLabel.set_visible(false);


const markupLabel = new Gtk.Label({
    label: '<i>Italics</i>',
    use_markup: true,
});
let useMarkup;

// If using native accessors, you can use `underscore_case` or `camelCase`
useMarkup = markupLabel.use_markup;
useMarkup = markupLabel.useMarkup;

// Anywhere the property name is a string, you must use `kebab-case`
markupLabel['use-markup'] = true;
markupLabel.connect('notify::use-markup', () => {});

// Getter and setter functions are always case sensitive
useMarkup = markupLabel.get_use_markup();
markupLabel.set_use_markup(true);


const changingLabel = Gtk.Label.new('Original Label');

const labelId = changingLabel.connect('notify::label', (object, _pspec) => {
    console.log(`New label is "${object.label}"`);
});


const prefsTitle = new Gtk.Label({
    label: 'Preferences',
    css_classes: ['heading'],
});
const prefsBox = new Gtk.Box();

// Bind the visibility of the box and label
prefsTitle.bind_property('visible', prefsBox, 'visible',
    GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

// Try to make the properties different
prefsTitle.visible = !prefsBox.visible;

if (prefsTitle.visible === prefsBox.visible)
    console.log('properties are equal!');


const searchEntry = new Gtk.Entry();
const searchButton = new Gtk.Button({
    label: 'Go',
});

searchEntry.bind_property_full('text', searchButton, 'sensitive',
    GObject.BindingFlags.DEFAULT,
    (binding, value) => [true, !!value],
    null);

GObject property names have a canonical form that is kebab-cased, however they are accessed differently depending on the situation:

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const invisibleLabel = new Gtk.Label({
    visible: false,
});
let visible;

// Three different ways to get or set properties
visible = invisibleLabel.visible;
visible = invisibleLabel['visible'];
visible = invisibleLabel.get_visible();

invisibleLabel.visible = false;
invisibleLabel['visible'] = false;
invisibleLabel.set_visible(false);


const markupLabel = new Gtk.Label({
    label: '<i>Italics</i>',
    use_markup: true,
});
let useMarkup;

// If using native accessors, you can use `underscore_case` or `camelCase`
useMarkup = markupLabel.use_markup;
useMarkup = markupLabel.useMarkup;

// Anywhere the property name is a string, you must use `kebab-case`
markupLabel['use-markup'] = true;
markupLabel.connect('notify::use-markup', () => {});

// Getter and setter functions are always case sensitive
useMarkup = markupLabel.get_use_markup();
markupLabel.set_use_markup(true);


const changingLabel = Gtk.Label.new('Original Label');

const labelId = changingLabel.connect('notify::label', (object, _pspec) => {
    console.log(`New label is "${object.label}"`);
});


const prefsTitle = new Gtk.Label({
    label: 'Preferences',
    css_classes: ['heading'],
});
const prefsBox = new Gtk.Box();

// Bind the visibility of the box and label
prefsTitle.bind_property('visible', prefsBox, 'visible',
    GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

// Try to make the properties different
prefsTitle.visible = !prefsBox.visible;

if (prefsTitle.visible === prefsBox.visible)
    console.log('properties are equal!');


const searchEntry = new Gtk.Entry();
const searchButton = new Gtk.Button({
    label: 'Go',
});

searchEntry.bind_property_full('text', searchButton, 'sensitive',
    GObject.BindingFlags.DEFAULT,
    (binding, value) => [true, !!value],
    null);

Property Change Notification

Most GObject properties will emit GObject.Object::notify when the value is changed (more on signals below). You can connect to this signal in the form of notify::property-name to invoke a callback when it changes:

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const invisibleLabel = new Gtk.Label({
    visible: false,
});
let visible;

// Three different ways to get or set properties
visible = invisibleLabel.visible;
visible = invisibleLabel['visible'];
visible = invisibleLabel.get_visible();

invisibleLabel.visible = false;
invisibleLabel['visible'] = false;
invisibleLabel.set_visible(false);


const markupLabel = new Gtk.Label({
    label: '<i>Italics</i>',
    use_markup: true,
});
let useMarkup;

// If using native accessors, you can use `underscore_case` or `camelCase`
useMarkup = markupLabel.use_markup;
useMarkup = markupLabel.useMarkup;

// Anywhere the property name is a string, you must use `kebab-case`
markupLabel['use-markup'] = true;
markupLabel.connect('notify::use-markup', () => {});

// Getter and setter functions are always case sensitive
useMarkup = markupLabel.get_use_markup();
markupLabel.set_use_markup(true);


const changingLabel = Gtk.Label.new('Original Label');

const labelId = changingLabel.connect('notify::label', (object, _pspec) => {
    console.log(`New label is "${object.label}"`);
});


const prefsTitle = new Gtk.Label({
    label: 'Preferences',
    css_classes: ['heading'],
});
const prefsBox = new Gtk.Box();

// Bind the visibility of the box and label
prefsTitle.bind_property('visible', prefsBox, 'visible',
    GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

// Try to make the properties different
prefsTitle.visible = !prefsBox.visible;

if (prefsTitle.visible === prefsBox.visible)
    console.log('properties are equal!');


const searchEntry = new Gtk.Entry();
const searchButton = new Gtk.Button({
    label: 'Go',
});

searchEntry.bind_property_full('text', searchButton, 'sensitive',
    GObject.BindingFlags.DEFAULT,
    (binding, value) => [true, !!value],
    null);

Property Bindings

GObject provides a simple way to bind a property between objects, which can be used to link the state of two objects. The direction and behavior can be controlled by the GObject.BindingFlags passed when the binding is created.

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const invisibleLabel = new Gtk.Label({
    visible: false,
});
let visible;

// Three different ways to get or set properties
visible = invisibleLabel.visible;
visible = invisibleLabel['visible'];
visible = invisibleLabel.get_visible();

invisibleLabel.visible = false;
invisibleLabel['visible'] = false;
invisibleLabel.set_visible(false);


const markupLabel = new Gtk.Label({
    label: '<i>Italics</i>',
    use_markup: true,
});
let useMarkup;

// If using native accessors, you can use `underscore_case` or `camelCase`
useMarkup = markupLabel.use_markup;
useMarkup = markupLabel.useMarkup;

// Anywhere the property name is a string, you must use `kebab-case`
markupLabel['use-markup'] = true;
markupLabel.connect('notify::use-markup', () => {});

// Getter and setter functions are always case sensitive
useMarkup = markupLabel.get_use_markup();
markupLabel.set_use_markup(true);


const changingLabel = Gtk.Label.new('Original Label');

const labelId = changingLabel.connect('notify::label', (object, _pspec) => {
    console.log(`New label is "${object.label}"`);
});


const prefsTitle = new Gtk.Label({
    label: 'Preferences',
    css_classes: ['heading'],
});
const prefsBox = new Gtk.Box();

// Bind the visibility of the box and label
prefsTitle.bind_property('visible', prefsBox, 'visible',
    GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

// Try to make the properties different
prefsTitle.visible = !prefsBox.visible;

if (prefsTitle.visible === prefsBox.visible)
    console.log('properties are equal!');


const searchEntry = new Gtk.Entry();
const searchButton = new Gtk.Button({
    label: 'Go',
});

searchEntry.bind_property_full('text', searchButton, 'sensitive',
    GObject.BindingFlags.DEFAULT,
    (binding, value) => [true, !!value],
    null);

If you need to transform the value between the source and target, you can use GObject.Object.bind_property_full().

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const invisibleLabel = new Gtk.Label({
    visible: false,
});
let visible;

// Three different ways to get or set properties
visible = invisibleLabel.visible;
visible = invisibleLabel['visible'];
visible = invisibleLabel.get_visible();

invisibleLabel.visible = false;
invisibleLabel['visible'] = false;
invisibleLabel.set_visible(false);


const markupLabel = new Gtk.Label({
    label: '<i>Italics</i>',
    use_markup: true,
});
let useMarkup;

// If using native accessors, you can use `underscore_case` or `camelCase`
useMarkup = markupLabel.use_markup;
useMarkup = markupLabel.useMarkup;

// Anywhere the property name is a string, you must use `kebab-case`
markupLabel['use-markup'] = true;
markupLabel.connect('notify::use-markup', () => {});

// Getter and setter functions are always case sensitive
useMarkup = markupLabel.get_use_markup();
markupLabel.set_use_markup(true);


const changingLabel = Gtk.Label.new('Original Label');

const labelId = changingLabel.connect('notify::label', (object, _pspec) => {
    console.log(`New label is "${object.label}"`);
});


const prefsTitle = new Gtk.Label({
    label: 'Preferences',
    css_classes: ['heading'],
});
const prefsBox = new Gtk.Box();

// Bind the visibility of the box and label
prefsTitle.bind_property('visible', prefsBox, 'visible',
    GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);

// Try to make the properties different
prefsTitle.visible = !prefsBox.visible;

if (prefsTitle.visible === prefsBox.visible)
    console.log('properties are equal!');


const searchEntry = new Gtk.Entry();
const searchButton = new Gtk.Button({
    label: 'Go',
});

searchEntry.bind_property_full('text', searchButton, 'sensitive',
    GObject.BindingFlags.DEFAULT,
    (binding, value) => [true, !!value],
    null);

Signals

GObjects support a signaling system, similar to events and EventListeners in the JavaScript Web API. Here we will cover the basics of connecting and disconnection signals, as well as using callbacks.

Connecting Signals

TIP

When a GObject is destroyed, all signal connections are destroyed with it.

Signals are connected by calling GObject.Object.prototype.connect(), which returns a handler ID that is always truthy. Signals are disconnected by passing that ID to GObject.Object.prototype.disconnect():

js
import Gdk from 'gi://Gdk?version=4.0';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const copyLabel = Gtk.Label.new('Lorem Ipsum');

// Connecting a signal
const handlerId = copyLabel.connect('copy-clipboard', label => {
    console.log(`Copied "${label.label}" to clipboard!`);
});

// Disconnecting a signal
if (handlerId)
    copyLabel.disconnect(handlerId);


const selectLabel = Gtk.Label.new('This label has a popup!');

selectLabel.connect('move-cursor', (label, step, count, extendSelection) => {
    if (label === selectLabel)
        console.log('selectLabel emitted the signal!');

    if (step === Gtk.MovementStep.WORDS)
        console.log(`The cursor was moved ${count} word(s)`);

    if (extendSelection)
        console.log('The selection was extended');
});


const linkLabel = new Gtk.Label({
    label: '<a href="https://www.gnome.org">GNOME</a>',
    use_markup: true,
});

linkLabel.connect('activate-link', (label, uri) => {
    if (uri.startsWith('file://')) {
        console.log(`Ignoring ${uri}`);
        return true;
    }

    return false;
});

linkLabel.connect('activate-link', (label, uri) => {
    // Do something asynchronous with the signal arguments
    Promise.resolve(uri).catch(logError);

    return true;
});

Callback Arguments

Signals often have multiple callback arguments, but the first is always the emitting object:

js
import Gdk from 'gi://Gdk?version=4.0';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const copyLabel = Gtk.Label.new('Lorem Ipsum');

// Connecting a signal
const handlerId = copyLabel.connect('copy-clipboard', label => {
    console.log(`Copied "${label.label}" to clipboard!`);
});

// Disconnecting a signal
if (handlerId)
    copyLabel.disconnect(handlerId);


const selectLabel = Gtk.Label.new('This label has a popup!');

selectLabel.connect('move-cursor', (label, step, count, extendSelection) => {
    if (label === selectLabel)
        console.log('selectLabel emitted the signal!');

    if (step === Gtk.MovementStep.WORDS)
        console.log(`The cursor was moved ${count} word(s)`);

    if (extendSelection)
        console.log('The selection was extended');
});


const linkLabel = new Gtk.Label({
    label: '<a href="https://www.gnome.org">GNOME</a>',
    use_markup: true,
});

linkLabel.connect('activate-link', (label, uri) => {
    if (uri.startsWith('file://')) {
        console.log(`Ignoring ${uri}`);
        return true;
    }

    return false;
});

linkLabel.connect('activate-link', (label, uri) => {
    // Do something asynchronous with the signal arguments
    Promise.resolve(uri).catch(logError);

    return true;
});

Callback Return Values

WARNING

A callback with no return value will implicitly return undefined, while an async function will implicitly return a Promise.

Some signals expect a return value, usually a boolean. The type and behavior of the return value will be described in the documentation for the signal.

js
import Gdk from 'gi://Gdk?version=4.0';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const copyLabel = Gtk.Label.new('Lorem Ipsum');

// Connecting a signal
const handlerId = copyLabel.connect('copy-clipboard', label => {
    console.log(`Copied "${label.label}" to clipboard!`);
});

// Disconnecting a signal
if (handlerId)
    copyLabel.disconnect(handlerId);


const selectLabel = Gtk.Label.new('This label has a popup!');

selectLabel.connect('move-cursor', (label, step, count, extendSelection) => {
    if (label === selectLabel)
        console.log('selectLabel emitted the signal!');

    if (step === Gtk.MovementStep.WORDS)
        console.log(`The cursor was moved ${count} word(s)`);

    if (extendSelection)
        console.log('The selection was extended');
});


const linkLabel = new Gtk.Label({
    label: '<a href="https://www.gnome.org">GNOME</a>',
    use_markup: true,
});

linkLabel.connect('activate-link', (label, uri) => {
    if (uri.startsWith('file://')) {
        console.log(`Ignoring ${uri}`);
        return true;
    }

    return false;
});

linkLabel.connect('activate-link', (label, uri) => {
    // Do something asynchronous with the signal arguments
    Promise.resolve(uri).catch(logError);

    return true;
});

Using an async function as a signal handler will return an implicit Promise, which will be coerced to a truthy value. If necessary, use a traditional Promise chain and return the expected value type explicitly.

js
import Gdk from 'gi://Gdk?version=4.0';
import Gtk from 'gi://Gtk?version=4.0';


Gtk.init();

const copyLabel = Gtk.Label.new('Lorem Ipsum');

// Connecting a signal
const handlerId = copyLabel.connect('copy-clipboard', label => {
    console.log(`Copied "${label.label}" to clipboard!`);
});

// Disconnecting a signal
if (handlerId)
    copyLabel.disconnect(handlerId);


const selectLabel = Gtk.Label.new('This label has a popup!');

selectLabel.connect('move-cursor', (label, step, count, extendSelection) => {
    if (label === selectLabel)
        console.log('selectLabel emitted the signal!');

    if (step === Gtk.MovementStep.WORDS)
        console.log(`The cursor was moved ${count} word(s)`);

    if (extendSelection)
        console.log('The selection was extended');
});


const linkLabel = new Gtk.Label({
    label: '<a href="https://www.gnome.org">GNOME</a>',
    use_markup: true,
});

linkLabel.connect('activate-link', (label, uri) => {
    if (uri.startsWith('file://')) {
        console.log(`Ignoring ${uri}`);
        return true;
    }

    return false;
});

linkLabel.connect('activate-link', (label, uri) => {
    // Do something asynchronous with the signal arguments
    Promise.resolve(uri).catch(logError);

    return true;
});

MIT Licensed | GJS, A GNOME Project