Line data Source code
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 5828 : public class Vala.GtkModule : GSignalModule {
26 :
27 1 : class InvalidClass : Class {
28 3 : public InvalidClass (string name) {
29 1 : base (name, null, null);
30 1 : error = true;
31 : }
32 0 : public override bool check (CodeContext context) {
33 : return false;
34 : }
35 : }
36 :
37 8 : class InvalidProperty : Property {
38 111 : public InvalidProperty (string name) {
39 37 : base (name, null, null, null);
40 37 : error = true;
41 : }
42 0 : public override bool check (CodeContext context) {
43 : return false;
44 : }
45 : }
46 :
47 : /* C type-func name to Vala class mapping */
48 1457 : private HashMap<string, Class> type_id_to_vala_map = null;
49 : /* C class name to Vala class mapping */
50 1457 : private HashMap<string, Class> cclass_to_vala_map = null;
51 : /* GResource name to real file name mapping */
52 1457 : private HashMap<string, string> gresource_to_file_map = null;
53 : /* GtkBuilder xml handler set */
54 2914 : private HashMap<string, string> handler_map = new HashMap<string, string>(str_hash, str_equal);
55 : /* GtkBuilder xml handler to Vala property mapping */
56 2914 : 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 2914 : 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 2914 : 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 2914 : private List<Class> current_required_app_classes = new ArrayList<Class>();
63 :
64 : /* Stack of occuring object elements in the template */
65 2914 : List<Class> current_object_stack = new ArrayList<Class> ();
66 1457 : Class? current_object;
67 :
68 40 : void push_object (Class cl) {
69 40 : current_object_stack.add (current_object);
70 11613 : current_object = cl;
71 : }
72 :
73 40 : void pop_object () {
74 40 : current_object = current_object_stack.remove_at (current_object_stack.size - 1);
75 : }
76 :
77 : /* Stack of occuring property elements in the template */
78 2914 : List<Property> current_property_stack = new ArrayList<Property> ();
79 1457 : Property? current_property;
80 :
81 168 : void push_property (Property prop) {
82 168 : current_property_stack.add (current_property);
83 168 : current_property = prop;
84 : }
85 :
86 168 : void pop_property () {
87 168 : current_property = current_property_stack.remove_at (current_property_stack.size - 1);
88 : }
89 :
90 8 : private void ensure_type_id_to_vala_map () {
91 : // map C type-func name of gtypeinstance classes to Vala classes
92 8 : if (type_id_to_vala_map != null) {
93 : return;
94 : }
95 8 : type_id_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
96 8 : recurse_type_id_to_vala_map (context.root);
97 : }
98 :
99 5952 : private void recurse_type_id_to_vala_map (Symbol sym) {
100 : unowned List<Class> classes;
101 5952 : if (sym is Namespace) {
102 1328 : foreach (var inner in ((Namespace) sym).get_namespaces()) {
103 440 : recurse_type_id_to_vala_map (inner);
104 : }
105 448 : classes = ((Namespace) sym).get_classes ();
106 5504 : } else if (sym is ObjectTypeSymbol) {
107 5504 : classes = ((ObjectTypeSymbol) sym).get_classes ();
108 : } else {
109 : return;
110 : }
111 16960 : foreach (var cl in classes) {
112 9112 : if (!cl.is_compact) {
113 3608 : var type_id = get_ccode_type_id (cl);
114 3608 : if (type_id == null)
115 0 : continue;
116 :
117 3608 : var i = type_id.index_of_char ('(');
118 3608 : if (i > 0) {
119 3352 : type_id = type_id.substring (0, i - 1).strip ();
120 : } else {
121 256 : type_id = type_id.strip ();
122 : }
123 3608 : type_id_to_vala_map.set (type_id, cl);
124 : }
125 5504 : recurse_type_id_to_vala_map (cl);
126 : }
127 : }
128 :
129 8 : private void ensure_cclass_to_vala_map () {
130 : // map C name of gtypeinstance classes to Vala classes
131 8 : if (cclass_to_vala_map != null) {
132 : return;
133 : }
134 8 : cclass_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
135 8 : recurse_cclass_to_vala_map (context.root);
136 : }
137 :
138 5952 : private void recurse_cclass_to_vala_map (Symbol sym) {
139 : unowned List<Class> classes;
140 5952 : if (sym is Namespace) {
141 1328 : foreach (var inner in ((Namespace) sym).get_namespaces()) {
142 440 : recurse_cclass_to_vala_map (inner);
143 : }
144 448 : classes = ((Namespace) sym).get_classes ();
145 5504 : } else if (sym is ObjectTypeSymbol) {
146 5504 : classes = ((ObjectTypeSymbol) sym).get_classes ();
147 : } else {
148 : return;
149 : }
150 16960 : foreach (var cl in classes) {
151 5504 : if (!cl.is_compact) {
152 3608 : cclass_to_vala_map.set (get_ccode_name (cl), cl);
153 : }
154 5504 : recurse_cclass_to_vala_map (cl);
155 : }
156 : }
157 :
158 8 : private void ensure_gresource_to_file_map () {
159 : // map gresource paths to real file names
160 8 : if (gresource_to_file_map != null) {
161 : return;
162 : }
163 8 : gresource_to_file_map = new HashMap<string, string>(str_hash, str_equal);
164 24 : foreach (var gresource in context.gresources) {
165 8 : if (!FileUtils.test (gresource, FileTest.EXISTS)) {
166 0 : Report.error (null, "GResources file `%s' does not exist", gresource);
167 0 : continue;
168 : }
169 :
170 8 : MarkupReader reader = new MarkupReader (gresource);
171 :
172 8 : int state = 0;
173 8 : string prefix = null;
174 8 : string alias = null;
175 :
176 8 : MarkupTokenType current_token = reader.read_token (null, null);
177 80 : while (current_token != MarkupTokenType.EOF) {
178 72 : if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "gresource") {
179 8 : prefix = reader.get_attribute ("prefix");
180 64 : } else if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "file") {
181 8 : alias = reader.get_attribute ("alias");
182 8 : state = 1;
183 64 : } else if (state == 1 && current_token == MarkupTokenType.TEXT) {
184 8 : var name = reader.content;
185 8 : var filename = context.get_gresource_path (gresource, name);
186 8 : if (alias != null) {
187 0 : gresource_to_file_map.set (Path.build_filename (prefix, alias), filename);
188 : }
189 8 : gresource_to_file_map.set (Path.build_filename (prefix, name), filename);
190 8 : state = 0;
191 : }
192 72 : current_token = reader.read_token (null, null);
193 : }
194 : }
195 : }
196 :
197 16 : 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 8 : ensure_type_id_to_vala_map ();
201 8 : ensure_cclass_to_vala_map();
202 8 : ensure_gresource_to_file_map();
203 :
204 8 : current_handler_to_signal_map = null;
205 8 : current_child_to_class_map = null;
206 8 : var ui_file = gresource_to_file_map.get (ui_resource);
207 8 : if (ui_file == null || !FileUtils.test (ui_file, FileTest.EXISTS)) {
208 0 : node.error = true;
209 0 : 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 0 : return;
211 : }
212 8 : handler_map = new HashMap<string, string>(str_hash, str_equal);
213 8 : current_handler_to_signal_map = new HashMap<string, Signal>(str_hash, str_equal);
214 8 : current_child_to_class_map = new HashMap<string, Class>(str_hash, str_equal);
215 8 : current_object_stack = new ArrayList<Class> ();
216 8 : current_property_stack = new ArrayList<Property> ();
217 :
218 8 : MarkupReader reader = new MarkupReader (ui_file);
219 8 : string? current_handler = null;
220 :
221 8 : bool template_tag_found = false;
222 8 : MarkupTokenType current_token = reader.read_token (null, null);
223 880 : while (current_token != MarkupTokenType.EOF) {
224 872 : unowned string current_name = reader.name;
225 872 : if (current_token == MarkupTokenType.START_ELEMENT && (current_name == "object" || current_name == "template")) {
226 40 : Class? current_class = null;
227 :
228 72 : if (current_name == "object") {
229 32 : var type_id = reader.get_attribute ("type-func");
230 32 : if (type_id != null) {
231 0 : current_class = type_id_to_vala_map.get (type_id);
232 : }
233 8 : } else if (current_name == "template") {
234 : template_tag_found = true;
235 : }
236 :
237 112 : if (current_class == null) {
238 40 : var class_name = reader.get_attribute ("class");
239 40 : if (class_name == null) {
240 0 : Report.error (node.source_reference, "Invalid %s in ui file `%s'", current_name, ui_file);
241 0 : current_token = reader.read_token (null, null);
242 0 : continue;
243 : }
244 40 : current_class = cclass_to_vala_map.get (class_name);
245 :
246 40 : if (current_class == null) {
247 1 : push_object (new InvalidClass (class_name));
248 1 : if (current_name == "template") {
249 1 : Report.error (node.source_reference, "Unknown template `%s' in ui file `%s'", class_name, ui_file);
250 : } else {
251 0 : Report.warning (node.source_reference, "Unknown object `%s' in ui file `%s'", class_name, ui_file);
252 : }
253 : }
254 : }
255 :
256 79 : if (current_class != null) {
257 39 : var child_name = reader.get_attribute ("id");
258 39 : if (child_name != null) {
259 24 : current_child_to_class_map.set (child_name, current_class);
260 : }
261 39 : push_object (current_class);
262 : }
263 832 : } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "object" || current_name == "template")) {
264 40 : pop_object ();
265 808 : } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "signal") {
266 16 : var signal_name = reader.get_attribute ("name");
267 16 : var handler_name = reader.get_attribute ("handler");
268 :
269 16 : if (signal_name == null || handler_name == null) {
270 0 : if (signal_name != null) {
271 0 : Report.error (node.source_reference, "Invalid signal `%s' without handler in ui file `%s'", signal_name, ui_file);
272 0 : } else if (handler_name != null) {
273 0 : Report.error (node.source_reference, "Invalid signal without name in ui file `%s'", ui_file);
274 : } else {
275 0 : Report.error (node.source_reference, "Invalid signal without name and handler in ui file `%s'", ui_file);
276 : }
277 0 : current_token = reader.read_token (null, null);
278 0 : continue;
279 : }
280 16 : var sep_idx = signal_name.index_of ("::");
281 16 : if (sep_idx >= 0) {
282 : // detailed signal, we don't care about the detail
283 0 : signal_name = signal_name.substring (0, sep_idx);
284 : }
285 :
286 16 : var sig = SemanticAnalyzer.symbol_lookup_inherited (current_object, signal_name.replace ("-", "_")) as Signal;
287 16 : if (sig != null) {
288 16 : current_handler_to_signal_map.set (handler_name, sig);
289 : } else {
290 0 : Report.error (node.source_reference, "Unknown signal `%s::%s' in ui file `%s'", current_object.get_full_name (), signal_name, ui_file);
291 0 : current_token = reader.read_token (null, null);
292 0 : continue;
293 : }
294 907 : } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && (current_name == "property" || current_name == "binding")) {
295 168 : var property_name = reader.get_attribute ("name");
296 168 : if (property_name == null) {
297 0 : Report.error (node.source_reference, "Invalid %s without name in ui file `%s'", current_name, ui_file);
298 0 : current_token = reader.read_token (null, null);
299 0 : continue;
300 : }
301 :
302 168 : property_name = property_name.replace ("-", "_");
303 168 : var property = SemanticAnalyzer.symbol_lookup_inherited (current_object, property_name) as Property;
304 168 : if (property != null) {
305 131 : push_property (property);
306 : } else {
307 37 : push_property (new InvalidProperty (property_name));
308 37 : if (current_name == "binding") {
309 0 : 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 37 : current_token = reader.read_token (null, null);
312 37 : continue;
313 : }
314 608 : } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "property" || current_name == "binding")) {
315 168 : pop_property ();
316 448 : } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "closure") {
317 8 : var handler_name = reader.get_attribute ("function");
318 :
319 8 : if (current_property != null) {
320 8 : if (handler_name == null) {
321 0 : Report.error (node.source_reference, "Invalid %s without function in ui file `%s'", current_name, ui_file);
322 0 : current_token = reader.read_token (null, null);
323 0 : continue;
324 : }
325 8 : if (current_property is InvalidProperty) {
326 0 : 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 8 : current_handler_to_property_map.set (handler_name, current_property);
331 16 : current_handler = handler_name;
332 0 : } else if (current_handler != null) {
333 : // Track nested closure elements
334 0 : handler_map.set (handler_name, current_handler);
335 0 : current_handler = handler_name;
336 : }
337 : }
338 835 : current_token = reader.read_token (null, null);
339 : }
340 :
341 8 : if (!template_tag_found) {
342 0 : Report.error (node.source_reference, "ui resource `%s' does not describe a valid composite template", ui_resource);
343 : }
344 : }
345 :
346 4713 : private bool is_gtk_template (Class cl) {
347 4713 : var attr = cl.get_attribute ("GtkTemplate");
348 38 : if (attr != null) {
349 38 : if (gtk_widget_type == null || !cl.is_subtype_of (gtk_widget_type)) {
350 1 : if (!cl.error) {
351 1 : Report.error (attr.source_reference, "subclassing Gtk.Widget is required for using Gtk templates");
352 1 : cl.error = true;
353 : }
354 1 : return false;
355 : }
356 37 : return true;
357 : }
358 4713 : return false;
359 : }
360 :
361 742 : public override void generate_class_init (Class cl) {
362 734 : base.generate_class_init (cl);
363 :
364 734 : if (cl.error || !is_gtk_template (cl)) {
365 726 : return;
366 : }
367 :
368 : /* Gtk builder widget template */
369 8 : var ui = cl.get_attribute_string ("GtkTemplate", "ui");
370 8 : if (ui == null) {
371 0 : Report.error (cl.source_reference, "empty ui resource declaration for Gtk widget template");
372 0 : cl.error = true;
373 0 : return;
374 : }
375 :
376 8 : process_current_ui_resource (ui, cl);
377 :
378 8 : var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_set_template_from_resource"));
379 8 : call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
380 8 : call.add_argument (new CCodeConstant ("\"%s\"".printf (ui)));
381 8 : ccode.add_expression (call);
382 :
383 8 : current_required_app_classes.clear ();
384 : }
385 :
386 510 : public override void visit_property (Property prop) {
387 510 : if (prop.has_attribute ("GtkChild") && prop.field == null) {
388 1 : Report.error (prop.source_reference, "[GtkChild] is only allowed on automatic properties");
389 : }
390 :
391 510 : base.visit_property (prop);
392 : }
393 :
394 1626 : public override void visit_field (Field f) {
395 1624 : base.visit_field (f);
396 :
397 1624 : var cl = current_class;
398 1274 : if (cl == null || cl.error) {
399 350 : return;
400 : }
401 :
402 1274 : if (f.binding != MemberBinding.INSTANCE || !f.has_attribute ("GtkChild")) {
403 1268 : 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 6 : if (!is_gtk_template (cl)) {
409 1 : Report.error (f.source_reference, "[GtkChild] is only allowed in classes with a [GtkTemplate] attribute");
410 1 : return;
411 : }
412 :
413 5 : push_context (class_init_context);
414 :
415 : /* Map ui widget to a class field */
416 5 : var gtk_name = f.get_attribute_string ("GtkChild", "name", f.name);
417 5 : var child_class = current_child_to_class_map.get (gtk_name);
418 5 : if (child_class == null) {
419 2 : Report.error (f.source_reference, "could not find child `%s'", gtk_name);
420 2 : return;
421 : }
422 :
423 : /* We allow Gtk child to have stricter type than class field */
424 3 : unowned Class? field_class = f.variable_type.type_symbol as Class;
425 3 : if (field_class == null || !child_class.is_subtype_of (field_class)) {
426 1 : 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 1 : return;
428 : }
429 :
430 2 : var internal_child = f.get_attribute_bool ("GtkChild", "internal");
431 :
432 : CCodeExpression offset;
433 3 : 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 1 : var private_field_offset = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET"));
436 1 : private_field_offset.add_argument (new CCodeIdentifier ("%sPrivate".printf (get_ccode_name (cl))));
437 1 : private_field_offset.add_argument (new CCodeIdentifier (get_ccode_name (f)));
438 1 : offset = new CCodeBinaryExpression (CCodeBinaryOperator.PLUS, new CCodeIdentifier ("%s_private_offset".printf (get_ccode_name (cl))), private_field_offset);
439 : } else {
440 1 : var offset_call = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET"));
441 1 : offset_call.add_argument (new CCodeIdentifier (get_ccode_name (cl)));
442 1 : offset_call.add_argument (new CCodeIdentifier (get_ccode_name (f)));
443 1 : offset = offset_call;
444 : }
445 :
446 2 : var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_child_full"));
447 2 : call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
448 2 : call.add_argument (new CCodeConstant ("\"%s\"".printf (gtk_name)));
449 2 : call.add_argument (new CCodeConstant (internal_child ? "TRUE" : "FALSE"));
450 2 : call.add_argument (offset);
451 2 : ccode.add_expression (call);
452 :
453 2 : pop_context ();
454 :
455 2 : if (!field_class.external && !field_class.external_package) {
456 0 : current_required_app_classes.add (field_class);
457 : }
458 : }
459 :
460 5072 : public override void visit_method (Method m) {
461 5068 : base.visit_method (m);
462 :
463 5068 : var cl = current_class;
464 3197 : if (cl == null || cl.error || !is_gtk_template (cl)) {
465 5052 : return;
466 : }
467 :
468 16 : if (!m.has_attribute ("GtkCallback")) {
469 11 : return;
470 : }
471 :
472 : /* Handler name as defined in the gtkbuilder xml */
473 5 : var handler_name = m.get_attribute_string ("GtkCallback", "name", m.name);
474 5 : var callback = handler_map.get (handler_name);
475 5 : var sig = current_handler_to_signal_map.get (handler_name);
476 5 : var prop = current_handler_to_property_map.get (handler_name);
477 5 : if (callback == null && sig == null && prop == null) {
478 1 : Report.error (m.source_reference, "could not find signal or property for handler `%s'", handler_name);
479 1 : return;
480 : }
481 :
482 4 : push_context (class_init_context);
483 :
484 7 : if (sig != null) {
485 3 : sig.check (context);
486 3 : var method_type = new MethodType (m);
487 3 : var signal_type = new SignalType (sig);
488 3 : var delegate_type = signal_type.get_handler_type ();
489 5 : if (!method_type.compatible (delegate_type)) {
490 1 : 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 2 : var wrapper = generate_delegate_wrapper (m, signal_type.get_handler_type (), m);
493 :
494 2 : var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full"));
495 2 : call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
496 2 : call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name)));
497 2 : call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (wrapper)));
498 2 : ccode.add_expression (call);
499 : }
500 : }
501 5 : if (prop != null || callback != null) {
502 1 : if (prop != null) {
503 1 : prop.check (context);
504 : }
505 : //TODO Perform signature check
506 1 : var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full"));
507 1 : call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)"));
508 1 : call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name)));
509 1 : call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (get_ccode_name (m))));
510 1 : ccode.add_expression (call);
511 : }
512 :
513 4 : pop_context ();
514 : }
515 :
516 :
517 787 : public override void end_instance_init (Class cl) {
518 779 : if (cl == null || cl.error || !is_gtk_template (cl)) {
519 771 : return;
520 : }
521 :
522 8 : foreach (var req in current_required_app_classes) {
523 : /* ensure custom application widgets are initialized */
524 0 : var call = new CCodeFunctionCall (new CCodeIdentifier ("g_type_ensure"));
525 0 : call.add_argument (get_type_id_expression (SemanticAnalyzer.get_data_type_for_symbol (req)));
526 0 : ccode.add_expression (call);
527 : }
528 :
529 8 : var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_init_template"));
530 8 : call.add_argument (new CCodeIdentifier ("GTK_WIDGET (self)"));
531 8 : ccode.add_expression (call);
532 : }
533 : }
534 :
|