3

I have an app that needs to open a file and update UI elements accordingly. I can select and open the file (and log the file contents), but I can't tell the UI elements to update.

I have tried read that I can create and add signals to just about any object, but I need to emit a signal from a function in an imported library.

I'm trying to do something like this: (in my function that has read a file from disk)

try {
                Signals.addSignalMethods(this);
                this.emit('update_ui', true);
              } catch(e) {
                print(e);
              }

(and in the main app class)

Signals.addSignalMethods(this);
  this.connect('update_ui',() => {
    try {
      print('>>> updating UI');
      this.ui.updateUI();
    } catch (e) {
      print(e);
    }
  });

I don't get any errors when I run the app, but the update function is never called. How can I get the signal to go through?

Here's the code from the main.js file that should catch the signal :

#!/usr/bin/gjs


Gio = imports.gi.Gio;
GLib = imports.gi.GLib;
Gtk = imports.gi.Gtk;
Lang = imports.lang;
Webkit = imports.gi.WebKit2;
Signals = imports.signals;
GObject = imports.gi.GObject;
Pango = imports.gi.Pango;

//
// add app folder to path
//

function getAppFileInfo() {
  let stack = (new Error()).stack,
    stackLine = stack.split('\n')[1],
    coincidence, path, file;

  if (!stackLine) throw new Error('Could not find current file (1)');

  coincidence = new RegExp('@(.+):\\d+').exec(stackLine);
  if (!coincidence) throw new Error('Could not find current file (2)');

  path = coincidence[1];
  file = Gio.File.new_for_path(path);
  return [file.get_path(), file.get_parent().get_path(), file.get_basename()];
}
const path = getAppFileInfo()[1];
imports.searchPath.push(path);

const myApp = new Lang.Class({
  Name: 'My Application',

  // Create the application itself
  _init: function() {
    this.application = new Gtk.Application();

    // Connect 'activate' and 'startup' signals to the callback functions
    this.application.connect('activate', Lang.bind(this, this._onActivate));
    this.application.connect('startup', Lang.bind(this, this._onStartup));
  },

  // Callback function for 'activate' signal presents windows when active
  _onActivate: function() {
    this._window.present();
  },

  // Callback function for 'startup' signal builds the UI
  _onStartup: function() {
    this._buildUI();
  },

  // Build the application's UI
  _buildUI: function() {

    // Create the application window
    this._window = new Gtk.ApplicationWindow({
      application: this.application,
      title: "My App",
      default_height: 200,
      default_width: 400,
      window_position: Gtk.WindowPosition.CENTER
    });

    //
    // menu bar
    //
    const Menubar = imports.lib.menubar;
    this._window.set_titlebar(Menubar.getHeader());

    Signals.addSignalMethods(this);
    this.connect('update_ui', () => {
      try {
        print('>>> updating UI');
        //this.ui.updateUI();
      } catch (e) {
        print(e);
      }
    });

    // Vbox to hold the switcher and stack.
    this._Vbox = new Gtk.VBox({
      spacing: 6
    });
    this._Hbox = new Gtk.HBox({
      spacing: 6,
      homogeneous: true
    });

    // const UI = imports.UI.UI;
    // this.ui = new UI.UIstack();
    // this.ui._buildStack();

    // this._Hbox.pack_start(this.ui._stack_switcher, true, true, 0);
    this._Vbox.pack_start(this._Hbox, false, false, 0);
    // this._Vbox.pack_start(this.ui._Stack, true, true, 0);

    // Show the vbox widget
    this._window.add(this._Vbox);

    // Show the window and all child widgets
    this._window.show_all();

  },

});

// Run the application
const app = new myApp();
app.application.run(ARGV);

and here's the header bar file that emits the signal:

const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const File = imports.lib.file;

const PopWidget = function(properties) {

  let label = new Gtk.Label({
    label: properties.label
  });
  let image = new Gtk.Image({
    icon_name: 'pan-down-symbolic',
    icon_size: Gtk.IconSize.SMALL_TOOLBAR
  });
  let widget = new Gtk.Grid();
  widget.attach(label, 0, 0, 1, 1);
  widget.attach(image, 1, 0, 1, 1);

  this.pop = new Gtk.Popover();
  this.button = new Gtk.ToggleButton();
  this.button.add(widget);
  this.button.connect('clicked', () => {
    if (this.button.get_active()) {
      this.pop.show_all();
    }
  });
  this.pop.connect('closed', () => {
    if (this.button.get_active()) {
      this.button.set_active(false);
    }
  });
  this.pop.set_relative_to(this.button);
  this.pop.set_size_request(-1, -1);
  this.pop.set_border_width(8);
  this.pop.add(properties.widget);
};

const getHeader = function() {

  let headerBar, headerStart, imageNew, buttonNew, popMenu, imageMenu, buttonMenu;

  headerBar = new Gtk.HeaderBar();
  headerBar.set_title("My App");
  headerBar.set_subtitle("Some subtitle text here");
  headerBar.set_show_close_button(true);

  headerStart = new Gtk.Grid({
    column_spacing: headerBar.spacing
  });

  // this.widgetOpen = new PopWidget({ label: "Open", widget: this.getPopOpen() });

  imageNew = new Gtk.Image({
    icon_name: 'document-open-symbolic',
    icon_size: Gtk.IconSize.SMALL_TOOLBAR
  });
  buttonNew = new Gtk.Button({
    image: imageNew
  });
  buttonNew.connect('clicked', () => {
    const opener = new Gtk.FileChooserDialog({
      title: 'Select a file'
    });
    opener.set_action(Gtk.FileChooserAction.OPEN);
    opener.add_button('open', Gtk.ResponseType.ACCEPT);
    opener.add_button('cancel', Gtk.ResponseType.CANCEL);
    const res = opener.run();
    if (res == Gtk.ResponseType.ACCEPT) {
      const filename = opener.get_filename();
      print(filename);

      const fileData = File.open(filename);
      print(JSON.stringify(fileData, null, 2));
      File.unRoll(fileData);
      // 
      // SHOULD SEND A SIGNAL
      //
      try {
        Signals.addSignalMethods(this);
        this.emit('update_ui', true);
      } catch (e) {
        print(e);
      }

      // this._window.ui.updateUI();
    }
    opener.destroy();
  });

  // headerStart.attach(this.widgetOpen.button, 0, 0, 1, 1);
  headerStart.attach(buttonNew, 1, 0, 1, 1);
  headerBar.pack_start(headerStart);

  popMenu = new Gtk.Popover();
  imageMenu = new Gtk.Image({
    icon_name: 'document-save-symbolic',
    icon_size: Gtk.IconSize.SMALL_TOOLBAR
  });
  buttonMenu = new Gtk.MenuButton({
    image: imageMenu
  });
  buttonMenu.set_popover(popMenu);
  popMenu.set_size_request(-1, -1);
  buttonMenu.set_menu_model(this.getMenu());

  headerBar.pack_end(buttonMenu);

  return headerBar;
};

const getPopOpen = function() {
  /* Widget popover */

  let widget = new Gtk.Grid(),
    label = new Gtk.Label({
      label: "Label 1"
    }),
    button = new Gtk.Button({
      label: "Other Documents ..."
    });

  button.connect('clicked', () => {
    this.widgetOpen.pop.hide();
    this.printText('Open other documents');
  });
  button.set_size_request(200, -1);

  widget.attach(label, 0, 0, 1, 1);
  widget.attach(button, 0, 1, 1, 1);
  widget.set_halign(Gtk.Align.CENTER);

  return widget;
};

const getMenu = function() {
  /* GMenu popover */

  let menu, section, submenu;

  menu = new Gio.Menu();

  section = new Gio.Menu();
  section.append("Save As...", 'app.saveAs');
  section.append("Save All", 'app.saveAll');
  menu.append_section(null, section);

  section = new Gio.Menu();
  submenu = new Gio.Menu();
  section.append_submenu('View', submenu);
  submenu.append("View something", 'app.toggle');
  submenu = new Gio.Menu();
  section.append_submenu('Select', submenu);
  submenu.append("Selection 1", 'app.select::one');
  submenu.append("Selection 2", 'app.select::two');
  submenu.append("Selection 3", 'app.select::thr');
  menu.append_section(null, section);

  section = new Gio.Menu();
  section.append("Close All", 'app.close1');
  section.append("Close", 'app.close2');
  menu.append_section(null, section);

  // Set menu actions
  let actionSaveAs = new Gio.SimpleAction({
    name: 'saveAs'
  });
  actionSaveAs.connect('activate', () => {
    const saver = new Gtk.FileChooserDialog({
      title: 'Select a destination'
    });
    saver.set_action(Gtk.FileChooserAction.SAVE);
    saver.add_button('save', Gtk.ResponseType.ACCEPT);
    saver.add_button('cancel', Gtk.ResponseType.CANCEL);
    const res = saver.run();
    if (res == Gtk.ResponseType.ACCEPT) {
      const filename = saver.get_filename();
      print(filename);
      const data = File.rollUp();
      File.save(filename, data);
      // let data = JSON.stringify(<FILE DATA>, null, '\t');
      // GLib.file_set_contents(filename, data);
    }
    saver.destroy();
  });
  APP.add_action(actionSaveAs);

  let actionSaveAll = new Gio.SimpleAction({
    name: 'saveAll'
  });
  actionSaveAll.connect('activate', () => {
    Gtk.FileChooserAction.OPEN
  });
  APP.add_action(actionSaveAll);

  let actionClose1 = new Gio.SimpleAction({
    name: 'close1'
  });
  actionClose1.connect('activate', () => {
    this.printText('Action close all');
  });
  APP.add_action(actionClose1);

  let actionClose2 = new Gio.SimpleAction({
    name: 'close2'
  });
  actionClose2.connect('activate', () => {
    this.printText('Action close');
  });
  APP.add_action(actionClose2);

  let actionToggle = new Gio.SimpleAction({
    name: 'toggle',
    state: new GLib.Variant('b', true)
  });
  actionToggle.connect('activate', (action) => {
    let state = action.get_state().get_boolean();
    if (state) {
      action.set_state(new GLib.Variant('b', false));
    } else {
      action.set_state(new GLib.Variant('b', true));
    }
    this.printText('View ' + state);
  });
  APP.add_action(actionToggle);

  let variant = new GLib.Variant('s', 'one');
  let actionSelect = new Gio.SimpleAction({
    name: 'select',
    state: variant,
    parameter_type: variant.get_type()
  });
  actionSelect.connect('activate', (action, parameter) => {
    let str = parameter.get_string()[0];
    if (str === 'one') {
      action.set_state(new GLib.Variant('s', 'one'));
    }
    if (str === 'two') {
      action.set_state(new GLib.Variant('s', 'two'));
    }
    if (str === 'thr') {
      action.set_state(new GLib.Variant('s', 'thr'));
    }
    this.printText('Selection ' + str);
  });
  APP.add_action(actionSelect);

  return menu;
};

it's not a class, just a static library... it's imported by the main.js file, so I would think the scope would be the same, but maybe not...

brainstormtrooper
  • 485
  • 2
  • 6
  • 18
  • Please add a MCVE, so we have something to test. – theGtknerd Feb 04 '19 at 02:32
  • I added more code samples. Hope that helps a bit... – brainstormtrooper Feb 04 '19 at 05:59
  • I have never used GJS, so I can't help out without having a Minimal Complete Verifiable Example. When I use Gtk and signals, I use Python. The technique is the same, but the language is not, so it takes too much of my time to try to figure out what you already could supply me with. Like, what is needed to compile GJS? – theGtknerd Feb 04 '19 at 11:46
  • Thanks for taking the time to answer. GJS doesn't really need to be compiled. Just make sure it's installed (probably is by default). then you can just execute GJS apps from the command line like you would a bash script (or make a .desktop file once the app is ready. I think my question is as much one of JavaScript scope as it is purely GTK/GJS... but I could be wrong. I'm guessing that the "this" scope in my included library is not the same as the "this" scope in my root file. Something like "self" in python included list of functions vs "self" in main class... – brainstormtrooper Feb 04 '19 at 12:35

1 Answers1

5

I'm going to over-answer while I get to your question again, because I see some difficulties and some "best practices" you could be using. I think it's much preferrable to use a direct subclass of GtkApplication and use ES6 classes.

const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

var MyApp = GObject.registerClass({
    // This must be unique, although I don't believe you have to specify it
    GTypeName: 'MyApp',

    // GObject already have their own signal system, so invoking addSignalMethods()
    // will override the existing methods and could cause you problems. Here's a
    // simple example of how you'd add a GObject signal to a subclass; the resulting
    // callback would look like:
    //
    //     myapp.connect('update-ui', (application, bool) => { ... });
    Signals: {
        'update-ui': {
            param_types: [GObject.TYPE_BOOLEAN]
        },
    }
}, class MyApp extends Gtk.Application {

    _init(params) {
        // You could pass the regular params straight through
        //super._init(params);

        // Or setup your class to be initted without having to pass any args in your
        // constructor
        super._init({
            // This will also become your well-known name on DBus, with the matching
            // object path of /org/github/username/MyApp.
            //
            // GApplication framework will also use these for other things like any
            // GResource you bundle with your application.
            application_id: 'com.github.username.MyApp',
            // There are other flags you can set to allow your app to be a target for
            // "Open with...", but that's not what you're asking about today :)
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });

        // You can also do other setup , however it's important to note that
        // GApplication's are generally "single-instance" processes, so you want most
        // of that setup in ::startup which will only run if this is the primary
        // instance.
        //
        // On the other hand if you pass HANDLES_OPEN, for example, that signal/vfunc
        // will still be invoked.
        GLib.set_prgname(this.application_id);
        GLib.set_application_name('MyApp');
    }

    _buildUI() {
        // ...
    }

    // defining a virtual function usually means overriding the default handler for a
    // a signal. In this case, it's not much of an issue but for frequently emitted
    // signals it avoids "marshalling" the C values into JS values and back again.
    //
    // In other words, defining this function means it will be called as if  you
    // connected to the ::activate signal.
    vfunc_activate() {
        this._window.present();
    }

    // ::activate is a special case, but for most vfunc's is necessary to "chain up",
    // which really just means calling the super class's original function inside your
    // override.
    vfunc_startup() {
        // In ::startup we need to chain up first since GApplication does important
        // setup checks here
        super.vfunc_startup();

        // your stuff after
        this._buildUI();

        // In _buildUI() you correctly passed your GtkApplication as the application
        // property, which will keep the application running so long as that window
        // is open.
        //
        // If you wanted your application to stay open regardless, you call hold()
        this.hold();
    }

    vfunc_shutdown() {
        // ::shutdown on the other hand is the reverse; first we do our stuff, then
        // chain up to super class. This is a good time to do any cleanup you need
        // before the process exits.
        this._destroyUI();

        // chain up
        super.vfunc_shutdown();
    }
});

// For technical reasons, this is the proper way you should start your application
(new MyApp()).run([imports.system.programInvocationName].concat(ARGV));

The addSignalMethods() function only needs to be called once, but as mentioned it will override the existing signal methods and that could cause you problems if you call it on a GObject that already has signals defined. It basically does this:

addSignalMethods(obj) {
    obj.emit = function() {
        // custom signal code
    }
    ...

It was really meant for pure JS classes that don't have a signal system already.

// SHOULD SEND A SIGNAL
//
try {
    Signals.addSignalMethods(this);
    this.emit('update_ui', true);
} catch (e) {
    print(e);
}

The reason why this wasn't working for you is that getHeaderbar() is a top-level function in imports.lib.menubar so when you called addSignalMethods(this), this === imports.lib.menubar. Therefore to catch that signal you would had to call:

imports.lib.menubar.connect('update_ui', () => {});

When you called it in _buildUI():

Signals.addSignalMethods(this);
this.connect('update_ui', () => {
    try {
        print('>>> updating UI');
        //this.ui.updateUI();
    } catch (e) {
        print(e);
    }
});

this === MyApp, so you were first overriding MyApp's signal methods, then connecting to itself. The Signals import is fairly lax so it doesn't require a signal to be defined before use; you just connect to what you want and emit what you want. This is why you weren't getting any errors or warnings.

There are couple ways you can solve your problem:

// (1) Pick an object to emit and connect from and only add the signal methods once
Signals.addSignalMethods(headerBar);

// Since you're using an arrow function you can call this in the same place since it
// should still be in scope
headerBar.emit('update_ui', true);

// You can connect by grabbing the headerbar widget from your constructed window
this._window.get_titlebar().connect('update_ui', (headerBar, bool) => {});

// (2) Define a signal *on* your GtkApplication and use it as a relay

// Connect from a function in MyApp (so in `this.connect`, `this === MyApp`)
// To set `this` for the callback itself, use `Function.bind()`
this.connect('update-ui', this.ui.updateUI.bind(ui));

// You can always grab the primary instance of your GtkApplication (from anywhere)
// and emit the signal to invoke the MyApp.ui.updateUI() function
let myApp = Gio.Application.get_default();
myApp.emit('update-ui', true);

// (3) How I'd probably do it; just invoke the method directly
let myApp = Gio.Application.get_default();
myApp.ui.updateUI();
andy.holmes
  • 3,383
  • 17
  • 28