xmpedit

GTK+ editor for XMP metadata embedded in images
git clone https://code.djc.id.au/git/xmpedit/
commit 4f3745e69661cbad3d2ea7c41c36c78009845e71
parent 333683dce2678f8ded9ed3dce87bc411a81f8d79
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sat, 21 Aug 2010 14:15:13 +1000

beginnings of UI for editing properties

Diffstat:
Msrc/ImageMetadata.vala | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/MainWindow.vala | 8+++++++-
Msrc/MetadataTreeView.vala | 6++++--
Asrc/PropertyDetailView.vala | 28++++++++++++++++++++++++++++
Msrc/RDF.vala | 38+++++++++++++++++++++++++++++++++++---
5 files changed, 170 insertions(+), 17 deletions(-)
diff --git a/src/ImageMetadata.vala b/src/ImageMetadata.vala
@@ -1,21 +1,97 @@
 
 namespace XmpEdit {
 
-public class PropertyEditor : Object {
+public interface PropertyEditor : Gtk.Widget {
 
-    public string prop { get; construct; }
-    public string value { get; construct; }
-    
-    public PropertyEditor(string prop, string value) {
-        Object(prop: prop, value: value);
+    public static Type[] all_types() {
+        return { typeof(Description) };
     }
+
+    public abstract string prop_name { get; }
+    public abstract RDF.Graph graph { get; set; }
+    public abstract RDF.URIRef subject { get; set; }
     
-    public string get_list_markup() {
-	    return @"<b>Unknown property ($prop)</b>\n$value";
+    public abstract bool exists_in_graph();
+    public abstract string value_summary();
+    public abstract void refresh();
+
+    public string list_markup() {
+        var display_name = prop_name.substring(0, 1).up() + prop_name.substring(1);
+	    return @"<b>$(display_name)</b>\n$(value_summary())";
 	}
 	
 }
 
+private class Description : Gtk.Table, PropertyEditor {
+
+    private static RDF.URIRef DC_DESCRIPTION = new RDF.URIRef("http://purl.org/dc/elements/1.1/description");
+    
+    public string prop_name { get { return "description"; } }
+    public RDF.Graph graph { get; set; }
+    public RDF.URIRef subject { get; set; }
+    private Gtk.TextView text_view = new Gtk.TextView();
+    
+    construct {
+        n_rows = 1;
+        n_columns = 1;
+        homogeneous = false;
+        attach(text_view,
+                0, 1, 0, 1,
+                Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
+                10, 10);
+    }
+
+    public bool exists_in_graph() {
+        return graph.find_objects(subject, DC_DESCRIPTION).size > 0;
+    }
+    
+    public string value_summary() {
+        var description = graph.find_object(subject, DC_DESCRIPTION);
+        if (description == null)
+            return "<i>(not set)</i>";
+        if (description is RDF.SubjectNode) {
+            var node = (RDF.SubjectNode) description;
+            if (graph.has_matching_statement(node,
+                    new RDF.URIRef(RDF.RDF_NS + "type"),
+                    new RDF.URIRef(RDF.RDF_NS + "Alt"))) {
+                description = select_alternative(node, graph);
+            }
+        }
+        if (!(description is RDF.PlainLiteral)) {
+            return "<i>(non-literal node)</i>";
+        }
+        var literal = (RDF.PlainLiteral) description;
+        return literal.lexical_value;
+    }
+    
+    public void refresh() {
+        text_view.buffer.text = value_summary();
+    }
+
+}
+
+/** Given an rdf:Alt node, returns the "best" alternative */
+private RDF.Node? select_alternative(RDF.SubjectNode alt_node, RDF.Graph graph) {
+    var preferred_lang = get_preferred_lang();
+    var alternatives = graph.find_objects(alt_node, new RDF.URIRef(RDF.RDF_NS + "li"));
+    if (alternatives.size == 0)
+        return null;
+    foreach (var alternative in alternatives) {
+        if (alternative is RDF.PlainLiteral) {
+            var literal = (RDF.PlainLiteral) alternative;
+            if (literal.lang == preferred_lang)
+                return literal;
+        }
+    }
+    return alternatives[0];
+}
+
+// XXX make this a user pref somehow?
+private string get_preferred_lang() {
+    var lang = Environment.get_variable("LANG");
+    return (lang != null ? lang.substring(0, 2) : "en");
+}
+
 public class ImageMetadata : Object {
 
     public string path { get; construct; }
@@ -37,12 +113,21 @@ public class ImageMetadata : Object {
         exiv_metadata.open_path(path);
         string xmp = exiv_metadata.get_xmp_packet();
         stdout.puts(xmp);
-        var g = new RDF.Graph.from_xml(xmp, File.new_for_path(path).get_uri());
+        var base_uri = File.new_for_path(path).get_uri();
+        var g = new RDF.Graph.from_xml(xmp, base_uri);
         foreach (var s in g.get_statements())
             stdout.puts(@"$s\n");
-        foreach (var tag in exiv_metadata.get_xmp_tags()) {
-            properties.add(new PropertyEditor(tag, exiv_metadata.get_xmp_tag_string(tag)));
+        var subject = new RDF.URIRef(base_uri);
+        foreach (var type in PropertyEditor.all_types()) {
+            var pe = (PropertyEditor) Object.new(type);
+            pe.graph = g;
+            pe.subject = subject;
+            if (pe.exists_in_graph())
+                properties.add(pe);
         }
+        //foreach (var tag in exiv_metadata.get_xmp_tags()) {
+        //    properties.add(new PropertyEditor(tag, exiv_metadata.get_xmp_tag_string(tag)));
+        //}
         updated();
     }
 
diff --git a/src/MainWindow.vala b/src/MainWindow.vala
@@ -8,7 +8,7 @@ public class MainWindow : Gtk.Window {
     private Gtk.Image image_preview;
     private Gtk.ScrolledWindow tree_view_scrolled;
     private MetadataTreeView tree_view;
-    private Gtk.ScrolledWindow detail_scrolled;
+    private PropertyDetailView detail_view;
     
     public MainWindow(string path) throws GLib.Error {
         Object(type: Gtk.WindowType.TOPLEVEL);
@@ -18,6 +18,7 @@ public class MainWindow : Gtk.Window {
         image_preview = new Gtk.Image.from_pixbuf(new Gdk.Pixbuf.from_file_at_scale(path, 320, 320, /* preserve aspect */ true));
         tree_view_scrolled = new Gtk.ScrolledWindow(null, null);
         tree_view = new MetadataTreeView.connected_to(image_metadata);
+        detail_view = new PropertyDetailView.connected_to(image_metadata, tree_view);
         
         title = File.new_for_path(path).get_basename();
         default_width = 640;
@@ -35,6 +36,11 @@ public class MainWindow : Gtk.Window {
                 0, 1, 0, 2,
                 Gtk.AttachOptions.FILL, Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
                 0, 0);
+                
+        table.attach(detail_view,
+                1, 2, 1, 2,
+                Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, Gtk.AttachOptions.FILL,
+                0, 0);
         
         add(table);
         show_all();
diff --git a/src/MetadataTreeView.vala b/src/MetadataTreeView.vala
@@ -4,7 +4,7 @@ namespace XmpEdit {
 public class MetadataTreeModel : Gtk.ListStore {
 
     public MetadataTreeModel() {
-        set_column_types({ typeof(string) });
+        set_column_types({ typeof(string), typeof(PropertyEditor) });
     }
     
     public void populate(ImageMetadata metadata) {
@@ -12,7 +12,8 @@ public class MetadataTreeModel : Gtk.ListStore {
         foreach (var property_editor in metadata.properties) {
             Gtk.TreeIter row;
             append(out row);
-            set_value(row, 0, property_editor.get_list_markup());
+            set_value(row, 0, property_editor.list_markup());
+            set_value(row, 1, property_editor);
         }
     }
 
@@ -37,6 +38,7 @@ public class MetadataTreeView : Gtk.TreeView {
         column.pack_start(cell_renderer, /* expand */ true);
         column.add_attribute(cell_renderer, "markup", 0);
         append_column(column);
+        get_selection().set_mode(Gtk.SelectionMode.BROWSE);
     }
 
 }
diff --git a/src/PropertyDetailView.vala b/src/PropertyDetailView.vala
@@ -0,0 +1,28 @@
+
+namespace XmpEdit {
+
+public class PropertyDetailView : Gtk.ScrolledWindow {
+
+    public MetadataTreeView tree_view { get; construct; }
+
+    public PropertyDetailView.connected_to(ImageMetadata image_metadata, MetadataTreeView tree_view) {
+        Object(tree_view: tree_view);
+    }
+
+    construct {
+        set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+        tree_view.cursor_changed.connect(() => {
+            Gtk.TreeIter iter;
+            tree_view.get_selection().get_selected(null, out iter);
+            Value value;
+            tree_view.model.get_value(iter, 1, out value);
+            PropertyEditor pe = (PropertyEditor) value.get_object();
+            pe.refresh();
+            //remove(child);
+            add_with_viewport(pe);
+        });
+    }
+
+}
+
+}
diff --git a/src/RDF.vala b/src/RDF.vala
@@ -249,11 +249,11 @@ public class Graph : Object {
         }
     }
     
-    public Gee.Collection<Statement> get_statements() {
-        return statements;
+    public Gee.List<Statement> get_statements() {
+        return statements.read_only_view;
     }
     
-    public Gee.Collection<Statement> find_matching_statements(
+    public Gee.List<Statement> find_matching_statements(
             SubjectNode? subject, URIRef? predicate, Node? object) {
         // XXX naive
         var result = new Gee.ArrayList<Statement>((EqualFunc) Statement.equal);
@@ -265,6 +265,38 @@ public class Graph : Object {
         }
         return result;
     }
+        
+    public bool has_matching_statement(
+            SubjectNode? subject, URIRef? predicate, Node? object) {
+        // XXX naive
+        foreach (var s in statements) {
+            if ((subject == null || s.subject.equals(subject)) &&
+                    (predicate == null || s.predicate.equals(predicate)) &&
+                    (object == null || s.object.equals(object)))
+                return true;
+        }
+        return false;
+    }
+    
+    public Gee.List<Node> find_objects(SubjectNode subject, URIRef predicate) {
+        // XXX naive
+        var result = new Gee.ArrayList<Node>();
+        foreach (var s in statements) {
+            if ((subject == null || s.subject.equals(subject)) &&
+                    (predicate == null || s.predicate.equals(predicate)))
+                result.add(s.object);
+        }
+        return result;
+    }
+    
+    public Node? find_object(SubjectNode subject, URIRef predicate) {
+        foreach (var s in statements) {
+            if ((subject == null || s.subject.equals(subject)) &&
+                    (predicate == null || s.predicate.equals(predicate)))
+                return s.object;
+        }
+        return null;
+    }
     
 }