/* valagtkmodule.vala * * Copyright (C) 2013 Jürg Billeter * Copyright (C) 2013-2014 Luca Bruno * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Author: * Luca Bruno */ public class Vala.GtkModule : GSignalModule { class InvalidClass : Class { public InvalidClass (string name) { base (name, null, null); error = true; } public override bool check (CodeContext context) { return false; } } class InvalidProperty : Property { public InvalidProperty (string name) { base (name, null, null, null); error = true; } public override bool check (CodeContext context) { return false; } } /* C type-func name to Vala class mapping */ private HashMap type_id_to_vala_map = null; /* C class name to Vala class mapping */ private HashMap cclass_to_vala_map = null; /* GResource name to real file name mapping */ private HashMap gresource_to_file_map = null; /* GtkBuilder xml handler set */ private HashMap handler_map = new HashMap(str_hash, str_equal); /* GtkBuilder xml handler to Vala property mapping */ private HashMap current_handler_to_property_map = new HashMap(str_hash, str_equal); /* GtkBuilder xml handler to Vala signal mapping */ private HashMap current_handler_to_signal_map = new HashMap(str_hash, str_equal); /* GtkBuilder xml child to Vala class mapping */ private HashMap current_child_to_class_map = new HashMap(str_hash, str_equal); /* Required custom application-specific gtype classes to be ref'd before initializing the template */ private List current_required_app_classes = new ArrayList(); /* Stack of occuring object elements in the template */ List current_object_stack = new ArrayList (); Class? current_object; void push_object (Class cl) { current_object_stack.add (current_object); current_object = cl; } void pop_object () { current_object = current_object_stack.remove_at (current_object_stack.size - 1); } /* Stack of occuring property elements in the template */ List current_property_stack = new ArrayList (); Property? current_property; void push_property (Property prop) { current_property_stack.add (current_property); current_property = prop; } void pop_property () { current_property = current_property_stack.remove_at (current_property_stack.size - 1); } private void ensure_type_id_to_vala_map () { // map C type-func name of gtypeinstance classes to Vala classes if (type_id_to_vala_map != null) { return; } type_id_to_vala_map = new HashMap(str_hash, str_equal); recurse_type_id_to_vala_map (context.root); } private void recurse_type_id_to_vala_map (Symbol sym) { unowned List classes; if (sym is Namespace) { foreach (var inner in ((Namespace) sym).get_namespaces()) { recurse_type_id_to_vala_map (inner); } classes = ((Namespace) sym).get_classes (); } else if (sym is ObjectTypeSymbol) { classes = ((ObjectTypeSymbol) sym).get_classes (); } else { return; } foreach (var cl in classes) { if (!cl.is_compact) { var type_id = get_ccode_type_id (cl); if (type_id == null) continue; var i = type_id.index_of_char ('('); if (i > 0) { type_id = type_id.substring (0, i - 1).strip (); } else { type_id = type_id.strip (); } type_id_to_vala_map.set (type_id, cl); } recurse_type_id_to_vala_map (cl); } } private void ensure_cclass_to_vala_map () { // map C name of gtypeinstance classes to Vala classes if (cclass_to_vala_map != null) { return; } cclass_to_vala_map = new HashMap(str_hash, str_equal); recurse_cclass_to_vala_map (context.root); } private void recurse_cclass_to_vala_map (Symbol sym) { unowned List classes; if (sym is Namespace) { foreach (var inner in ((Namespace) sym).get_namespaces()) { recurse_cclass_to_vala_map (inner); } classes = ((Namespace) sym).get_classes (); } else if (sym is ObjectTypeSymbol) { classes = ((ObjectTypeSymbol) sym).get_classes (); } else { return; } foreach (var cl in classes) { if (!cl.is_compact) { cclass_to_vala_map.set (get_ccode_name (cl), cl); } recurse_cclass_to_vala_map (cl); } } private void ensure_gresource_to_file_map () { // map gresource paths to real file names if (gresource_to_file_map != null) { return; } gresource_to_file_map = new HashMap(str_hash, str_equal); foreach (var gresource in context.gresources) { if (!FileUtils.test (gresource, FileTest.EXISTS)) { Report.error (null, "GResources file `%s' does not exist", gresource); continue; } MarkupReader reader = new MarkupReader (gresource); int state = 0; string prefix = null; string alias = null; MarkupTokenType current_token = reader.read_token (null, null); while (current_token != MarkupTokenType.EOF) { if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "gresource") { prefix = reader.get_attribute ("prefix"); } else if (current_token == MarkupTokenType.START_ELEMENT && reader.name == "file") { alias = reader.get_attribute ("alias"); state = 1; } else if (state == 1 && current_token == MarkupTokenType.TEXT) { var name = reader.content; var filename = context.get_gresource_path (gresource, name); if (alias != null) { gresource_to_file_map.set (Path.build_filename (prefix, alias), filename); } gresource_to_file_map.set (Path.build_filename (prefix, name), filename); state = 0; } current_token = reader.read_token (null, null); } } } private void process_current_ui_resource (string ui_resource, CodeNode node) { /* Scan a single gtkbuilder file for signal handlers in elements, and save an handler string -> Vala.Signal mapping for each of them */ ensure_type_id_to_vala_map (); ensure_cclass_to_vala_map(); ensure_gresource_to_file_map(); current_handler_to_signal_map = null; current_child_to_class_map = null; var ui_file = gresource_to_file_map.get (ui_resource); if (ui_file == null || !FileUtils.test (ui_file, FileTest.EXISTS)) { node.error = true; 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); return; } handler_map = new HashMap(str_hash, str_equal); current_handler_to_signal_map = new HashMap(str_hash, str_equal); current_child_to_class_map = new HashMap(str_hash, str_equal); current_object_stack = new ArrayList (); current_property_stack = new ArrayList (); MarkupReader reader = new MarkupReader (ui_file); string? current_handler = null; bool template_tag_found = false; MarkupTokenType current_token = reader.read_token (null, null); while (current_token != MarkupTokenType.EOF) { unowned string current_name = reader.name; if (current_token == MarkupTokenType.START_ELEMENT && (current_name == "object" || current_name == "template")) { Class? current_class = null; if (current_name == "object") { var type_id = reader.get_attribute ("type-func"); if (type_id != null) { current_class = type_id_to_vala_map.get (type_id); } } else if (current_name == "template") { template_tag_found = true; } if (current_class == null) { var class_name = reader.get_attribute ("class"); if (class_name == null) { Report.error (node.source_reference, "Invalid %s in ui file `%s'", current_name, ui_file); current_token = reader.read_token (null, null); continue; } current_class = cclass_to_vala_map.get (class_name); if (current_class == null) { push_object (new InvalidClass (class_name)); if (current_name == "template") { Report.error (node.source_reference, "Unknown template `%s' in ui file `%s'", class_name, ui_file); } else { Report.warning (node.source_reference, "Unknown object `%s' in ui file `%s'", class_name, ui_file); } } } if (current_class != null) { var child_name = reader.get_attribute ("id"); if (child_name != null) { current_child_to_class_map.set (child_name, current_class); } push_object (current_class); } } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "object" || current_name == "template")) { pop_object (); } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "signal") { var signal_name = reader.get_attribute ("name"); var handler_name = reader.get_attribute ("handler"); if (signal_name == null || handler_name == null) { if (signal_name != null) { Report.error (node.source_reference, "Invalid signal `%s' without handler in ui file `%s'", signal_name, ui_file); } else if (handler_name != null) { Report.error (node.source_reference, "Invalid signal without name in ui file `%s'", ui_file); } else { Report.error (node.source_reference, "Invalid signal without name and handler in ui file `%s'", ui_file); } current_token = reader.read_token (null, null); continue; } var sep_idx = signal_name.index_of ("::"); if (sep_idx >= 0) { // detailed signal, we don't care about the detail signal_name = signal_name.substring (0, sep_idx); } var sig = SemanticAnalyzer.symbol_lookup_inherited (current_object, signal_name.replace ("-", "_")) as Signal; if (sig != null) { current_handler_to_signal_map.set (handler_name, sig); } else { Report.error (node.source_reference, "Unknown signal `%s::%s' in ui file `%s'", current_object.get_full_name (), signal_name, ui_file); current_token = reader.read_token (null, null); continue; } } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && (current_name == "property" || current_name == "binding")) { var property_name = reader.get_attribute ("name"); if (property_name == null) { Report.error (node.source_reference, "Invalid %s without name in ui file `%s'", current_name, ui_file); current_token = reader.read_token (null, null); continue; } property_name = property_name.replace ("-", "_"); var property = SemanticAnalyzer.symbol_lookup_inherited (current_object, property_name) as Property; if (property != null) { push_property (property); } else { push_property (new InvalidProperty (property_name)); if (current_name == "binding") { Report.error (node.source_reference, "Unknown property `%s:%s' for binding in ui file `%s'", current_object.get_full_name (), property_name, ui_file); } current_token = reader.read_token (null, null); continue; } } else if (current_token == MarkupTokenType.END_ELEMENT && (current_name == "property" || current_name == "binding")) { pop_property (); } else if (current_object != null && current_token == MarkupTokenType.START_ELEMENT && current_name == "closure") { var handler_name = reader.get_attribute ("function"); if (current_property != null) { if (handler_name == null) { Report.error (node.source_reference, "Invalid %s without function in ui file `%s'", current_name, ui_file); current_token = reader.read_token (null, null); continue; } if (current_property is InvalidProperty) { 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); } //TODO Retrieve signature declaration? c-type to vala-type? current_handler_to_property_map.set (handler_name, current_property); current_handler = handler_name; } else if (current_handler != null) { // Track nested closure elements handler_map.set (handler_name, current_handler); current_handler = handler_name; } } current_token = reader.read_token (null, null); } if (!template_tag_found) { Report.error (node.source_reference, "ui resource `%s' does not describe a valid composite template", ui_resource); } } private bool is_gtk_template (Class cl) { var attr = cl.get_attribute ("GtkTemplate"); if (attr != null) { if (gtk_widget_type == null || !cl.is_subtype_of (gtk_widget_type)) { if (!cl.error) { Report.error (attr.source_reference, "subclassing Gtk.Widget is required for using Gtk templates"); cl.error = true; } return false; } return true; } return false; } public override void generate_class_init (Class cl) { base.generate_class_init (cl); if (cl.error || !is_gtk_template (cl)) { return; } /* Gtk builder widget template */ var ui = cl.get_attribute_string ("GtkTemplate", "ui"); if (ui == null) { Report.error (cl.source_reference, "empty ui resource declaration for Gtk widget template"); cl.error = true; return; } process_current_ui_resource (ui, cl); var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_set_template_from_resource")); call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)")); call.add_argument (new CCodeConstant ("\"%s\"".printf (ui))); ccode.add_expression (call); current_required_app_classes.clear (); } public override void visit_property (Property prop) { if (prop.get_attribute ("GtkChild") != null && prop.field == null) { Report.error (prop.source_reference, "[GtkChild] is only allowed on automatic properties"); } base.visit_property (prop); } public override void visit_field (Field f) { base.visit_field (f); var cl = current_class; if (cl == null || cl.error) { return; } if (f.binding != MemberBinding.INSTANCE || f.get_attribute ("GtkChild") == null) { return; } /* If the field has a [GtkChild] attribute but its class doesn'thave a [GtkTemplate] attribute, we throw an error */ if (!is_gtk_template (cl)) { Report.error (f.source_reference, "[GtkChild] is only allowed in classes with a [GtkTemplate] attribute"); return; } push_context (class_init_context); /* Map ui widget to a class field */ var gtk_name = f.get_attribute_string ("GtkChild", "name", f.name); var child_class = current_child_to_class_map.get (gtk_name); if (child_class == null) { Report.error (f.source_reference, "could not find child `%s'", gtk_name); return; } /* We allow Gtk child to have stricter type than class field */ unowned Class? field_class = f.variable_type.type_symbol as Class; if (field_class == null || !child_class.is_subtype_of (field_class)) { Report.error (f.source_reference, "cannot convert from Gtk child type `%s' to `%s'", child_class.get_full_name(), field_class.get_full_name()); return; } var internal_child = f.get_attribute_bool ("GtkChild", "internal"); CCodeExpression offset; if (f.is_private_symbol ()) { // new glib api, we add the private struct offset to get the final field offset out of the instance var private_field_offset = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET")); private_field_offset.add_argument (new CCodeIdentifier ("%sPrivate".printf (get_ccode_name (cl)))); private_field_offset.add_argument (new CCodeIdentifier (get_ccode_name (f))); offset = new CCodeBinaryExpression (CCodeBinaryOperator.PLUS, new CCodeIdentifier ("%s_private_offset".printf (get_ccode_name (cl))), private_field_offset); } else { var offset_call = new CCodeFunctionCall (new CCodeIdentifier ("G_STRUCT_OFFSET")); offset_call.add_argument (new CCodeIdentifier (get_ccode_name (cl))); offset_call.add_argument (new CCodeIdentifier (get_ccode_name (f))); offset = offset_call; } var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_child_full")); call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)")); call.add_argument (new CCodeConstant ("\"%s\"".printf (gtk_name))); call.add_argument (new CCodeConstant (internal_child ? "TRUE" : "FALSE")); call.add_argument (offset); ccode.add_expression (call); pop_context (); if (!field_class.external && !field_class.external_package) { current_required_app_classes.add (field_class); } } public override void visit_method (Method m) { base.visit_method (m); var cl = current_class; if (cl == null || cl.error || !is_gtk_template (cl)) { return; } if (m.get_attribute ("GtkCallback") == null) { return; } /* Handler name as defined in the gtkbuilder xml */ var handler_name = m.get_attribute_string ("GtkCallback", "name", m.name); var callback = handler_map.get (handler_name); var sig = current_handler_to_signal_map.get (handler_name); var prop = current_handler_to_property_map.get (handler_name); if (callback == null && sig == null && prop == null) { Report.error (m.source_reference, "could not find signal or property for handler `%s'", handler_name); return; } push_context (class_init_context); if (sig != null) { sig.check (context); var method_type = new MethodType (m); var signal_type = new SignalType (sig); var delegate_type = signal_type.get_handler_type (); if (!method_type.compatible (delegate_type)) { 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)); } else { var wrapper = generate_delegate_wrapper (m, signal_type.get_handler_type (), m); var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full")); call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)")); call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name))); call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (wrapper))); ccode.add_expression (call); } } if (prop != null || callback != null) { if (prop != null) { prop.check (context); } //TODO Perform signature check var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_class_bind_template_callback_full")); call.add_argument (new CCodeIdentifier ("GTK_WIDGET_CLASS (klass)")); call.add_argument (new CCodeConstant ("\"%s\"".printf (handler_name))); call.add_argument (new CCodeIdentifier ("G_CALLBACK(%s)".printf (get_ccode_name (m)))); ccode.add_expression (call); } pop_context (); } public override void end_instance_init (Class cl) { if (cl == null || cl.error || !is_gtk_template (cl)) { return; } foreach (var req in current_required_app_classes) { /* ensure custom application widgets are initialized */ var call = new CCodeFunctionCall (new CCodeIdentifier ("g_type_ensure")); call.add_argument (get_type_id_expression (SemanticAnalyzer.get_data_type_for_symbol (req))); ccode.add_expression (call); } var call = new CCodeFunctionCall (new CCodeIdentifier ("gtk_widget_init_template")); call.add_argument (new CCodeIdentifier ("GTK_WIDGET (self)")); ccode.add_expression (call); } }