e129fcba035a2ab07ff2c1ebec6daeb52c6b3dab
[roobuilder] / src / codegen / valagtkmodule.vala
1 /* valagtkmodule.vala
2  *
3  * Copyright (C) 2013  Jürg Billeter
4  * Copyright (C) 2013-2014  Luca Bruno
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
19  *
20  * Author:
21  *      Luca Bruno <lucabru@src.gnome.org>
22  */
23
24
25 public class Vala.GtkModule : GSignalModule {
26
27         class InvalidClass : Class {
28                 public InvalidClass (string name) {
29                         base (name, null, null);
30                         error = true;
31                 }
32                 public override bool check (CodeContext context) {
33                         return false;
34                 }
35         }
36
37         class InvalidProperty : Property {
38                 public InvalidProperty (string name) {
39                         base (name, null, null, null);
40                         error = true;
41                 }
42                 public override bool check (CodeContext context) {
43                         return false;
44                 }
45         }
46
47         /* C type-func name to Vala class mapping */
48         private HashMap<string, Class> type_id_to_vala_map = null;
49         /* C class name to Vala class mapping */
50         private HashMap<string, Class> cclass_to_vala_map = null;
51         /* GResource name to real file name mapping */
52         private HashMap<string, string> gresource_to_file_map = null;
53         /* GtkBuilder xml handler set */
54         private HashMap<string, string> handler_map = new HashMap<string, string>(str_hash, str_equal);
55         /* GtkBuilder xml handler to Vala property mapping */
56         private HashMap<string, Property> current_handler_to_property_map = new HashMap<string, Property>(str_hash, str_equal);
57         /* GtkBuilder xml handler to Vala signal mapping */
58         private HashMap<string, Signal> current_handler_to_signal_map = new HashMap<string, Signal>(str_hash, str_equal);
59         /* GtkBuilder xml child to Vala class mapping */
60         private HashMap<string, Class> current_child_to_class_map = new HashMap<string, Class>(str_hash, str_equal);
61         /* Required custom application-specific gtype classes to be ref'd before initializing the template */
62         private List<Class> current_required_app_classes = new ArrayList<Class>();
63
64         /* Stack of occuring object elements in the template */
65         List<Class> current_object_stack = new ArrayList<Class> ();
66         Class? current_object;
67
68         void push_object (Class cl) {
69                 current_object_stack.add (current_object);
70                 current_object = cl;
71         }
72
73         void pop_object () {
74                 current_object = current_object_stack.remove_at (current_object_stack.size - 1);
75         }
76
77         /* Stack of occuring property elements in the template */
78         List<Property> current_property_stack = new ArrayList<Property> ();
79         Property? current_property;
80
81         void push_property (Property prop) {
82                 current_property_stack.add (current_property);
83                 current_property = prop;
84         }
85
86         void pop_property () {
87                 current_property = current_property_stack.remove_at (current_property_stack.size - 1);
88         }
89
90         private void ensure_type_id_to_vala_map () {
91                 // map C type-func name of gtypeinstance classes to Vala classes
92                 if (type_id_to_vala_map != null) {
93                         return;
94                 }
95                 type_id_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
96                 recurse_type_id_to_vala_map (context.root);
97         }
98
99         private void recurse_type_id_to_vala_map (Symbol sym) {
100                 unowned List<Class> classes;
101                 if (sym is Namespace) {
102                         foreach (var inner in ((Namespace) sym).get_namespaces()) {
103                                 recurse_type_id_to_vala_map (inner);
104                         }
105                         classes = ((Namespace) sym).get_classes ();
106                 } else if (sym is ObjectTypeSymbol) {
107                         classes = ((ObjectTypeSymbol) sym).get_classes ();
108                 } else {
109                         return;
110                 }
111                 foreach (var cl in classes) {
112                         if (!cl.is_compact) {
113                                 var type_id = get_ccode_type_id (cl);
114                                 if (type_id == null)
115                                         continue;
116
117                                 var i = type_id.index_of_char ('(');
118                                 if (i > 0) {
119                                         type_id = type_id.substring (0, i - 1).strip ();
120                                 } else {
121                                         type_id = type_id.strip ();
122                                 }
123                                 type_id_to_vala_map.set (type_id, cl);
124                         }
125                         recurse_type_id_to_vala_map (cl);
126                 }
127         }
128
129         private void ensure_cclass_to_vala_map () {
130                 // map C name of gtypeinstance classes to Vala classes
131                 if (cclass_to_vala_map != null) {
132                         return;
133                 }
134                 cclass_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
135                 recurse_cclass_to_vala_map (context.root);
136         }
137
138         private void recurse_cclass_to_vala_map (Symbol sym) {
139                 unowned List<Class> classes;
140                 if (sym is Namespace) {
141                         foreach (var inner in ((Namespace) sym).get_namespaces()) {
142                                 recurse_cclass_to_vala_map (inner);
143                         }
144                         classes = ((Namespace) sym).get_classes ();
145                 } else if (sym is ObjectTypeSymbol) {
146                         classes = ((ObjectTypeSymbol) sym).get_classes ();
147                 } else {
148                         return;
149                 }
150                 foreach (var cl in classes) {
151                         if (!cl.is_compact) {
152                                 cclass_to_vala_map.set (get_ccode_name (cl), cl);
153                         }
154                         recurse_cclass_to_vala_map (cl);
155                 }
156         }
157
158         private void ensure_gresource_to_file_map () {
159                 // map gresource paths to real file names
160                 if (gresource_to_file_map != null) {
161                         return;
162                 }
163                 gresource_to_file_map = new HashMap<string, string>(str_hash, str_equal);
164                 foreach (var gresource in context.gresources) {
165                         if (!FileUtils.test (gresource, FileTest.EXISTS)) {
166                                 Report.error (null, "GResources file `%s' does not exist", gresource);
167                                 continue;
168                         }
169
170                         MarkupReader reader = new MarkupReader (gresource);
171
172                         int state = 0;
173                         string prefix = null;
174                         string alias = null;
175
176                         MarkupTokenType current_token = reader.read_token (null, null);
177                         while (current_token != MarkupTokenType.EOF) {
178                                 if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "gresource") {
179                                         prefix = reader.get_attribute ("prefix");
180                                 } else if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "file") {
181                                         alias = reader.get_attribute ("alias");
182                                         state = 1;
183                                 } else if (state == 1 && current_token == MarkupTokenType.TEXT) {
184                                         var name = reader.content;
185                                         var filename = context.get_gresource_path (gresource, name);
186                                         if (alias != null) {
187                                                 gresource_to_file_map.set (Path.build_filename (prefix, alias), filename);
188                                         }
189                                         gresource_to_file_map.set (Path.build_filename (prefix, name), filename);
190                                         state = 0;
191                                 }
192                                 current_token = reader.read_token (null, null);
193                         }
194                 }
195         }
196
197         private void process_current_ui_resource (string ui_resource, CodeNode node) {
198                 /* Scan a single gtkbuilder file for signal handlers in <object> elements,
199                    and save an handler string -> Vala.Signal mapping for each of them */
200                 ensure_type_id_to_vala_map ();
201                 ensure_cclass_to_vala_map();
202                 ensure_gresource_to_file_map();
203
204                 current_handler_to_signal_map = null;
205                 current_child_to_class_map = null;
206                 var ui_file = gresource_to_file_map.get (ui_resource);
207                 if (ui_file == null || !FileUtils.test (ui_file, FileTest.EXISTS)) {
208                         node.error = true;
209                         Report.error (node.source_reference, "UI resource not found: `%s'. Please make sure to specify the proper GResources xml files with --gresources and alternative search locations with --gresourcesdir.", ui_resource);
210                         return;
211                 }
212                 handler_map = new HashMap<string, string>(str_hash, str_equal);
213                 current_handler_to_signal_map = new HashMap<string, Signal>(str_hash, str_equal);
214                 current_child_to_class_map = new HashMap<string, Class>(str_hash, str_equal);
215                 current_object_stack = new ArrayList<Class> ();
216                 current_property_stack = new ArrayList<Property> ();
217
218                 MarkupReader reader = new MarkupReader (ui_file);
219                 string? current_handler = null;
220
221                 bool template_tag_found = false;
222                 MarkupTokenType current_token = reader.read_token (null, null);
223                 while (current_token != MarkupTokenType.EOF) {
224                         unowned string current_name = reader.name;
225                         if (current_token == MarkupTokenType.START_ELEMENT && (current_name == "object" || current_name == "template")) {
226                                 Class? current_class = null;
227
228                                 if (current_name == "object") {
229                                         var type_id = reader.get_attribute ("type-func");
230                                         if (type_id != null) {
231                                                 current_class = type_id_to_vala_map.get (type_id);
232                                         }
233                                 } else if (current_name == "template") {
234                                         template_tag_found = true;
235                                 }
236
237                                 if (current_class == null) {
238                                         var class_name = reader.get_attribute ("class");
239                                         if (class_name == null) {
240                                                 Report.error (node.source_reference, "Invalid %s in ui file `%s'", current_name, ui_file);
241                                                 current_token = reader.read_token (null, null);
242                                                 continue;
243                                         }
244                                         current_class = cclass_to_vala_map.get (class_name);
245
246                                         if (current_class == null) {
247                                                 push_object (new InvalidClass (class_name));
248                                                 if (current_name == "template") {
249                                                         Report.error (node.source_reference, "Unknown template `%s' in ui file `%s'", class_name, ui_file);
250                                                 } else {
251                                                         Report.warning (node.source_reference, "Unknown object `%s' in ui file `%s'", class_name, ui_file);
252                                                 }
253                                         }
254                                 }
255
256                                 if (current_class != null) {
257                                         var child_name = reader.get_attribute ("id");
258                                         if (child_name != null) {
259                                                 current_child_to_class_map.set (child_name, current_class);
260                                         }
261                                         push_object (current_class);
262                                 }
263                         } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "object" || current_name == "template")) {
264                                 pop_object ();
265                         } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "signal") {
266                                 var signal_name = reader.get_attribute ("name");
267                                 var handler_name = reader.get_attribute ("handler");
268
269                                 if (signal_name == null || handler_name == null) {
270                                         if (signal_name != null) {
271                                                 Report.error (node.source_reference, "Invalid signal `%s' without handler in ui file `%s'", signal_name, ui_file);
272                                         } else if (handler_name != null) {
273                                                 Report.error (node.source_reference, "Invalid signal without name in ui file `%s'", ui_file);
274                                         } else {
275                                                 Report.error (node.source_reference, "Invalid signal without name and handler in ui file `%s'", ui_file);
276                                         }
277                                         current_token = reader.read_token (null, null);
278                                         continue;
279                                 }
280                                 var sep_idx = signal_name.index_of ("::");
281                                 if (sep_idx >= 0) {
282                                         // detailed signal, we don't care about the detail
283                                         signal_name = signal_name.substring (0, sep_idx);
284                                 }
285
286                                 var sig = SemanticAnalyzer.symbol_lookup_inherited (current_object, signal_name.replace ("-", "_")) as Signal;
287                                 if (sig != null) {
288                                         current_handler_to_signal_map.set (handler_name, sig);
289                                 } else {
290                                         Report.error (node.source_reference, "Unknown signal `%s::%s' in ui file `%s'", current_object.get_full_name (), signal_name, ui_file);
291                                         current_token = reader.read_token (null, null);
292                                         continue;
293                                 }
294                         } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && (current_name == "property" || current_name == "binding")) {
295                                 var property_name = reader.get_attribute ("name");
296                                 if (property_name == null) {
297                                         Report.error (node.source_reference, "Invalid binding in ui file `%s'", ui_file);
298                                         current_token = reader.read_token (null, null);
299                                         continue;
300                                 }
301
302                                 property_name = property_name.replace ("-", "_");
303                                 var property = SemanticAnalyzer.symbol_lookup_inherited (current_object, property_name) as Property;
304                                 if (property != null) {
305                                         push_property (property);
306                                 } else {
307                                         push_property (new InvalidProperty (property_name));
308                                         if (current_name == "binding") {
309                                                 Report.error (node.source_reference, "Unknown property `%s:%s' for binding in ui file `%s'", current_object.get_full_name (), property_name, ui_file);
310                                         }
311                                         current_token = reader.read_token (null, null);
312                                         continue;
313                                 }
314                         } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "property" || current_name == "binding")) {
315                                 pop_property ();
316                         } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "closure") {
317                                 var handler_name = reader.get_attribute ("function");
318
319                                 if (current_property != null) {
320                                         if (handler_name == null) {
321                                                 Report.error (node.source_reference, "Invalid closure in ui file `%s'", ui_file);
322                                                 current_token = reader.read_token (null, null);
323                                                 continue;
324                                         }
325                                         if (current_property is InvalidProperty) {
326                                                 Report.error (node.source_reference, "Unknown property `%s:%s' for binding in ui file `%s'", current_object.get_full_name (), current_property.name, ui_file);
327                                         }
328
329                                         //TODO Retrieve signature declaration? c-type to vala-type?
330                                         current_handler_to_property_map.set (handler_name, current_property);
331                                         current_handler = handler_name;
332                                 } else if (current_handler != null) {
333                                         // Track nested closure elements
334                                         handler_map.set (handler_name, current_handler);
335                                         current_handler = handler_name;
336                                 }
337                         }
338                         current_token = reader.read_token (null, null);
339                 }
340
341                 if (!template_tag_found) {
342                         Report.error (node.source_reference, "ui resource `%s' does not describe a valid composite template", ui_resource);
343                 }
344         }
345
346         private bool is_gtk_template (Class cl) {
347                 var attr = cl.get_attribute ("GtkTemplate");
348                 if (attr != null) {
349                         if (gtk_widget_type == null || !cl.is_subtype_of (gtk_widget_type)) {
350                                 if (!cl.error) {
351                                         Report.error (attr.source_reference, "subclassing Gtk.Widget is required for using Gtk templates");
352                                         cl.error = true;
353                                 }
354                                 return false;
355                         }
356                         return true;
357                 }
358                 return false;
359         }
360
361         public override void generate_class_init (Class cl) {
362                 base.generate_class_init (cl);
363
364                 if (cl.error || !is_gtk_template (cl)) {
365                         return;
366                 }
367
368                 /* Gtk builder widget template */
369                 var ui = cl.get_attribute_string ("GtkTemplate", "ui");
370                 if (ui == null) {
371                         Report.error (cl.source_reference, "empty ui resource declaration for Gtk widget template");
372                         cl.error = true;
373                         return;
374                 }
375
376                 process_current_ui_resource (ui, cl);
377
378                 var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_set_template_from_resource"));
379                 call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
380                 call.add_argument (new CCodeConstant ("\"%s\"".printf (ui)));
381                 ccode.add_expression (call);
382
383                 current_required_app_classes.clear ();
384         }
385
386         public override void visit_property (Property prop) {
387                 if (prop.get_attribute ("GtkChild") != null && prop.field == null) {
388                         Report.error (prop.source_reference, "[GtkChild] is only allowed on automatic properties");
389                 }
390
391                 base.visit_property (prop);
392         }
393
394         public override void visit_field (Field f) {
395                 base.visit_field (f);
396
397                 var cl = current_class;
398                 if (cl == null || cl.error) {
399                         return;
400                 }
401
402                 if (f.binding != MemberBinding.INSTANCE || f.get_attribute ("GtkChild") == null) {
403                         return;
404                 }
405
406                 /* If the field has a [GtkChild] attribute but its class doesn'thave a
407                          [GtkTemplate] attribute, we throw an error */
408                 if (!is_gtk_template (cl)) {
409                         Report.error (f.source_reference, "[GtkChild] is only allowed in classes with a [GtkTemplate] attribute");
410                         return;
411                 }
412
413                 push_context (class_init_context);
414
415                 /* Map ui widget to a class field */
416                 var gtk_name = f.get_attribute_string ("GtkChild", "name", f.name);
417                 var child_class = current_child_to_class_map.get (gtk_name);
418                 if (child_class == null) {
419                         Report.error (f.source_reference, "could not find child `%s'", gtk_name);
420                         return;
421                 }
422
423                 /* We allow Gtk child to have stricter type than class field */
424                 unowned Class? field_class = f.variable_type.type_symbol as Class;
425                 if (field_class == null || !child_class.is_subtype_of (field_class)) {
426                         Report.error (f.source_reference, "cannot convert from Gtk child type `%s' to `%s'", child_class.get_full_name(), field_class.get_full_name());
427                         return;
428                 }
429
430                 var internal_child = f.get_attribute_bool ("GtkChild", "internal");
431
432                 CCodeExpression offset;
433                 if (f.is_private_symbol ()) {
434                         // new glib api, we add the private struct offset to get the final field offset out of the instance
435                         var private_field_offset = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET"));
436                         private_field_offset.add_argument (new CCodeIdentifier ("%sPrivate".printf (get_ccode_name (cl))));
437                         private_field_offset.add_argument (new CCodeIdentifier (get_ccode_name (f)));
438                         offset = new CCodeBinaryExpression (CCodeBinaryOperator.PLUS, new CCodeIdentifier ("%s_private_offset".printf (get_ccode_name (cl))), private_field_offset);
439                 } else {
440                         var offset_call = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET"));
441                         offset_call.add_argument (new CCodeIdentifier (get_ccode_name (cl)));
442                         offset_call.add_argument (new CCodeIdentifier (get_ccode_name (f)));
443                         offset = offset_call;
444                 }
445
446                 var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_child_full"));
447                 call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
448                 call.add_argument (new CCodeConstant ("\"%s\"".printf (gtk_name)));
449                 call.add_argument (new CCodeConstant (internal_child ? "TRUE" : "FALSE"));
450                 call.add_argument (offset);
451                 ccode.add_expression (call);
452
453                 pop_context ();
454
455                 if (!field_class.external && !field_class.external_package) {
456                         current_required_app_classes.add (field_class);
457                 }
458         }
459
460         public override void visit_method (Method m) {
461                 base.visit_method (m);
462
463                 var cl = current_class;
464                 if (cl == null || cl.error || !is_gtk_template (cl)) {
465                         return;
466                 }
467
468                 if (m.get_attribute ("GtkCallback") == null) {
469                         return;
470                 }
471
472                 /* Handler name as defined in the gtkbuilder xml */
473                 var handler_name = m.get_attribute_string ("GtkCallback", "name", m.name);
474                 var callback = handler_map.get (handler_name);
475                 var sig = current_handler_to_signal_map.get (handler_name);
476                 var prop = current_handler_to_property_map.get (handler_name);
477                 if (callback == null && sig == null && prop == null) {
478                         Report.error (m.source_reference, "could not find signal or property for handler `%s'", handler_name);
479                         return;
480                 }
481
482                 push_context (class_init_context);
483
484                 if (sig != null) {
485                         sig.check (context);
486                         var method_type = new MethodType (m);
487                         var signal_type = new SignalType (sig);
488                         var delegate_type = signal_type.get_handler_type ();
489                         if (!method_type.compatible (delegate_type)) {
490                                 Report.error (m.source_reference, "method `%s' is incompatible with signal `%s', expected `%s'", method_type.to_string (), delegate_type.to_string (), delegate_type.to_prototype_string (m.name));
491                         } else {
492                                 var wrapper = generate_delegate_wrapper (m, signal_type.get_handler_type (), m);
493
494                                 var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full"));
495                                 call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
496                                 call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name)));
497                                 call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (wrapper)));
498                                 ccode.add_expression (call);
499                         }
500                 }
501                 if (prop != null || callback != null) {
502                         if (prop != null) {
503                                 prop.check (context);
504                         }
505                         //TODO Perform signature check
506                         var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full"));
507                         call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
508                         call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name)));
509                         call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (get_ccode_name (m))));
510                         ccode.add_expression (call);
511                 }
512
513                 pop_context ();
514         }
515
516
517         public override void end_instance_init (Class cl) {
518                 if (cl == null || cl.error || !is_gtk_template (cl)) {
519                         return;
520                 }
521
522                 foreach (var req in current_required_app_classes) {
523                         /* ensure custom application widgets are initialized */
524                         var call = new CCodeFunctionCall (new CCodeIdentifier ("g_type_ensure"));
525                         call.add_argument (get_type_id_expression (SemanticAnalyzer.get_data_type_for_symbol (req)));
526                         ccode.add_expression (call);
527                 }
528
529                 var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_init_template"));
530                 call.add_argument (new CCodeIdentifier ("GTK_WIDGET (self)"));
531                 ccode.add_expression (call);
532         }
533 }
534