3 * Copyright (C) 2013 Jürg Billeter
4 * Copyright (C) 2013-2014 Luca Bruno
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.
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.
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
21 * Luca Bruno <lucabru@src.gnome.org>
25 public class Vala.GtkModule : GSignalModule {
27 class InvalidClass : Class {
28 public InvalidClass (string name) {
29 base (name, null, null);
32 public override bool check (CodeContext context) {
37 class InvalidProperty : Property {
38 public InvalidProperty (string name) {
39 base (name, null, null, null);
42 public override bool check (CodeContext context) {
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>();
64 /* Stack of occuring object elements in the template */
65 List<Class> current_object_stack = new ArrayList<Class> ();
66 Class? current_object;
68 void push_object (Class cl) {
69 current_object_stack.add (current_object);
74 current_object = current_object_stack.remove_at (current_object_stack.size - 1);
77 /* Stack of occuring property elements in the template */
78 List<Property> current_property_stack = new ArrayList<Property> ();
79 Property? current_property;
81 void push_property (Property prop) {
82 current_property_stack.add (current_property);
83 current_property = prop;
86 void pop_property () {
87 current_property = current_property_stack.remove_at (current_property_stack.size - 1);
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) {
95 type_id_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
96 recurse_type_id_to_vala_map (context.root);
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);
105 classes = ((Namespace) sym).get_classes ();
106 } else if (sym is ObjectTypeSymbol) {
107 classes = ((ObjectTypeSymbol) sym).get_classes ();
111 foreach (var cl in classes) {
112 if (!cl.is_compact) {
113 var type_id = get_ccode_type_id (cl);
117 var i = type_id.index_of_char ('(');
119 type_id = type_id.substring (0, i - 1).strip ();
121 type_id = type_id.strip ();
123 type_id_to_vala_map.set (type_id, cl);
125 recurse_type_id_to_vala_map (cl);
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) {
134 cclass_to_vala_map = new HashMap<string, Class>(str_hash, str_equal);
135 recurse_cclass_to_vala_map (context.root);
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);
144 classes = ((Namespace) sym).get_classes ();
145 } else if (sym is ObjectTypeSymbol) {
146 classes = ((ObjectTypeSymbol) sym).get_classes ();
150 foreach (var cl in classes) {
151 if (!cl.is_compact) {
152 cclass_to_vala_map.set (get_ccode_name (cl), cl);
154 recurse_cclass_to_vala_map (cl);
158 private void ensure_gresource_to_file_map () {
159 // map gresource paths to real file names
160 if (gresource_to_file_map != null) {
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);
170 MarkupReader reader = new MarkupReader (gresource);
173 string prefix = null;
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");
183 } else if (state == 1 && current_token == MarkupTokenType.TEXT) {
184 var name = reader.content;
185 var filename = context.get_gresource_path (gresource, name);
187 gresource_to_file_map.set (Path.build_filename (prefix, alias), filename);
189 gresource_to_file_map.set (Path.build_filename (prefix, name), filename);
192 current_token = reader.read_token (null, null);
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();
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)) {
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);
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> ();
218 MarkupReader reader = new MarkupReader (ui_file);
219 string? current_handler = null;
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;
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);
233 } else if (current_name == "template") {
234 template_tag_found = true;
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);
244 current_class = cclass_to_vala_map.get (class_name);
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);
251 Report.warning (node.source_reference, "Unknown object `%s' in ui file `%s'", class_name, ui_file);
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);
261 push_object (current_class);
263 } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "object" || current_name == "template")) {
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");
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);
275 Report.error (node.source_reference, "Invalid signal without name and handler in ui file `%s'", ui_file);
277 current_token = reader.read_token (null, null);
280 var sep_idx = signal_name.index_of ("::");
282 // detailed signal, we don't care about the detail
283 signal_name = signal_name.substring (0, sep_idx);
286 var sig = SemanticAnalyzer.symbol_lookup_inherited (current_object, signal_name.replace ("-", "_")) as Signal;
288 current_handler_to_signal_map.set (handler_name, sig);
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);
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);
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);
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);
311 current_token = reader.read_token (null, null);
314 } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "property" || current_name == "binding")) {
316 } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "closure") {
317 var handler_name = reader.get_attribute ("function");
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);
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);
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;
338 current_token = reader.read_token (null, null);
341 if (!template_tag_found) {
342 Report.error (node.source_reference, "ui resource `%s' does not describe a valid composite template", ui_resource);
346 private bool is_gtk_template (Class cl) {
347 var attr = cl.get_attribute ("GtkTemplate");
349 if (gtk_widget_type == null || !cl.is_subtype_of (gtk_widget_type)) {
351 Report.error (attr.source_reference, "subclassing Gtk.Widget is required for using Gtk templates");
361 public override void generate_class_init (Class cl) {
362 base.generate_class_init (cl);
364 if (cl.error || !is_gtk_template (cl)) {
368 /* Gtk builder widget template */
369 var ui = cl.get_attribute_string ("GtkTemplate", "ui");
371 Report.error (cl.source_reference, "empty ui resource declaration for Gtk widget template");
376 process_current_ui_resource (ui, cl);
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);
383 current_required_app_classes.clear ();
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");
391 base.visit_property (prop);
394 public override void visit_field (Field f) {
395 base.visit_field (f);
397 var cl = current_class;
398 if (cl == null || cl.error) {
402 if (f.binding != MemberBinding.INSTANCE || f.get_attribute ("GtkChild") == null) {
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");
413 push_context (class_init_context);
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);
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());
430 var internal_child = f.get_attribute_bool ("GtkChild", "internal");
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);
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;
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);
455 if (!field_class.external && !field_class.external_package) {
456 current_required_app_classes.add (field_class);
460 public override void visit_method (Method m) {
461 base.visit_method (m);
463 var cl = current_class;
464 if (cl == null || cl.error || !is_gtk_template (cl)) {
468 if (m.get_attribute ("GtkCallback") == null) {
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);
482 push_context (class_init_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));
492 var wrapper = generate_delegate_wrapper (m, signal_type.get_handler_type (), m);
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);
501 if (prop != null || callback != null) {
503 prop.check (context);
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);
517 public override void end_instance_init (Class cl) {
518 if (cl == null || cl.error || !is_gtk_template (cl)) {
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);
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);