commit 34b02c88da37fe337bf6b73de3bad9900a7df35c
parent adba375c015544a4cdbc48833aeb63c792e04054
Author: Dan Callaghan <djc@djc.id.au>
Date: Sat, 20 Nov 2010 18:10:48 +1000
separated image property model from editing view; working implementation of revert
Diffstat:
5 files changed, 213 insertions(+), 119 deletions(-)
diff --git a/src/ImageMetadata.vala b/src/ImageMetadata.vala
@@ -6,16 +6,17 @@
namespace Xmpedit {
-public interface PropertyEditor : Gtk.Widget {
+public interface ImageProperty : Object {
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 signal void committed();
+ public abstract string name { get; }
+ public abstract RDF.Graph graph { get; construct; }
+ public abstract RDF.URIRef subject { get; construct; }
+
+ public signal void changed();
/**
* Examine the graph and return a one-line summary of the value found
@@ -23,72 +24,49 @@ public interface PropertyEditor : Gtk.Widget {
*/
protected abstract string value_summary();
- public string prop_display_name() {
- return prop_name.substring(0, 1).up() + prop_name.substring(1);
+ public string display_name() {
+ return name.substring(0, 1).up() + name.substring(1);
}
public string list_markup() {
- return @"<b>$(prop_display_name())</b>\n$(value_summary())";
+ return @"<b>$(display_name())</b>\n$(value_summary())";
}
}
-private class Description : Gtk.Table, PropertyEditor {
+public class Description : Object, ImageProperty {
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; }
+ public string name { get { return "description"; } }
+ public RDF.Graph graph { get; construct; }
+ public RDF.URIRef subject { get; construct; }
- private Gtk.ScrolledWindow text_scrolled = new Gtk.ScrolledWindow(null, null);
- private Gtk.TextView text_view = new Gtk.TextView();
- private Gtk.Entry lang_entry = new Gtk.Entry(); // XXX make a combo
+ private string _value;
+ private string _lang;
- construct {
- n_rows = 2;
- n_columns = 2;
- homogeneous = false;
-
- var label = new Gtk.Label(prop_display_name());
- label.xalign = 0;
- label.mnemonic_widget = text_view;
- attach(label,
- 0, 1, 0, 1,
- Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, 0,
- 0, 0);
- set_row_spacing(0, 4);
-
- var lang_hbox = new Gtk.HBox(/* homogeneous */ false, /* spacing */ 4);
- lang_entry.width_chars = 8;
- var lang_label = new Gtk.Label("Language:");
- lang_label.xalign = 1;
- lang_label.mnemonic_widget = lang_entry;
- lang_hbox.add(lang_label);
- lang_hbox.add(lang_entry);
- attach(lang_hbox,
- 1, 2, 0, 1,
- 0, 0,
- 0, 0);
- set_col_spacing(0, 10);
-
- text_view.wrap_mode = Gtk.WrapMode.WORD;
-
- text_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
- text_scrolled.shadow_type = Gtk.ShadowType.ETCHED_IN;
- text_scrolled.add(text_view);
- attach(text_scrolled,
- 0, 2, 1, 2,
- Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
- 0, 0);
+ public string value {
+ get { return _value; }
+ set { _value = value; update(); }
+ }
+ public string lang {
+ get { return _lang; }
+ set { _lang = value; update(); }
+ }
- show.connect(load);
- hide.connect(commit);
+ construct {
+ var literal = find_literal();
+ if (literal == null) {
+ _value = "";
+ _lang = "";
+ } else {
+ _value = literal.lexical_value;
+ _lang = (literal.lang != null ? literal.lang : "");
+ }
}
protected string value_summary() {
- var literal = find_literal();
- return (literal != null ? literal.lexical_value : "<i>not set</i>");
+ return _value;
}
private RDF.PlainLiteral? find_literal() {
@@ -102,42 +80,29 @@ private class Description : Gtk.Table, PropertyEditor {
new RDF.URIRef(RDF.RDF_NS + "Alt"))) {
description = select_alternative(node, graph);
if (description == null) {
- warning("found rdf:Alt with no alternatives for %s", prop_name);
+ warning("found rdf:Alt with no alternatives for %s", name);
return null;
}
}
}
if (!(description is RDF.PlainLiteral)) {
- warning("found non-literal node for %s", prop_name);
+ warning("found non-literal node for %s", name);
return null;
}
return (RDF.PlainLiteral) description;
}
- private void load() {
- var literal = find_literal();
- if (literal == null) {
- text_view.buffer.text = "";
- lang_entry.text = "";
- } else {
- text_view.buffer.text = literal.lexical_value;
- lang_entry.text = (literal.lang != null ? literal.lang : "");
- }
- }
-
- private void commit() {
+ private void update() {
graph.remove_matching_statements(subject, DC_DESCRIPTION, null);
- string value = text_view.buffer.text;
- string lang = lang_entry.text;
- if (value.length > 0) {
+ if (_value.length > 0) {
RDF.PlainLiteral object;
- if (lang.length > 0)
- object = new RDF.PlainLiteral.with_lang(value, lang);
+ if (_lang.length > 0)
+ object = new RDF.PlainLiteral.with_lang(_value, _lang);
else
- object = new RDF.PlainLiteral(value);
+ object = new RDF.PlainLiteral(_value);
graph.insert(new RDF.Statement(subject, DC_DESCRIPTION, object));
}
- committed();
+ changed();
}
}
@@ -167,25 +132,23 @@ private string get_preferred_lang() {
public class ImageMetadata : Object, Gtk.TreeModel {
public string path { get; construct; }
- public Gee.List<PropertyEditor> properties { get; construct; }
- public bool dirty = false;
+ public Gee.List<ImageProperty> properties { get; construct; }
+ public bool dirty { get; set; }
private Exiv2.Image image;
private size_t xmp_packet_size;
private RDF.Graph graph;
private RDF.URIRef subject;
- public signal void updated();
-
// TreeModel stuff
private static int last_stamp = 1;
private int stamp;
public ImageMetadata(string path) {
- Object(path: path);
+ Object(path: path, dirty: false);
}
construct {
- properties = new Gee.LinkedList<PropertyEditor>();
+ properties = new Gee.LinkedList<ImageProperty>();
lock (last_stamp) {
stamp = last_stamp ++;
}
@@ -195,6 +158,15 @@ public class ImageMetadata : Object, Gtk.TreeModel {
return_if_fail(image == null); // only call this once
image = new Exiv2.Image.from_path(path);
image.read_metadata();
+ read_xmp();
+ }
+
+ public void revert() {
+ return_if_fail(image != null);
+ read_xmp();
+ }
+
+ private void read_xmp() {
unowned string xmp = image.xmp_packet;
xmp_packet_size = xmp.length;
#if DEBUG
@@ -210,21 +182,11 @@ public class ImageMetadata : Object, Gtk.TreeModel {
stderr.puts(@"$s\n");
#endif
subject = new RDF.URIRef(base_uri);
- foreach (var type in PropertyEditor.all_types()) {
- var pe = (PropertyEditor) Object.new(type);
- pe.graph = graph;
- pe.subject = subject;
- pe.committed.connect(() => {
- dirty = true;
- updated();
- });
- properties.add(pe);
+ clear_properties();
+ foreach (var type in ImageProperty.all_types()) {
+ add_property(type);
}
- //foreach (var tag in exiv_metadata.get_xmp_tags()) {
- // properties.add(new PropertyEditor(tag, exiv_metadata.get_xmp_tag_string(tag)));
- //}
dirty = false;
- updated();
}
public void save() {
@@ -256,14 +218,39 @@ public class ImageMetadata : Object, Gtk.TreeModel {
image.xmp_packet = xml.str;
image.write_metadata();
dirty = false;
- updated();
+ }
+
+ private void clear_properties() {
+ for (var i = properties.size - 1; i >= 0; i --) {
+ properties.remove_at(i);
+ row_deleted(path_for_index(i));
+ }
+ }
+
+ private void add_property(Type type) {
+ var index = properties.size;
+ var p = (ImageProperty) Object.new(type, graph: graph, subject: subject);
+ properties.add(p);
+ row_inserted(path_for_index(index), iter_for_index(index));
+ p.changed.connect(() => {
+ dirty = true;
+ row_changed(path_for_index(index), iter_for_index(index));
+ });
}
/****** TREEMODEL IMPLEMENTATION STUFF **********/
+ private Gtk.TreePath path_for_index(int index) {
+ return new Gtk.TreePath.from_indices(index);
+ }
+
+ private Gtk.TreeIter iter_for_index(int index) {
+ return { stamp, (void*) properties[index], null, null };
+ }
+
public Type get_column_type(int column) {
return_val_if_fail(column == 0, 0);
- return typeof(PropertyEditor);
+ return typeof(ImageProperty);
}
public int get_n_columns() {
@@ -278,26 +265,26 @@ public class ImageMetadata : Object, Gtk.TreeModel {
if (path.get_depth() > 1) return false;
var index = path.get_indices()[0];
if (index > properties.size - 1) return false;
- iter = { stamp, (void*) properties[index], null, null };
+ iter = iter_for_index(index);
return true;
}
public Gtk.TreePath get_path(Gtk.TreeIter iter) {
return_val_if_fail(iter.stamp == stamp, null);
- var pe = (PropertyEditor) iter.user_data;
- return new Gtk.TreePath.from_indices(properties.index_of(pe));
+ var p = (ImageProperty) iter.user_data;
+ return path_for_index(properties.index_of(p));
}
public void get_value(Gtk.TreeIter iter, int column, out Value value) {
return_if_fail(iter.stamp == stamp);
return_if_fail(column == 0);
- value = Value(typeof(PropertyEditor));
- value.set_object((PropertyEditor) iter.user_data);
+ value = Value(typeof(ImageProperty));
+ value.set_object((ImageProperty) iter.user_data);
}
public bool iter_children(out Gtk.TreeIter iter, Gtk.TreeIter? parent) {
- if (parent == null) {
- iter = { stamp, (void*) properties[0], null, null };
+ if (parent == null && !properties.is_empty) {
+ iter = iter_for_index(0);
return true;
}
return false;
@@ -315,7 +302,7 @@ public class ImageMetadata : Object, Gtk.TreeModel {
public bool iter_next(ref Gtk.TreeIter iter) {
return_val_if_fail(iter.stamp == stamp, false);
- var index = properties.index_of((PropertyEditor) iter.user_data);
+ var index = properties.index_of((ImageProperty) iter.user_data);
if (index < properties.size - 1) {
iter.user_data = (void*) properties[index + 1];
return true;
diff --git a/src/MainWindow.vala b/src/MainWindow.vala
@@ -53,7 +53,8 @@ public class MainWindow : Gtk.Window {
tree_view_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
var revert_button = new Gtk.Button.from_stock(Gtk.Stock.REVERT_TO_SAVED);
revert_button.clicked.connect(() => {
- critical("IMPLEMENTME");
+ image_metadata.revert();
+ tree_view.set_cursor(new Gtk.TreePath.first(), null, false);
});
var save_button = new Gtk.Button.from_stock(Gtk.Stock.SAVE);
save_button.clicked.connect(() => {
@@ -61,9 +62,11 @@ public class MainWindow : Gtk.Window {
});
revert_button.sensitive = false;
save_button.sensitive = false;
- image_metadata.updated.connect(() => {
- revert_button.sensitive = image_metadata.dirty;
- save_button.sensitive = image_metadata.dirty;
+ image_metadata.notify.connect((p) => {
+ if (p.name == "dirty") {
+ revert_button.sensitive = image_metadata.dirty;
+ save_button.sensitive = image_metadata.dirty;
+ }
});
var left_button_box = new Gtk.HButtonBox();
left_button_box.spacing = 5;
diff --git a/src/MetadataTreeView.vala b/src/MetadataTreeView.vala
@@ -8,13 +8,13 @@ namespace Xmpedit {
private class PropertySummaryCellRenderer : Gtk.CellRendererText {
- private PropertyEditor _property_editor;
- public PropertyEditor property_editor {
+ private ImageProperty _image_property;
+ public ImageProperty image_property {
get {
- return _property_editor;
+ return _image_property;
}
set {
- _property_editor = value;
+ _image_property = value;
markup = value.list_markup();
}
}
@@ -34,7 +34,7 @@ public class MetadataTreeView : Gtk.TreeView {
column.fixed_width = 300;
var cell_renderer = new PropertySummaryCellRenderer();
column.pack_start(cell_renderer, /* expand */ true);
- column.add_attribute(cell_renderer, "property_editor", 0);
+ column.add_attribute(cell_renderer, "image_property", 0);
append_column(column);
get_selection().set_mode(Gtk.SelectionMode.BROWSE);
}
diff --git a/src/PropertyDetailView.vala b/src/PropertyDetailView.vala
@@ -6,6 +6,67 @@
namespace Xmpedit {
+private class DescriptionView : Gtk.Table {
+
+ public Description description { get; construct; }
+
+ public DescriptionView(Description description) {
+ Object(description: description);
+ }
+
+ construct {
+ n_rows = 2;
+ n_columns = 2;
+ homogeneous = false;
+
+ var text_view = new Gtk.TextView();
+ text_view.wrap_mode = Gtk.WrapMode.WORD;
+
+ var label = new Gtk.Label(description.display_name());
+ label.xalign = 0;
+ label.mnemonic_widget = text_view;
+ attach(label,
+ 0, 1, 0, 1,
+ Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, 0,
+ 0, 0);
+ set_row_spacing(0, 4);
+
+ var lang_hbox = new Gtk.HBox(/* homogeneous */ false, /* spacing */ 4);
+ var lang_entry = new Gtk.Entry(); // XXX make a combo
+ lang_entry.width_chars = 8;
+ var lang_label = new Gtk.Label("Language:");
+ lang_label.xalign = 1;
+ lang_label.mnemonic_widget = lang_entry;
+ lang_hbox.add(lang_label);
+ lang_hbox.add(lang_entry);
+ attach(lang_hbox,
+ 1, 2, 0, 1,
+ 0, 0,
+ 0, 0);
+ set_col_spacing(0, 10);
+
+ var text_scrolled = new Gtk.ScrolledWindow(null, null);
+ text_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+ text_scrolled.shadow_type = Gtk.ShadowType.ETCHED_IN;
+ text_scrolled.add(text_view);
+ attach(text_scrolled,
+ 0, 2, 1, 2,
+ Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND, Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
+ 0, 0);
+
+
+ text_view.buffer.text = description.value;
+ lang_entry.text = description.lang;
+ text_view.buffer.changed.connect(() => {
+ description.value = text_view.buffer.text;
+ });
+ lang_entry.changed.connect(() => {
+ description.lang = lang_entry.text;
+ });
+ }
+
+}
+
public class PropertyDetailView : Gtk.Alignment {
public MetadataTreeView tree_view { get; construct; }
@@ -21,13 +82,14 @@ public class PropertyDetailView : Gtk.Alignment {
tree_view.get_selection().get_selected(null, out iter);
Value value;
tree_view.model.get_value(iter, 0, out value);
- PropertyEditor pe = (PropertyEditor) value.get_object();
+ var p = (ImageProperty) value.get_object();
if (child != null) {
- child.hide();
remove(child);
}
- pe.show();
- add(pe);
+ // XXX
+ var d = new DescriptionView((Description) p);
+ add(d);
+ d.show_all();
});
}
diff --git a/test/guitest.py b/test/guitest.py
@@ -59,6 +59,7 @@ class XmpeditTestCase(unittest.TestCase):
def close_window(self):
window = self.get_window()
+ assert window is not None, "Where'd the window go?"
WM_PROTOCOLS = window.display.get_atom('WM_PROTOCOLS')
WM_DELETE_WINDOW = window.display.get_atom('WM_DELETE_WINDOW')
assert WM_DELETE_WINDOW in window.get_wm_protocols()
@@ -93,7 +94,6 @@ class Test(XmpeditTestCase):
entry.text = 'new description'
lang = window.child(roleName='ROLE_TEXT', label='Language:')
lang.text = 'en'
- pe.grabFocus() # XXX DELETEME
self.close_window()
alert = xmpedit.child(roleName='alert')
self.assertEquals(alert.child(roleName='label').name,
@@ -102,6 +102,48 @@ class Test(XmpeditTestCase):
alert.button('Close without saving').doAction('click')
self.assert_stopped()
self.assert_image_unmodified('24-06-06_1449.jpg')
+
+ def test_edit_then_revert(self):
+ xmpedit = dogtail.tree.Root().application('xmpedit')
+ window = xmpedit.child(roleName='frame')
+ pe, = [child for child in window.child('Image properties').children
+ if child.name.splitlines()[0] == 'Description'] # ugh
+ pe.select()
+ entry = window.child(roleName='ROLE_TEXT', label='Description')
+ entry.grabFocus()
+ entry.text = 'new description'
+ lang = window.child(roleName='ROLE_TEXT', label='Language:')
+ lang.text = 'en'
+ assert window.button('Revert').sensitive
+ window.button('Revert').doAction('click')
+ entry = window.child(roleName='ROLE_TEXT', label='Description')
+ self.assertEquals(entry.text, 'Edward Scissorhands stencil graffiti '
+ 'on the wall of John Hines building.')
+ lang = window.child(roleName='ROLE_TEXT', label='Language:')
+ self.assertEquals(lang.text, 'x-default')
+ self.close_window()
+ self.assert_stopped()
+ self.assert_image_unmodified('24-06-06_1449.jpg')
+
+ def test_update_description(self):
+ xmpedit = dogtail.tree.Root().application('xmpedit')
+ window = xmpedit.child(roleName='frame')
+ pe, = [child for child in window.child('Image properties').children
+ if child.name.splitlines()[0] == 'Description'] # ugh
+ pe.select()
+ entry = window.child(roleName='ROLE_TEXT', label='Description')
+ entry.grabFocus()
+ entry.text = 'new description'
+ lang = window.child(roleName='ROLE_TEXT', label='Language:')
+ lang.text = 'en'
+ assert window.button('Save').sensitive
+ window.button('Save').doAction('click')
+ self.close_window()
+ self.assert_stopped()
+ xmp = extract_xmp(self.tempfile.name)
+ self.assertEquals(len(xmp), 2675)
+ self.assertEquals(extract_xmp(self.tempfile.name),
+ u'''<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmpedit 0.0-dev"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about=""><Iptc4xmlCore:Location xmlns:Iptc4xmlCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/">UQ St Lucia</Iptc4xmlCore:Location><dc:description xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en">new description</dc:description></rdf:Description></rdf:RDF></x:xmpmeta>''' + ' ' * 2179 + '''<?xpacket end="w"?>''')
if __name__ == '__main__':
unittest.main()