commit 002f421a8da7ff536e8f5aa9c6f38db8c3a4ecc1
parent 1a61e2c49e69a7b49c64515a2978e4cfb42a3dca
Author: Dan Callaghan <djc@djc.id.au>
Date: Sun, 19 Sep 2010 19:39:24 +1000
explicit save button, with dirty tracking and close-without-saving warning dialog (TODO implement revert)
Diffstat:
3 files changed, 121 insertions(+), 14 deletions(-)
diff --git a/src/ImageMetadata.vala b/src/ImageMetadata.vala
@@ -15,6 +15,7 @@ public interface PropertyEditor : Gtk.Widget {
public abstract string prop_name { get; }
public abstract RDF.Graph graph { get; set; }
public abstract RDF.URIRef subject { get; set; }
+ public signal void committed();
/**
* Examine the graph and return a one-line summary of the value found
@@ -136,6 +137,7 @@ private class Description : Gtk.Table, PropertyEditor {
object = new RDF.PlainLiteral(value);
graph.insert(new RDF.Statement(subject, DC_DESCRIPTION, object));
}
+ committed();
}
}
@@ -166,6 +168,7 @@ public class ImageMetadata : Object, Gtk.TreeModel {
public string path { get; construct; }
public Gee.List<PropertyEditor> properties { get; construct; }
+ public bool dirty = false;
private Exiv2.Image image;
private size_t xmp_packet_size;
private RDF.Graph graph;
@@ -211,17 +214,22 @@ public class ImageMetadata : Object, Gtk.TreeModel {
var pe = (PropertyEditor) Object.new(type);
pe.graph = graph;
pe.subject = subject;
+ pe.committed.connect(() => {
+ dirty = true;
+ updated();
+ });
properties.add(pe);
}
//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() {
return_if_fail(image != null); // only call after successful loading
- // XXX shouldn't write if not dirty!
+ return_if_fail(dirty); // only call if dirty
#if DEBUG
stderr.puts("=== Final RDF graph:\n");
foreach (var s in graph.get_statements())
@@ -247,6 +255,8 @@ public class ImageMetadata : Object, Gtk.TreeModel {
#endif
image.xmp_packet = xml.str;
image.write_metadata();
+ dirty = false;
+ updated();
}
/****** TREEMODEL IMPLEMENTATION STUFF **********/
diff --git a/src/MainWindow.vala b/src/MainWindow.vala
@@ -6,21 +6,27 @@
namespace Xmpedit {
+private const string STOCK_CLOSE_WITHOUT_SAVING = "xmpedit-close-without-saving";
+
public class MainWindow : Gtk.Window {
+ private File file;
private ImageMetadata image_metadata;
private Gtk.Table table;
private Gtk.Image image_preview;
- private Gtk.ScrolledWindow tree_view_scrolled;
private MetadataTreeView tree_view;
private PropertyDetailView detail_view;
+ static construct {
+ add_stock();
+ }
+
public MainWindow(string path) throws GLib.Error {
Object(type: Gtk.WindowType.TOPLEVEL);
+ file = File.new_for_path(path);
image_metadata = new ImageMetadata(path);
image_metadata.load();
table = new Gtk.Table(/* rows */ 2, /* cols */ 2, /* homogeneous */ false);
- var file = File.new_for_path(path);
title = file.get_basename();
default_width = 640;
@@ -42,10 +48,33 @@ public class MainWindow : Gtk.Window {
tree_view = new MetadataTreeView.connected_to(image_metadata);
((Atk.Object) tree_view.get_accessible())
.set_name("Image properties");
- tree_view_scrolled = new Gtk.ScrolledWindow(null, null);
+ var tree_view_scrolled = new Gtk.ScrolledWindow(null, null);
tree_view_scrolled.add(tree_view);
tree_view_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
- table.attach(tree_view_scrolled,
+ var revert_button = new Gtk.Button.from_stock(Gtk.STOCK_REVERT_TO_SAVED);
+ revert_button.clicked.connect(() => {
+ critical("IMPLEMENTME");
+ });
+ var save_button = new Gtk.Button.from_stock(Gtk.STOCK_SAVE);
+ save_button.clicked.connect(() => {
+ image_metadata.save();
+ });
+ revert_button.sensitive = false;
+ save_button.sensitive = false;
+ image_metadata.updated.connect(() => {
+ revert_button.sensitive = image_metadata.dirty;
+ save_button.sensitive = image_metadata.dirty;
+ });
+ var left_button_box = new Gtk.HButtonBox();
+ left_button_box.spacing = 5;
+ left_button_box.layout_style = Gtk.ButtonBoxStyle.END;
+ left_button_box.add(revert_button);
+ left_button_box.add(save_button);
+ var left_vbox = new Gtk.VBox(/* homogeneous */ false, /* spacing */ 0);
+ left_vbox.pack_start(tree_view_scrolled, /* expand */ true, /* fill */ true);
+ left_vbox.pack_start(left_button_box, /* expand */ false, /* fill */ false,
+ /* padding */ 5);
+ table.attach(left_vbox,
0, 1, 0, 2,
Gtk.AttachOptions.FILL, Gtk.AttachOptions.FILL | Gtk.AttachOptions.EXPAND,
0, 0);
@@ -60,11 +89,57 @@ public class MainWindow : Gtk.Window {
add(table);
show_all();
- destroy.connect(() => {
- image_metadata.save();
+ delete_event.connect(() => {
+ if (image_metadata.dirty) {
+ var dialog = new Gtk.MessageDialog.with_markup(
+ /* parent */ this, Gtk.DialogFlags.MODAL,
+ Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE,
+ "<big><b>Your changes to image \"%s\" have not been saved.</b></big>\n\n" +
+ "Save changes before closing?",
+ file.get_basename());
+ dialog.add_button(STOCK_CLOSE_WITHOUT_SAVING, 1);
+ dialog.add_button(Gtk.STOCK_CANCEL, 2);
+ dialog.add_button(Gtk.STOCK_SAVE, 3);
+ dialog.set_default_response(3);
+ var response = dialog.run();
+ dialog.destroy();
+ switch (response) {
+ case 3: // save
+ image_metadata.save();
+ return false;
+ case 1: // close
+ return false;
+ case 2: // cancel
+ default:
+ return true;
+ }
+ }
+ return false;
});
+
+ add_stock();
}
-
+
+ private static Gtk.StockItem[] STOCK = {
+ Gtk.StockItem() {
+ stock_id = STOCK_CLOSE_WITHOUT_SAVING,
+ label = "Close _without saving",
+ modifier = 0,
+ keyval = 0,
+ translation_domain = null
+ }
+ };
+
+ /** Create custom stock entries used in the application. */
+ private static void add_stock() {
+ Gtk.stock_add_static(STOCK);
+
+ var icon_factory = new Gtk.IconFactory();
+ icon_factory.add(STOCK_CLOSE_WITHOUT_SAVING,
+ Gtk.IconFactory.lookup_default(Gtk.STOCK_CLOSE));
+ icon_factory.add_default();
+ }
+
}
}
diff --git a/test/guitest.py b/test/guitest.py
@@ -24,6 +24,7 @@ class XmpeditTestCase(unittest.TestCase):
self.tempfile = tempfile.NamedTemporaryFile()
self.tempfile.write(open(os.path.join('testdata', image_filename)).read())
self.tempfile.flush()
+ self.tempfile.seek(0)
self.popen = subprocess.Popen(
[os.path.join('target', 'xmpedit'), self.tempfile.name])
@@ -40,6 +41,10 @@ class XmpeditTestCase(unittest.TestCase):
return
time.sleep(1)
assert False, 'Process did not end'
+
+ def assert_image_unmodified(self, image_filename):
+ self.tempfile.seek(0)
+ assert self.tempfile.read() == open(os.path.join('testdata', image_filename)).read(), 'Image should be unmodified'
def get_window(self):
root = Xlib.display.Display().screen().root # XXX multiple screens?
@@ -65,16 +70,33 @@ class Test(XmpeditTestCase):
def tearDown(self):
self.stop()
- def test_roundtrip(self):
+ def test_do_nothing(self):
+ xmpedit = dogtail.tree.Root().application('xmpedit')
+ window = xmpedit.child(roleName='frame')
+ self.close_window()
+ self.assert_stopped()
+ self.assert_image_unmodified('24-06-06_1449.jpg')
+
+ def test_close_without_saving(self):
xmpedit = dogtail.tree.Root().application('xmpedit')
window = xmpedit.child(roleName='frame')
- time.sleep(0.5)
+ 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'
+ pe.grabFocus() # XXX DELETEME
self.close_window()
+ alert = xmpedit.child(roleName='alert')
+ self.assertEquals(alert.child(roleName='label').name,
+ 'Your changes to image "%s" have not been saved.\n\nSave changes before closing?'
+ % os.path.basename(self.tempfile.name))
+ alert.button('Close without saving').doAction('click')
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/"><rdf:Alt><rdf:li xml:lang="x-default">Edward Scissorhands stencil graffiti on the wall of John Hines building.</rdf:li></rdf:Alt></dc:description></rdf:Description></rdf:RDF></x:xmpmeta>''' + ' ' * 2079 + '''<?xpacket end="w"?>''')
+ self.assert_image_unmodified('24-06-06_1449.jpg')
if __name__ == '__main__':
unittest.main()