Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A very hacky undo redo implementation #531

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/FileFormat/AkiraFile.vala
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public class Akira.FileFormat.AkiraFile : Akira.FileFormat.ZipArchiveHandler {
save_images.begin ();
var content = new FileFormat.JsonContent (window);

content.save_content ();
content.inner_save_content ();
var json = content.finalize_content ();
write_content_to_file (content_file, json);

Expand Down
50 changes: 35 additions & 15 deletions src/FileFormat/JsonContent.vala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,20 @@ public class Akira.FileFormat.JsonContent : Object {
canvas = window.main_window.main_canvas.canvas;
}

public void save_content () {
public string finalize_content () {
builder.end_object ();

Json.Node root = builder.get_root ();
generator.set_root (root);

return generator.to_data (null);
}

public void inner_save_content() {
save_content(builder, canvas);
}

public static void save_content (Json.Builder builder, Akira.Lib.Canvas canvas) {
// Save the current version of Akira.
builder.set_member_name ("version");
builder.add_string_value (Constants.VERSION);
Expand All @@ -49,22 +62,22 @@ public class Akira.FileFormat.JsonContent : Object {
builder.set_member_name ("scale");
builder.add_double_value (canvas.get_scale ());
builder.set_member_name ("hadjustment");
builder.add_double_value (window.main_window.main_canvas.main_scroll.hadjustment.value);
builder.add_double_value (0);
builder.set_member_name ("vadjustment");
builder.add_double_value (window.main_window.main_canvas.main_scroll.vadjustment.value);
builder.add_double_value (0);

// Convert Artboards to JSON.
save_artboards ();
save_artboards (builder, canvas);

// Convert Items to JSON.
save_items ();
save_items (builder, canvas);
}

private void save_artboards () {
private static void save_artboards (Json.Builder builder, Akira.Lib.Canvas canvas) {
builder.set_member_name ("artboards");
builder.begin_array ();

foreach (var artboard in window.items_manager.artboards) {
foreach (var artboard in canvas.window.items_manager.artboards) {
var item = new JsonObject (artboard);
builder.begin_object ();
builder.set_member_name ("artboard");
Expand All @@ -75,11 +88,11 @@ public class Akira.FileFormat.JsonContent : Object {
builder.end_array ();
}

private void save_items () {
private static void save_items (Json.Builder builder, Akira.Lib.Canvas canvas) {
builder.set_member_name ("items");
builder.begin_array ();

foreach (var _item in window.items_manager.free_items) {
foreach (var _item in canvas.window.items_manager.free_items) {
var item = new JsonObject (_item);
builder.begin_object ();
builder.set_member_name ("item");
Expand All @@ -88,7 +101,7 @@ public class Akira.FileFormat.JsonContent : Object {
}

// Save all the items inside this Artboard.
foreach (var artboard in window.items_manager.artboards) {
foreach (var artboard in canvas.window.items_manager.artboards) {
foreach (var _item in artboard.items) {
var child_item = new JsonObject (_item);
builder.begin_object ();
Expand All @@ -101,12 +114,19 @@ public class Akira.FileFormat.JsonContent : Object {
builder.end_array ();
}

public string finalize_content () {
builder.end_object ();
public static string serialize_canvas (Akira.Lib.Canvas canvas) {
var inner_generator = new Json.Generator ();
inner_generator.pretty = true;
var inner_builder = new Json.Builder ();
inner_builder.begin_object ();

Json.Node root = builder.get_root ();
generator.set_root (root);
save_content(inner_builder, canvas);

return generator.to_data (null);
inner_builder.end_object ();

Json.Node root = inner_builder.get_root ();
inner_generator.set_root (root);

return inner_generator.to_data (null);
}
}
34 changes: 19 additions & 15 deletions src/FileFormat/JsonLoader.vala
Original file line number Diff line number Diff line change
Expand Up @@ -33,47 +33,51 @@ public class Akira.FileFormat.JsonLoader : Object {
construct {
load_content ();
}

public void load_content () {
inner_load_content(window.main_window.main_canvas.canvas, obj);

}

public static void inner_load_content (Akira.Lib.Canvas canvas, Json.Object json_object) {
// Se the canvas to simulate a click + holding state to avoid triggering
// redrawing methods connected to that state.
window.main_window.main_canvas.canvas.holding = true;
canvas.holding = true;

// Load saved Artboards.
if (obj.get_member ("artboards") != null) {
Json.Array artboards = obj.get_member ("artboards").get_array ();
if (json_object.get_member ("artboards") != null) {
Json.Array artboards = json_object.get_member ("artboards").get_array ();
var artboards_list = artboards.get_elements ();
artboards_list.reverse ();

foreach (unowned Json.Node node in artboards_list) {
load_item (node.get_object (), "artboard");
load_item (canvas, node.get_object (), "artboard");
}
}

// Load saved Items.
if (obj.get_member ("items") != null) {
Json.Array items = obj.get_member ("items").get_array ();
if (json_object.get_member ("items") != null) {
Json.Array items = json_object.get_member ("items").get_array ();
var items_list = items.get_elements ();
items_list.reverse ();

foreach (unowned Json.Node node in items_list) {
load_item (node.get_object (), "item");
load_item (canvas, node.get_object (), "item");
}
}

window.event_bus.set_scale (obj.get_double_member ("scale"));
window.main_window.main_canvas.main_scroll.hadjustment.value = obj.get_double_member ("hadjustment");
window.main_window.main_canvas.main_scroll.vadjustment.value = obj.get_double_member ("vadjustment");
//window.event_bus.set_scale (obj.get_double_member ("scale"));
//window.main_window.main_canvas.main_scroll.hadjustment.value = obj.get_double_member ("hadjustment");
//window.main_window.main_canvas.main_scroll.vadjustment.value = obj.get_double_member ("vadjustment");

// Reset the holding state at the end of it.
window.main_window.main_canvas.canvas.holding = false;
canvas.holding = false;
}

private void load_item (Json.Object obj, string type) {
var item = obj.get_member (type).get_object ();
private static void load_item (Akira.Lib.Canvas canvas, Json.Object json_object, string type) {
var item = json_object.get_member (type).get_object ();
if (item != null) {
// debug ("loading %s", type);
window.items_manager.load_item (item);
canvas.window.items_manager.load_item (item);
}
}
}
6 changes: 6 additions & 0 deletions src/Lib/Canvas.vala
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class Akira.Lib.Canvas : Goo.Canvas {
MODE_PANNING,
}

public Managers.UndoManager undo_manager;
public Managers.ExportManager export_manager;
public Managers.SelectedBoundManager selected_bound_manager;
private Managers.NobManager nob_manager;
Expand Down Expand Up @@ -85,6 +86,7 @@ public class Akira.Lib.Canvas : Goo.Canvas {
events |= Gdk.EventMask.TOUCHPAD_GESTURE_MASK;
events |= Gdk.EventMask.TOUCH_MASK;

undo_manager = new Managers.UndoManager ();
export_manager = new Managers.ExportManager (this);
selected_bound_manager = new Managers.SelectedBoundManager (this);
nob_manager = new Managers.NobManager (this);
Expand Down Expand Up @@ -258,6 +260,7 @@ public class Akira.Lib.Canvas : Goo.Canvas {

switch (edit_mode) {
case EditMode.MODE_INSERT:
undo_manager.add_undo(this);
selected_bound_manager.reset_selection ();

var new_item = window.items_manager.insert_item (event.x, event.y);
Expand Down Expand Up @@ -375,6 +378,7 @@ public class Akira.Lib.Canvas : Goo.Canvas {
break;

case EditMode.MODE_SELECTION:
undo_manager.add_undo(this);
window.event_bus.detect_artboard_change ();
window.event_bus.detect_image_size_change ();
break;
Expand Down Expand Up @@ -424,6 +428,8 @@ public class Akira.Lib.Canvas : Goo.Canvas {

public void on_insert_item () {
edit_mode = EditMode.MODE_INSERT;

debug("add item");
}

public void on_set_focus_on_canvas () {
Expand Down
107 changes: 107 additions & 0 deletions src/Lib/Managers/UndoManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@







public class Akira.Lib.Managers.UndoManager : Object {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call this HistoryManager since it will handle both undos and redos.


private GLib.Queue<string> undos;
private GLib.Queue<string> redos;

public UndoManager () {
undos = new GLib.Queue<string>();
redos = new GLib.Queue<string>();
}


public void add_undo(Akira.Lib.Canvas canvas) {
redos.clear ();
inner_add_undo(Akira.FileFormat.JsonContent.serialize_canvas(canvas));
debug("%d %d", (int)undos.get_length(), (int)redos.get_length());
}

public void apply_undo(Akira.Lib.Canvas canvas) {
if (undos.get_length () == 0) {
debug("undo empty");
return;
}

var old_undo = undos.pop_head();
inner_add_redo(old_undo);

try {
var parser = new Json.Parser ();
parser.load_from_data (old_undo);
var obj = parser.get_root ().get_object ();

clear_canvas(canvas);
Akira.FileFormat.JsonLoader.inner_load_content(canvas, obj);
Comment on lines +39 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this, it seems a bit extreme.
With this if we move an item and we want to revert to its previous position, we're clearing the canvas and reloading everything. That seems expensive.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If clearing and reloading is expensive, we have bigger issues than undo/redo. There is no reason the view should be expensive to regenerate.

I'm open to new ideas, but am cautious about an alternative that doesn't require cleaning the slate. They tend to be rather brittle and crash or artifact prone approaches.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, the current implementation of clearing and reloading is not efficient, but that's a different story.


} catch (Error e) {
debug("failed to read undo");
return;
}

debug("%d %d", (int)undos.get_length(), (int)redos.get_length());
}

private void inner_add_undo(string undo_to_add) {
undos.push_head(undo_to_add);
}

private void inner_add_redo(string redo_to_add) {
redos.push_head(redo_to_add);
}

public void apply_redo(Akira.Lib.Canvas canvas) {
if (redos.get_length () == 0) {
debug("redo empty");
return;
}

var old_redo = redos.pop_head();
inner_add_undo(old_redo);


try {
var parser = new Json.Parser ();
parser.load_from_data (old_redo);
var obj = parser.get_root ().get_object ();

clear_canvas(canvas);
Akira.FileFormat.JsonLoader.inner_load_content(canvas, obj);

} catch (Error e) {
debug("failed to read redo");
return;
}

debug("%d %d", (int)undos.get_length(), (int)redos.get_length());

}

private void clear_canvas(Akira.Lib.Canvas canvas) {
var item_manager = canvas.window.items_manager;

var to_delete = new GLib.Queue<Akira.Lib.Items.CanvasItem>();

foreach (var item in item_manager.free_items) {
to_delete.push_head(item);
}

foreach (var item in item_manager.artboards) {
to_delete.push_head(item);
}

foreach (var item in item_manager.images) {
to_delete.push_head(item);
}

while (to_delete.get_length () != 0) {
canvas.window.event_bus.request_delete_item (to_delete.pop_head());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to collect the images as the request_delete_item will remove those automatically.
Just artboards and free items.
Here we could also make this method async and use multi threading to prevent UI freeze when clearing a very busy canvas.

}
}

}
17 changes: 17 additions & 0 deletions src/Services/ActionManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public class Akira.Services.ActionManager : Object {
public const string ACTION_ESCAPE = "action_escape";
public const string ACTION_SHORTCUTS = "action_shortcuts";
public const string ACTION_PICK_COLOR = "action_pick_color";
public const string ACTION_UNDO = "action_undo";
public const string ACTION_REDO = "action_redo";

public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string> ();
public static Gee.MultiMap<string, string> typing_accelerators = new Gee.HashMultiMap<string, string> ();
Expand Down Expand Up @@ -104,6 +106,8 @@ public class Akira.Services.ActionManager : Object {
{ ACTION_ESCAPE, action_escape },
{ ACTION_SHORTCUTS, action_shortcuts },
{ ACTION_PICK_COLOR, action_pick_color },
{ ACTION_UNDO, do_undo },
{ ACTION_REDO, do_redo },
};

public ActionManager (Akira.Application akira_app, Akira.Window window) {
Expand Down Expand Up @@ -149,6 +153,9 @@ public class Akira.Services.ActionManager : Object {
typing_accelerators.set (ACTION_DELETE, "Delete");
typing_accelerators.set (ACTION_DELETE, "BackSpace");
typing_accelerators.set (ACTION_TOGGLE_PIXEL_GRID, "<Shift>Tab");

typing_accelerators.set (ACTION_UNDO, "<Control>z");
typing_accelerators.set (ACTION_REDO, "<Control><Shift>z");
}

construct {
Expand Down Expand Up @@ -509,6 +516,16 @@ public class Akira.Services.ActionManager : Object {
});
}

private void do_undo () {
weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas;
canvas.undo_manager.apply_undo(canvas);
}

private void do_redo () {
weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas;
canvas.undo_manager.apply_redo(canvas);
}

public static void action_from_group (string action_name, ActionGroup? action_group) {
action_group.activate_action (action_name, null);
}
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ sources = files(
'Lib/Managers/NobManager.vala',
'Lib/Managers/SelectedBoundManager.vala',
'Lib/Managers/SnapManager.vala',
'Lib/Managers/UndoManager.vala',

'Lib/Selection/Nob.vala',

Expand Down