Interfacing Raku to Gnome GTK+

The Graphical User Interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
use v6;

use Gnome::Gtk3::TreeStore;
use Gnome::Gtk3::CellRendererText;
use Gnome::Gtk3::Main;
use Gnome::Gtk3::Window;
use Gnome::Gtk3::Grid;
use Gnome::Gtk3::TreeView;
use Gnome::Gtk3::TreeViewColumn;
use Gnome::Gtk3::ListStore;
use Gnome::Gtk3::TreePath;
use Gnome::Gtk3::TreeIter;

use Gnome::GObject::Type;
use Gnome::GObject::Value;

use GuiHandlers;

#-------------------------------------------------------------------------------
unit class Gui;

#-------------------------------------------------------------------------------
enum FileListColumns <FILENAME_COL TODO_COUNT_COL DATA_KEY_COL>;
enum MarkerListColumns <MARKER_COL LINE_COL COMMENT_COL>;

has Gnome::Gtk3::Main $!main;
has Gnome::Gtk3::TreeView $!fs-table;
has Gnome::Gtk3::TreeStore $!files;
has Gnome::Gtk3::TreeView $!mark-table;
has Gnome::Gtk3::ListStore $!markers;

has Hash $!data-hash;

#-------------------------------------------------------------------------------
submethod BUILD ( ) {
  $!main .= new;
  $!data-hash = %();

  my Gnome::Gtk3::Window $w .= new;
  my Gnome::Gtk3::Grid $g .= new;
  $w.add($g);
  $w.container-set-border-width(10);
  $w.window-set-default-size( 270, 300);
  $w.set-title('Todo Viewer');

  self!create-file-table;
  $g.attach( $!fs-table, 0, 0, 1, 1);

  self!create-markers-table;
  $g.attach( $!mark-table, 1, 0, 1, 1);

  my GuiHandlers::ListView $gh-flview .= new;
  $!fs-table.register-signal(
    $gh-flview, 'select-list-entry', 'row-activated',
    :data($!data-hash), :data-col(DATA_KEY_COL),
    :$!markers, :$!files
  );

  $!mark-table.register-signal(
    $gh-flview, 'select-marker-entry', 'row-activated', :$!markers
  );

  my GuiHandlers::Application $gh-app .= new(:$!main);
  $w.register-signal( $gh-app, 'exit-todo-viewer', 'destroy');

  $w.show-all;
}

#-------------------------------------------------------------------------------
method add-file-data ( Str $project-dir, Str $filename-path, Array $data ) {

  # only insert files which have data
  self!insert-in-table( $project-dir, $filename-path, $data) if $data.elems;
}

#-------------------------------------------------------------------------------
method activate ( ) {
  $!fs-table.expand-all;
  Gnome::Gtk3::Main.new.main;
}

#-------------------------------------------------------------------------------
method !create-file-table ( ) {
  $!files .= new( :field-types( G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING));
  $!fs-table .= new(:model($!files));
  $!fs-table.set-headers-visible(True);
  $!fs-table.set-hexpand(True);
  $!fs-table.set-vexpand(True);

  my Gnome::Gtk3::CellRendererText $crt .= new;
  my Gnome::GObject::Value $v .= new( :type(G_TYPE_STRING), :value<blue>);
  $crt.set-property( 'foreground', $v);
  my Gnome::Gtk3::TreeViewColumn $tvc .= new;
  $tvc.set-title('Filename');
  $tvc.pack-end( $crt, True);
  $tvc.add-attribute( $crt, 'text', FILENAME_COL);
  $!fs-table.append-column($tvc);

  $crt .= new;
  $v .= new( :type(G_TYPE_STRING), :value<red>);
  $crt.set-property( 'foreground', $v);
  $tvc .= new;
  $tvc.set-title('Mark Count');
  $tvc.pack-end( $crt, True);
  $tvc.add-attribute( $crt, 'text', TODO_COUNT_COL);
  $!fs-table.append-column($tvc);
}

#-------------------------------------------------------------------------------
method !create-markers-table ( ) {
  $!markers .= new(
    :field-types( G_TYPE_STRING, G_TYPE_INT, G_TYPE_STRING)
  );
  $!mark-table .= new(:model($!markers));
  $!mark-table.set-headers-visible(True);
  $!mark-table.set-hexpand(True);
  $!mark-table.set-vexpand(True);

  my Gnome::Gtk3::CellRendererText $crt .= new;
  my Gnome::GObject::Value $v .= new( :type(G_TYPE_STRING), :value<blue>);
  $crt.object-set-property( 'foreground', $v);
  my Gnome::Gtk3::TreeViewColumn $tvc .= new;
  $tvc.set-title('Marker');
  $tvc.pack-end( $crt, True);
  $tvc.add-attribute( $crt, 'text', MARKER_COL);
  $!mark-table.append-column($tvc);

  $crt .= new;
  $v .= new( :type(G_TYPE_STRING), :value<red>);
  $crt.set-property( 'foreground', $v);
  $tvc .= new;
  $tvc.set-title('Line #');
  $tvc.pack-end( $crt, True);
  $tvc.add-attribute( $crt, 'text', LINE_COL);
  $!mark-table.append-column($tvc);

  $crt .= new;
  $v .= new( :type(G_TYPE_STRING), :value<blue>);
  $crt.set-property( 'foreground', $v);
  $tvc .= new;
  $tvc.set-title('Comment');
  $tvc.pack-end( $crt, True);
  $tvc.add-attribute( $crt, 'text', COMMENT_COL);
  $!mark-table.append-column($tvc);
}

#-------------------------------------------------------------------------------
method !insert-in-table ( Str $project-dir, Str $filename-path, Array $data ) {

  # Prepare key to store in 3rd column
  my Str $abs-name = "$project-dir/$filename-path".IO.absolute.Str;

  # Sub to recursevly build the tree store from file and its data
  my Callable $s = sub (
    @path-parts is copy, Array :$ts-iter-path is copy = []
  ) {

    my Bool $found = False;
    my Gnome::Gtk3::TreeIter $iter;
    my Str $part = @path-parts.shift;

    $ts-iter-path.push(0);
    while ( ($iter = self!get-iter($ts-iter-path)).is-valid ) {

      my Array[Gnome::GObject::Value] $v =
         $!files.tree-store-get-value( $iter, FILENAME_COL);
      my Str $filename-table-entry = $v[0].get-string // '-';
      $v[0].clear-object;

      # Test if part is in the treestore at the provided path
      my Order $order = $part cmp $filename-table-entry;

      # Insert entry before this one and go a level deeper with the rest
      if $order == Less {
        $iter = $!files.tree-store-insert-before(
          self!get-parent-iter($ts-iter-path),      # parent
          self!get-iter($ts-iter-path)              # sybling
        );

        $!files.tree-store-set-value( $iter, FILENAME_COL, $part);

        if @path-parts.elems {
          $s( @path-parts, :$ts-iter-path)
        }

        # Coming back from the top means that all is done only other
        # columns needs to set
        else {
          $!files.tree-store-set(
            $iter, TODO_COUNT_COL, "$data.elems()", DATA_KEY_COL, $abs-name
          );
          $!data-hash{$abs-name} = $data;
        }

        $found = True;
        last;
      }

      elsif $order == Same {
        # Part is found, go one level deeper if possible
        if @path-parts.elems {
          $s( @path-parts, :$ts-iter-path);
        }

        # Coming back from the top means that all is done only other
        # columns needs to set
        else {
          $!files.tree-store-set(
            $iter, TODO_COUNT_COL, "$data.elems()", DATA_KEY_COL, $abs-name
          );
          $!data-hash{$abs-name} = $data;
        }

        $found = True;
        last;
      }

      else { # $order == More
        # Not found yet, try next entry
        $ts-iter-path[*-1]++;
      }
    }

    # Tree path in tree store is not found on this level => $iter not defined
    if !$found {
      $iter = self!get-parent-iter($ts-iter-path);
      $iter = $!files.tree-store-append($iter);

      $!files.tree-store-set-value( $iter, FILENAME_COL, $part);

      if @path-parts.elems {
        $s( @path-parts, :$ts-iter-path)
      }

      else {
        $!files.tree-store-set(
          $iter, TODO_COUNT_COL, "$data.elems()", DATA_KEY_COL, $abs-name
        );
        $!data-hash{$abs-name} = $data;
      }
    }
  }

  # Start inserting
  $s($filename-path.split('/'));
}

#-------------------------------------------------------------------------------
method !get-iter ( Array $store-location --> Gnome::Gtk3::TreeIter ) {

  my Gnome::Gtk3::TreePath $tp .= new(
    :string($store-location[0 .. *-1].join(':'))
  );

  $!files.tree-model-get-iter($tp)
}

#-------------------------------------------------------------------------------
method !get-parent-iter ( Array $store-location --> Gnome::Gtk3::TreeIter ) {

  my Gnome::Gtk3::TreeIter $parent-iter;
  if $store-location.elems > 1 {
    $parent-iter = $!files.tree-model-get-iter(
      Gnome::Gtk3::TreePath.new(:string($store-location[0 ..^ *-1].join(':')))
    );
  }

  else {
    # Not enough elements, return an invalid iterator
    $parent-iter .= new(:native-object(N-GtkTreeIter));
  }

  $parent-iter
}

Initialization

The graphical user interface needs more explanation. First a lot more modules are needed [5-16].

  • Main [7] is for the main loop control and stopping the program. Initialization is also done there but is hidden.
  • Window [8] is the window where all widgets are placed in.
  • Grid [9] The grid is where the tables will be.
  • TreeView [10] is for displaying the tables.
  • ListStore [12] and TreeStore [13] are used to store the values. These modules are models used by the TreeView.
  • A CellRendererText [14] is also used by TreeView to render the values retrieved from the ListStore and TreeStore.
  • TreePath [15] and TreeIter [16] are used to add, get or delete rows in the store modules.
  • The Type [5] and Value [6] modules are needed to provide a way to get the values out of the store locations for comparison or other operations.

Then the GuiHandlers module is loaded which is needed to handle signals.

The tables have column numbers starting from 0. It is therefore useful to enumerate them to have more meaningful names [21,22]. FileListColumns is for the first table where we want to show filenames and the number of marker entries. The third column will be invisible and holds a key into a hash table which can be retrieved later when the row is clicked on. MarkerListColumns is for the second table which shows the marker word, a line number and its text. The second table is filled in when a row from the first table is clicked (double click action!).

The BUILD submethod

The BUILD() submethod is responsible for setting up the interface. It starts with the creation of a window [37] and a grid [38] which is added to the window. Then a file list table is created [43] and inserted in the grid in the upper left corner [44]. The marker table is then generated [46] and also added in the upper right corner [47].

After all this we have to register some signals to get some action in our interface. First the handler classes must be initialized, GuiHandlers::ListView handler at [49] and GuiHandlers::Application on [60]. We register the sub select-list-entry() to handle the row-activated signal emitted from the files table when an entry was double clicked [50-54]. We provide some more information to the handler sub such as the data hash with all marker data, the column in the table to find the key into the data hash and the marker and files table. We also register select-marker-entry() to also handle a row-activated signal but this time on the marker table object [56-58]. The extra data provided is the marker data table. At last we make the user interface visible by calling $w.show-all() [63].

Method create-file-table

The first table is created in method create-file-table() [80-111]. $!files is a Gnome::Gtk3::TreeStore object. This means the the first column in those tables can be tree like display. Tree- and ListStores are models holding data. The store will have rows of three columns which are all of type string [81]. $!fs-table is a Gnome::Gtk3::TreeView object which is responsible to display the data. When initialized, it receives a model which, in this case, is the TreeStore $!files [82].

All columns must be rendered by a renderer. Here, in all cases this will be the Gnome::Gtk3::CellRendererText class [87]. We are changing the way text is displayed by setting a color. Blue for text and red for numbers. I’ve chosen to display the number of markers, an integer, to be displayed as a string because the entry will show 0 by default. Directory names are on their own row and cannot have marker information. It is ugly to show 0 on those rows. With a string type an empty string can be displayed. Then a Gnome::Gtk3::TreeViewColumn is created with a title for the column and the renderer for the column is added [90-92]. Then a link must be layed between the tree view and the renderer by pointing to a property in the text renderer text and specifying the column in the tree view [93]. Finally add the tree view column to the tree view.

The second column is for the number of markers found [96-103].

The last column in the Gnome::Gtk3::TreeStore $!files is the data key column. There is no need to setup a renderer for this column because we do not wish to see that column.

Here, it is important to note that not only you can decide to hide a column in a model, but that you can also show columns in other Gnome::Gtk3::TreeView widgets or even multiple widgets. Also, it is not necessary to show the same order of columns as they are in the model except that you want a hierarchical kind of value for the first column in a tree view whatever column it is in a tree store.

Method create-markers-table

The other table is setup by method create-markers-table() [107-142]. There we have 3 columns of which the middle one is an integer. The integer can still be displayed as a string and displayed in red.

Methods add-file-data and insert-in-table

The user of the interface calls add-file-data() [67-71] to add new data to the files table. It calls insert-in-table() [145-243] if there are any entries in the users data. That method splits the path to the file in parts which needs to be inserted one by one in the table. For this process, an anonymous subroutine is created which can be called recursively. The subroutine compares each part with existing entries on each level and decides to insert the part before the entry when the part is alphabetically higher, to go a level deeper with the next part if the entry is found or to try the next entry when the part is not yet found. When arrived at the end of a level, it appends the part as a new entry and then go a level deeper with the next part.

A path to a row in the model is a series of integers, a number for each level. When represented as a string the numbers are separated with a colon ‘:’. For example ‘0’ points to the first entry in the table, ‘2:1’ points to the third entry of the top level and the second entry on the next level. These paths are stored in Gnome::Gtk3::Path. To keep track on the path it is easier to manipulate when the integers are stored in an Array $ts-iter-path. The path is then used by an iterator Gnome::Gtk3::Iter which is used in turn by the viewer to manipulate the rows.