romgrk / node-gtk

GTK+ bindings for NodeJS (via GObject introspection)
MIT License
494 stars 42 forks source link

Implement Gtk/Gdk overrides to expose a nice API #31

Closed romgrk closed 4 years ago

romgrk commented 6 years ago

We need to implement overrides on the JS side to expose a nice API for users. One such example is described here: https://github.com/romgrk/node-gtk/issues/4#issuecomment-400532978

Off the top of my head, the things that need to be overriden are:

For more inspiration, look at https://gitlab.gnome.org/GNOME/pygobject/blob/master/gi/overrides/Gtk.py. The file in which those overrides would be (for Gtk-3.0) is https://github.com/romgrk/node-gtk/blob/master/lib/overrides/Gtk-3.0.js.

ping @WebReflection

romgrk commented 6 years ago

Sent from cm9tZ3JrLmNjQGdtYWlsLmNvbQ== (base64)

WebReflection commented 6 years ago

I have a question: is every method returning an Array or I need to gir-scan all methods of each prototype / class to figure out the return type and monkey-patch that ?

romgrk commented 6 years ago

Every method that has more than 1 return value returns an array. The easiest way to find which ones do so would be to use ./lib/inspect.js, as follow:

const inspect = require('./lib/inspect')
const gtk = inspect.parseNamespace('Gtk')
fnInfos = inspect.infos.filter(i => i.infoType === 'function' && i.return_tag !== 'void' && i.args.some(a => a.direction != 'IN'))
fnInfos.forEach(f => console.log(inspect.formatFunction(f)))
WebReflection commented 6 years ago

I guess my question was rather: if a method returns an Array, as actual return value, what should happen?

romgrk commented 6 years ago

Well this is the reason we need overrides: there's not always a rule as to what should happen. The goal here would be to make the API look nicer for "popular" methods of "popular" namespaces. (Hence why I just mention Gtk/Gdk in the title).

Some general cases/examples:

1) Some functions have return values that can be ignored, usually because they're not relevant in a javascript context. One such example is described here: https://gitlab.gnome.org/GNOME/gjs/issues/66 In those cases, we're left with a single relevant return value, which should be the JS return value.

2) E.g. gtk_widget_get_request_size: methods which return more than one relevant return value. In those cases, we need to return an object with properties named accordingly.

3) Functions as the following one:

gboolean
g_file_get_contents (const gchar *filename,
                     gchar **contents,
                     gsize *length,
                     GError **error);

Those return a boolean to allow the following style in C:

GError error;
if (!g_file_get_contents(..., &error)) {
  // Deal with error
}

This is irrelevant in JS, because an error would be thrown and the boolean return value is irrelevant. In those cases, we should just strip the boolean return value, and deal with the rest of the arguments. (Note that this example is in GLib, I'm not sure if or how many of these there are in Gtk/Gdk.) (Note also that in this example, only the contents value is relevant in javascript, so it would fold back into category 1)

That's what I could think about, there might be other things to deal with that we'll discover as we progress.

WebReflection commented 6 years ago

it's not fully clear how am I supposed to patch these functions

/*
 * Gtk-3.0.js
 */

const inspect = require('../inspect');

const multiReturn = i =>
  i.infoType === 'function' &&
  i.return_tag !== 'void' &&
  i.args.some(a => a.direction != 'IN');

const parse = (obj, name, whole) => {
  const nmsp = inspect.parseNamespace(whole);
  const fnInfos = inspect.infos.splice(0).filter(multiReturn);
  fnInfos.forEach(function (f) {
    console.log(f.name);
    if (f.parent === name) {
      /*
      // nope, it doesn't work
      const method = obj[f.name];
      obj[f.name] = function () {
        const result = method.call(this); // apply, arguments, whatever
        return result;
      };
      */
    } else {
      // parse(f.parent, f.parent.name, whole + '.' + f.parent.name);
    }
  });
};

exports.apply = (Gtk) => {
  parse(Gtk, 'Gtk', 'Gtk');
};

The inspect.parseNamespace also return an object, instead of an array, but even mapping its keys won't work through that filter neither.

Sorry I am a bit rusty with Gtk stuff in node, I used to do things slightly differently in GJS https://github.com/cgjs/cgjs/blob/master/packages/cgjs/cgjs/camel.js#L68

romgrk commented 6 years ago

Sorry, I might not have explained correctly. The snippet of code I included above is only useful to retrieve a list of functions that might need patching. But I was thinking, it would be better to manually patch the functions that need patching. Overrides are there to fix edge-cases for which we can't possibly include a logic that would handle all cases. Any other JS specific thing can also be added there. For example, PyGObject makes Gtk.TreePath iterable. This could also be done in JS by implementing Gtk.TreePath[Symbol.iterator].

So we would start with something like this:

/*
 * Gtk-3.0.js
 */

exports.apply = (Gtk) => {

  /*** 
   * Gtk.Widget 
   ***/

  /**
   * @typedef {Object} Dimension
   * @property {number} width
   * @property {number} height
   */
  const originalGetSizeRequest = Gtk.Widget.prototype.getSizeRequest
  /**
   * Gtk.Widget.prototype.getSizeRequest
   * @returns {Dimension}
   */
  Gtk.Widget.prototype.getSizeRequest = function() {
    const [width, height] = originalGetSizeRequest.call(this)
    return { width, height }
  }

  /*** 
   * Gtk.TreePath 
   ***/
  Gtk.TreePath.prototype[Symbol.iterator] = function () {
    // I've no idea how this works but check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
  }

  // Then, any other function/thing that needs patching
}

Note that I'm including jsdocs because it might help for #24, we could parse those so that TypeScript annotations include overrides.

WebReflection commented 6 years ago

that filter returns 194 methods to patch though ... this is going to be ready for Christmas if I have to also understand what each method returns and document it via jsdocs ... just saying.

WebReflection commented 6 years ago

Side note: did this project also become a no-semicolon one ? (just to clarify how to contribute)

romgrk commented 6 years ago

Yea, I know. The idea is not to patch everything, just the things that it makes sense to patch. Maybe there's not that much stuff to patch, I just haven't looked yet Don't worry about JSDocs for now, we're not there yet anyway.

romgrk commented 6 years ago

Side note: did this project also become a no-semicolon one ? (just to clarify how to contribute)

Well, this project hasn't chosen any style guide yet, so the code that has been added reflects the personal preferences of the person who wrote it. I don't use semi-colons in javascript because they're not necessary and less characters means less input for your brain to parse. That said, if you have strong opinions on semi-colons I'm ok with that. Maybe we could use prettier? Do you have any style guide proposition?

(I've added you to the project's collaborators. I usually use a branch workflow for major changes that possibly require discussions, and push to master for minor changes like these)

WebReflection commented 6 years ago

Maybe we could use prettier?

I don't care, but I prefer semi colons (and I have strong opinions about it but that's my preference).

I usually use a branch workflow for major changes that possibly require discussions, and push to master for minor changes like these

works for me.

WebReflection commented 6 years ago

I'm not sure Gtk brings Widget or any class at all with it ...

exports.apply = (Gtk) => {

  console.log(Gtk.Widget); // undefined

};

does that work for you?

Anyway, this is the list I could extract on Mac

AccelGroup.query (accel_key: guint32, accel_mods: flags.ModifierType, n_entries: guint32OUTE): struct.AccelGroupEntry[]
AccelMap.lookup_entry (accel_path: utf8, key: struct.AccelKeyOUT): gboolean
ActionGroup.query_action (action_name: utf8, enabled: gbooleanOUTE, parameter_type: struct.VariantTypeOUTE, state_type: struct.VariantTypeOUTE, state_hint: struct.VariantOUTE, state: struct.VariantOUTE): gboolean
Buildable.custom_tag_start (builder: object.Builder, child: object.Object, tagname: utf8, parser: struct.MarkupParserOUT, data: voidOUTE): gboolean
Builder.value_from_string (pspec: object.ParamSpec, string: utf8, value: struct.ValueOUT): gboolean
Builder.value_from_string_type (type: GType, string: utf8, value: struct.ValueOUT): gboolean
CellAreaClass.list_cell_properties (n_properties: guint32OUTE): object.ParamSpec[]
CellArea.get_cell_at_position (context: object.CellAreaContext, widget: object.Widget, cell_area: struct.Rectangle, x: gint32, y: gint32, alloc_area: struct.RectangleOUT): object.CellRenderer
CellView.get_size_of_row (path: struct.TreePath, requisition: struct.RequisitionOUT): gboolean
Clipboard.wait_for_rich_text (buffer: object.TextBuffer, format: struct.AtomOUT, length: guint64OUTE): guint8
Clipboard.wait_for_targets (targets: struct.Atom[]OUTE, n_targets: gint32OUTE): gboolean
ColorSelection.palette_from_string (str: utf8, colors: struct.Color[]OUTE, n_colors: gint32OUTE): gboolean
ComboBox.get_active_iter (iter: struct.TreeIterOUT): gboolean
ContainerClass.list_child_properties (n_properties: guint32OUTE): object.ParamSpec[]
Container.get_focus_chain (focusable_widgets: List<object.Widget>OUTC): gboolean
Editable.get_selection_bounds (start_pos: gint32OUTE, end_pos: gint32OUTE): gboolean
GestureDrag.get_offset (x: gdoubleOUTE, y: gdoubleOUTE): gboolean
GestureDrag.get_start_point (x: gdoubleOUTE, y: gdoubleOUTE): gboolean
GestureMultiPress.get_area (rect: struct.RectangleOUT): gboolean
GestureSwipe.get_velocity (velocity_x: gdoubleOUTE, velocity_y: gdoubleOUTE): gboolean
Gesture.get_bounding_box (rect: struct.RectangleOUT): gboolean
Gesture.get_bounding_box_center (x: gdoubleOUTE, y: gdoubleOUTE): gboolean
Gesture.get_point (sequence: struct.EventSequence, x: gdoubleOUTE, y: gdoubleOUTE): gboolean
Gradient.resolve (props: object.StyleProperties, resolved_gradient: struct.PatternOUTE): gboolean
IMContext.get_surrounding (text: utf8OUTE, cursor_index: gint32OUTE): gboolean
IconInfo.get_attach_points (points: struct.Point[]OUTE, n_points: gint32OUTE): gboolean
IconInfo.get_embedded_rect (rectangle: struct.RectangleOUT): gboolean
IconInfo.load_symbolic (fg: struct.RGBA, success_color: struct.RGBA, warning_color: struct.RGBA, error_color: struct.RGBA, was_symbolic: gbooleanOUTE): object.Pixbuf
IconInfo.load_symbolic_finish (res: interface.AsyncResult, was_symbolic: gbooleanOUTE): object.Pixbuf
IconInfo.load_symbolic_for_context (context: object.StyleContext, was_symbolic: gbooleanOUTE): object.Pixbuf
IconInfo.load_symbolic_for_context_finish (res: interface.AsyncResult, was_symbolic: gbooleanOUTE): object.Pixbuf
IconInfo.load_symbolic_for_style (style: object.Style, state: enum.StateType, was_symbolic: gbooleanOUTE): object.Pixbuf
IconSize.lookup (size: gint32, width: gint32OUTE, height: gint32OUTE): gboolean
IconSize.lookup_for_settings (settings: object.Settings, size: gint32, width: gint32OUTE, height: gint32OUTE): gboolean
IconView.get_cell_rect (path: struct.TreePath, cell: object.CellRenderer, rect: struct.RectangleOUT): gboolean
IconView.get_cursor (path: struct.TreePathOUTE, cell: object.CellRendererOUT): gboolean
IconView.get_dest_item_at_pos (drag_x: gint32, drag_y: gint32, path: struct.TreePathOUTE, pos: enum.IconViewDropPositionOUTE): gboolean
IconView.get_item_at_pos (x: gint32, y: gint32, path: struct.TreePathOUTE, cell: object.CellRendererOUT): gboolean
IconView.get_tooltip_context (x: gint32INOUTE, y: gint32INOUTE, keyboard_tip: gboolean, model: interface.TreeModelOUT, path: struct.TreePathOUTE, iter: struct.TreeIterOUT): gboolean
IconView.get_visible_range (start_path: struct.TreePathOUTE, end_path: struct.TreePathOUTE): gboolean
Label.get_selection_bounds (start: gint32OUTE, end: gint32OUTE): gboolean
LevelBar.get_offset_value (name: utf8, value: gdoubleOUTE): gboolean
Popover.get_pointing_to (rect: struct.RectangleOUT): gboolean
PrintContext.get_hard_margins (top: gdoubleOUTE, bottom: gdoubleOUTE, left: gdoubleOUTE, right: gdoubleOUTE): gboolean
PrintSettings.get_page_ranges (num_ranges: gint32OUTE): struct.PageRange[]
RecentChooser.get_uris (length: guint64OUTE): utf8[]
RecentInfo.get_application_info (app_name: utf8, app_exec: utf8OUT, count: guint32OUTE, time_: gint64OUTE): gboolean
RecentInfo.get_applications (length: guint64OUTE): utf8[]
RecentInfo.get_groups (length: guint64OUTE): utf8[]
Scrollable.get_border (border: struct.BorderOUT): gboolean
SelectionData.get_data (length: gint32OUTE): guint8[]
SelectionData.get_targets (targets: struct.Atom[]OUTC, n_atoms: gint32OUTE): gboolean
StatusIcon.get_geometry (screen: object.ScreenOUT, area: struct.RectangleOUT, orientation: enum.OrientationOUTE): gboolean
StyleContext.has_region (region_name: utf8, flags_return: flags.RegionFlagsOUTE): gboolean
StyleContext.lookup_color (color_name: utf8, color: struct.RGBAOUT): gboolean
StyleContext.state_is_running (state: enum.StateType, progress: gdoubleOUTE): gboolean
StyleProperties.get_property (property: utf8, state: flags.StateFlags, value: struct.ValueOUTE): gboolean
StyleProvider.get_style_property (path: struct.WidgetPath, state: flags.StateFlags, pspec: object.ParamSpec, value: struct.ValueOUT): gboolean
Style.lookup_color (color_name: utf8, color: struct.ColorOUT): gboolean
SymbolicColor.resolve (props: object.StyleProperties, resolved_color: struct.RGBAOUT): gboolean
TargetList.find (target: struct.Atom, info: guint32OUTE): gboolean
TextBuffer.get_deserialize_formats (n_formats: gint32OUTE): struct.Atom[]
TextBuffer.get_selection_bounds (start: struct.TextIterOUT, end: struct.TextIterOUT): gboolean
TextBuffer.get_serialize_formats (n_formats: gint32OUTE): struct.Atom[]
TextBuffer.serialize (content_buffer: object.TextBuffer, format: struct.Atom, start: struct.TextIter, end: struct.TextIter, length: guint64OUTE): guint8[]
TextIter.backward_search (str: utf8, flags: flags.TextSearchFlags, match_start: struct.TextIterOUT, match_end: struct.TextIterOUT, limit: struct.TextIter): gboolean
TextIter.forward_search (str: utf8, flags: flags.TextSearchFlags, match_start: struct.TextIterOUT, match_end: struct.TextIterOUT, limit: struct.TextIter): gboolean
TextIter.get_attributes (values: struct.TextAttributesOUT): gboolean
TextView.get_iter_at_location (iter: struct.TextIterOUT, x: gint32, y: gint32): gboolean
TextView.get_iter_at_position (iter: struct.TextIterOUT, trailing: gint32OUTE, x: gint32, y: gint32): gboolean
Text.get_run_attributes (offset: gint32, start_offset: gint32OUTE, end_offset: gint32OUTE): List<void>
Text.get_selection (selection_num: gint32, start_offset: gint32OUTE, end_offset: gint32OUTE): utf8
Text.get_string_at_offset (offset: gint32, granularity: enum.TextGranularity, start_offset: gint32OUTE, end_offset: gint32OUTE): utf8
Text.get_text_after_offset (offset: gint32, boundary_type: enum.TextBoundary, start_offset: gint32OUTE, end_offset: gint32OUTE): utf8
Text.get_text_at_offset (offset: gint32, boundary_type: enum.TextBoundary, start_offset: gint32OUTE, end_offset: gint32OUTE): utf8
Text.get_text_before_offset (offset: gint32, boundary_type: enum.TextBoundary, start_offset: gint32OUTE, end_offset: gint32OUTE): utf8
ThemingEngine.has_region (style_region: utf8, flags: flags.RegionFlagsOUTE): gboolean
ThemingEngine.lookup_color (color_name: utf8, color: struct.RGBAOUT): gboolean
ThemingEngine.state_is_running (state: enum.StateType, progress: gdoubleOUTE): gboolean
TreeModelFilter.convert_child_iter_to_iter (filter_iter: struct.TreeIterOUT, child_iter: struct.TreeIter): gboolean
TreeModelSort.convert_child_iter_to_iter (sort_iter: struct.TreeIterOUT, child_iter: struct.TreeIter): gboolean
TreeModel.get_iter (iter: struct.TreeIterOUT, path: struct.TreePath): gboolean
TreeModel.get_iter_first (iter: struct.TreeIterOUT): gboolean
TreeModel.get_iter_from_string (iter: struct.TreeIterOUT, path_string: utf8): gboolean
TreeModel.iter_children (iter: struct.TreeIterOUT, parent: struct.TreeIter): gboolean
TreeModel.iter_nth_child (iter: struct.TreeIterOUT, parent: struct.TreeIter, n: gint32): gboolean
TreeModel.iter_parent (iter: struct.TreeIterOUT, child: struct.TreeIter): gboolean
TreePath.get_indices (depth: gint32OUTE): gint32[]
TreeSelection.get_selected (model: interface.TreeModelOUT, iter: struct.TreeIterOUT): gboolean
TreeSelection.get_selected_rows (model: interface.TreeModelOUT): List<struct.TreePath>
TreeSortable.get_sort_column_id (sort_column_id: gint32OUTE, order: enum.SortTypeOUTE): gboolean
TreeViewColumn.cell_get_position (cell_renderer: object.CellRenderer, x_offset: gint32OUTE, width: gint32OUTE): gboolean
TreeView.get_dest_row_at_pos (drag_x: gint32, drag_y: gint32, path: struct.TreePathOUTE, pos: enum.TreeViewDropPositionOUTE): gboolean
TreeView.get_path_at_pos (x: gint32, y: gint32, path: struct.TreePathOUTE, column: object.TreeViewColumnOUT, cell_x: gint32OUTE, cell_y: gint32OUTE): gboolean
TreeView.get_tooltip_context (x: gint32INOUTE, y: gint32INOUTE, keyboard_tip: gboolean, model: interface.TreeModelOUT, path: struct.TreePathOUTE, iter: struct.TreeIterOUT): gboolean
TreeView.get_visible_range (start_path: struct.TreePathOUTE, end_path: struct.TreePathOUTE): gboolean
TreeView.is_blank_at_pos (x: gint32, y: gint32, path: struct.TreePathOUTE, column: object.TreeViewColumnOUT, cell_x: gint32OUTE, cell_y: gint32OUTE): gboolean
WidgetClass.list_style_properties (n_properties: guint32OUTE): object.ParamSpec[]
WidgetPath.iter_has_qregion (pos: gint32, qname: guint32, flags: flags.RegionFlagsOUTE): gboolean
WidgetPath.iter_has_region (pos: gint32, name: utf8, flags: flags.RegionFlagsOUTE): gboolean
Widget.intersect (area: struct.Rectangle, intersection: struct.RectangleOUT): gboolean
Widget.translate_coordinates (dest_widget: object.Widget, src_x: gint32, src_y: gint32, dest_x: gint32OUTE, dest_y: gint32OUTE): gboolean
Window.get_resize_grip_area (rect: struct.RectangleOUT): gboolean
Gtk.get_current_event_state (state: flags.ModifierTypeOUTE): gboolean
Gtk.icon_size_lookup (size: gint32, width: gint32OUTE, height: gint32OUTE): gboolean
Gtk.icon_size_lookup_for_settings (settings: object.Settings, size: gint32, width: gint32OUTE, height: gint32OUTE): gboolean
Gtk.init_check (argc: gint32INOUTE, argv: utf8[]INOUTE): gboolean
Gtk.init_with_args (argc: gint32INOUTE, argv: utf8[]INOUTE, parameter_string: utf8, entries: struct.OptionEntry[], translation_domain: utf8): gboolean
Gtk.parse_args (argc: gint32INOUTE, argv: utf8[]INOUTE): gboolean
Gtk.rc_parse_color (scanner: struct.Scanner, color: struct.ColorOUT): guint32
Gtk.rc_parse_color_full (scanner: struct.Scanner, style: object.RcStyle, color: struct.ColorOUT): guint32
Gtk.rc_parse_state (scanner: struct.Scanner, state: enum.StateTypeOUTE): guint32
Gtk.stock_lookup (stock_id: utf8, item: struct.StockItemOUT): gboolean
Gtk.target_table_new_from_list (list: struct.TargetList, n_targets: gint32OUTE): struct.TargetEntry[]
Gtk.test_list_all_types (n_types: guint32OUTE): GType[]
Gtk.tree_get_row_drag_data (selection_data: struct.SelectionData, tree_model: interface.TreeModelOUT, path: struct.TreePathOUTE): gboolean
romgrk commented 6 years ago

Yes, it works for me:

/*
 * Gtk-3.0.js
 */

exports.apply = (Gtk) => {
    console.log(Gtk.Widget) // => { [Function: GtkWidget] gtype: 63073824 }
    process.exit(0)
}

How did you test it? I tried node ./run.js

/*
 * run.js
 */

const gi = require('.')
const Gtk = gi.require('Gtk', '3.0')
WebReflection commented 6 years ago

I think I've messed up something, 'cause now I've tried again and it works indeed. Sorry for that

romgrk commented 6 years ago

TBH, I remember seeing something similar to what you described, but I don't remember what I was testing or how I started the thing :) I think it was related to requiring inspect.js before index.js, or something.

romgrk commented 6 years ago

Also: https://github.com/romgrk/node-gtk/blob/master/doc/overrides.md

@WebReflection This is one of the few remaining issues for the 1.0 release, I will probably start working on this at some point. If you do some work, be sure to git pull before, and git push fast.

romgrk commented 6 years ago

Override gtk_css_provider_load_from_data: convert string to uint8[]