Implement Undo and Redo (#354)
* Implement undo and redo * Create more save points and hook up menu entry * Fix operation ordering * Remove debug statement * Fix folder changed order * Don't store overlays in the history chain * Keep selection
This commit is contained in:
parent
47fbb8d0fa
commit
0ccb181e2c
|
|
@ -19,6 +19,8 @@ use std::collections::VecDeque;
|
|||
|
||||
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
||||
|
||||
type DocumentSave = (InternalDocument, HashMap<Vec<LayerId>, LayerData>);
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
|
||||
pub enum FlipAxis {
|
||||
X,
|
||||
|
|
@ -42,7 +44,8 @@ pub enum AlignAggregate {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentMessageHandler {
|
||||
pub document: InternalDocument,
|
||||
pub document_backup: Option<InternalDocument>,
|
||||
pub document_history: Vec<DocumentSave>,
|
||||
pub document_redo_history: Vec<DocumentSave>,
|
||||
pub name: String,
|
||||
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
||||
movement_handler: MovementMessageHandler,
|
||||
|
|
@ -52,7 +55,8 @@ impl Default for DocumentMessageHandler {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
document: InternalDocument::default(),
|
||||
document_backup: None,
|
||||
document_history: Vec::new(),
|
||||
document_redo_history: Vec::new(),
|
||||
name: String::from("Untitled Document"),
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
|
|
@ -90,6 +94,10 @@ pub enum DocumentMessage {
|
|||
SaveDocument,
|
||||
RenderDocument,
|
||||
Undo,
|
||||
Redo,
|
||||
DocumentHistoryBackward,
|
||||
DocumentHistoryForward,
|
||||
ClearOverlays,
|
||||
NudgeSelectedLayers(f64, f64),
|
||||
AlignSelectedLayers(AlignAxis, AlignAggregate),
|
||||
MoveSelectedLayersTo {
|
||||
|
|
@ -195,7 +203,8 @@ impl DocumentMessageHandler {
|
|||
pub fn with_name(name: String) -> Self {
|
||||
Self {
|
||||
document: InternalDocument::default(),
|
||||
document_backup: None,
|
||||
document_history: Vec::new(),
|
||||
document_redo_history: Vec::new(),
|
||||
name,
|
||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||
movement_handler: MovementMessageHandler::default(),
|
||||
|
|
@ -219,18 +228,42 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
|
||||
pub fn backup(&mut self) {
|
||||
self.document_backup = Some(self.document.clone())
|
||||
self.document_redo_history.clear();
|
||||
let new_layer_data = self
|
||||
.layer_data
|
||||
.iter()
|
||||
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||
.collect();
|
||||
self.document_history.push((self.document.clone_without_overlays(), new_layer_data))
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> Result<(), EditorError> {
|
||||
self.backup();
|
||||
self.reset()
|
||||
self.undo()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<(), EditorError> {
|
||||
match self.document_backup.take() {
|
||||
Some(backup) => {
|
||||
self.document = backup;
|
||||
pub fn undo(&mut self) -> Result<(), EditorError> {
|
||||
match self.document_history.pop() {
|
||||
Some((document, layer_data)) => {
|
||||
let document = std::mem::replace(&mut self.document, document);
|
||||
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
||||
self.document_redo_history.push((document, layer_data));
|
||||
Ok(())
|
||||
}
|
||||
None => Err(EditorError::NoTransactionInProgress),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Result<(), EditorError> {
|
||||
match self.document_redo_history.pop() {
|
||||
Some((document, layer_data)) => {
|
||||
let document = std::mem::replace(&mut self.document, document);
|
||||
let layer_data = std::mem::replace(&mut self.layer_data, layer_data);
|
||||
let new_layer_data = layer_data
|
||||
.iter()
|
||||
.filter_map(|(key, value)| (!self.document.layer(key).unwrap().overlay).then(|| (key.clone(), *value)))
|
||||
.collect();
|
||||
self.document_history.push((document.clone_without_overlays(), new_layer_data));
|
||||
Ok(())
|
||||
}
|
||||
None => Err(EditorError::NoTransactionInProgress),
|
||||
|
|
@ -284,10 +317,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
||||
}
|
||||
AbortTransaction => {
|
||||
self.reset().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
self.undo().unwrap_or_else(|e| log::warn!("{}", e));
|
||||
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
|
||||
}
|
||||
CommitTransaction => self.document_backup = None,
|
||||
CommitTransaction => (),
|
||||
ExportDocument => {
|
||||
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]);
|
||||
let size = bbox[1] - bbox[0];
|
||||
|
|
@ -325,11 +358,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
)
|
||||
}
|
||||
SetBlendModeForSelectedLayers(blend_mode) => {
|
||||
self.backup();
|
||||
for path in self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
|
||||
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
|
||||
}
|
||||
}
|
||||
SetOpacityForSelectedLayers(opacity) => {
|
||||
self.backup();
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
||||
for path in self.selected_layers().cloned() {
|
||||
|
|
@ -345,12 +380,20 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()),
|
||||
DeleteSelectedLayers => {
|
||||
self.backup();
|
||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
for path in self.selected_layers().cloned() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
|
||||
responses.push_front(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
}
|
||||
ClearOverlays => {
|
||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() {
|
||||
responses.push_front(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
self.backup();
|
||||
for path in self.selected_layers_sorted() {
|
||||
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
|
||||
}
|
||||
|
|
@ -365,7 +408,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
responses.extend(self.handle_folder_changed(Vec::new()));
|
||||
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
}
|
||||
SelectAllLayers => {
|
||||
let all_layer_paths = self
|
||||
|
|
@ -374,16 +417,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
.filter(|path| !path.is_empty() && !self.document.layer(path).unwrap().overlay)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
responses.push_back(SetSelectedLayers(all_layer_paths).into());
|
||||
responses.push_front(SetSelectedLayers(all_layer_paths).into());
|
||||
}
|
||||
DeselectAllLayers => {
|
||||
responses.push_back(SetSelectedLayers(vec![]).into());
|
||||
responses.push_front(SetSelectedLayers(vec![]).into());
|
||||
}
|
||||
DocumentHistoryBackward => self.undo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
DocumentHistoryForward => self.redo().unwrap_or_else(|e| log::warn!("{}", e)),
|
||||
Undo => {
|
||||
// this is a temporary fix and will be addressed by #123
|
||||
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() {
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
|
||||
}
|
||||
responses.push_back(SelectMessage::Abort.into());
|
||||
responses.push_back(DocumentHistoryBackward.into());
|
||||
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(FolderChanged(vec![]).into());
|
||||
}
|
||||
Redo => {
|
||||
responses.push_back(SelectMessage::Abort.into());
|
||||
responses.push_back(DocumentHistoryForward.into());
|
||||
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
responses.push_back(RenderDocument.into());
|
||||
responses.push_back(FolderChanged(vec![]).into());
|
||||
}
|
||||
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
|
||||
DispatchOperation(op) => match self.document.handle_operation(&op) {
|
||||
|
|
@ -446,6 +499,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
|
||||
NudgeSelectedLayers(x, y) => {
|
||||
self.backup();
|
||||
for path in self.selected_layers().cloned() {
|
||||
let operation = DocumentOperation::TransformLayerInViewport {
|
||||
path,
|
||||
|
|
@ -461,6 +515,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_position) => {
|
||||
self.backup();
|
||||
let all_layer_paths = self.all_layers_sorted();
|
||||
let selected_layers = self.selected_layers_sorted();
|
||||
if let Some(pivot) = match relative_position.signum() {
|
||||
|
|
@ -491,6 +546,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
FlipSelectedLayers(axis) => {
|
||||
self.backup();
|
||||
let scale = match axis {
|
||||
FlipAxis::X => DVec2::new(-1., 1.),
|
||||
FlipAxis::Y => DVec2::new(1., -1.),
|
||||
|
|
@ -512,6 +568,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
AlignSelectedLayers(axis, aggregate) => {
|
||||
self.backup();
|
||||
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
|
||||
|
||||
let axis = match axis {
|
||||
|
|
@ -550,6 +607,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(DocumentMessageDiscriminant;
|
||||
Undo,
|
||||
Redo,
|
||||
SelectAllLayers,
|
||||
DeselectAllLayers,
|
||||
RenderDocument,
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ impl Default for Mapping {
|
|||
// Editor Actions
|
||||
entry! {action=FrontendMessage::OpenDocumentBrowse, key_down=KeyO, modifiers=[KeyControl]},
|
||||
// Document Actions
|
||||
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
|
||||
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ impl SelectToolData {
|
|||
}
|
||||
}
|
||||
|
||||
fn add_boundnig_box(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
|
||||
fn add_boundnig_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
|
||||
let path = vec![generate_uuid()];
|
||||
responses.push_back(
|
||||
responses.push(
|
||||
Operation::AddBoundingBox {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
|
|
@ -124,22 +124,25 @@ impl Fsm for SelectToolFsmState {
|
|||
if let ToolMessage::Select(event) = event {
|
||||
match (self, event) {
|
||||
(_, UpdateSelectionBoundingBox) => {
|
||||
let mut buffer = Vec::new();
|
||||
let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) {
|
||||
(None, Some(path)) => Operation::DeleteLayer { path }.into(),
|
||||
(Some([pos1, pos2]), path) => {
|
||||
let path = path.unwrap_or_else(|| add_boundnig_box(responses));
|
||||
let path = path.unwrap_or_else(|| add_boundnig_box(&mut buffer));
|
||||
data.bounding_box_id = Some(path.clone());
|
||||
let transform = transform_from_box(pos1, pos2);
|
||||
Operation::SetLayerTransformInViewport { path, transform }.into()
|
||||
}
|
||||
(_, _) => Message::NoOp,
|
||||
};
|
||||
responses.push_back(response);
|
||||
responses.push_front(response);
|
||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||
self
|
||||
}
|
||||
(Ready, DragStart { add_to_selection }) => {
|
||||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
let mut buffer = Vec::new();
|
||||
let mut selected: Vec<_> = document.selected_layers().cloned().collect();
|
||||
let quad = data.selection_quad();
|
||||
let intersection = document.document.intersects_quad_root(quad);
|
||||
|
|
@ -147,25 +150,29 @@ impl Fsm for SelectToolFsmState {
|
|||
if selected.is_empty() {
|
||||
if let Some(layer) = intersection.last() {
|
||||
selected.push(layer.clone());
|
||||
responses.push_back(DocumentMessage::SetSelectedLayers(selected.clone()).into());
|
||||
buffer.push(DocumentMessage::SetSelectedLayers(selected.clone()).into());
|
||||
}
|
||||
}
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
// Otherwise enter the box select mode
|
||||
if selected.iter().any(|path| intersection.contains(path)) {
|
||||
let state = if selected.iter().any(|path| intersection.contains(path)) {
|
||||
buffer.push(DocumentMessage::StartTransaction.into());
|
||||
data.layers_dragging = selected;
|
||||
Dragging
|
||||
} else {
|
||||
if !input.keyboard.get(add_to_selection as usize) {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
buffer.push(DocumentMessage::DeselectAllLayers.into());
|
||||
}
|
||||
data.drag_box_id = Some(add_boundnig_box(responses));
|
||||
data.drag_box_id = Some(add_boundnig_box(&mut buffer));
|
||||
DrawingBox
|
||||
}
|
||||
};
|
||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||
state
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
for path in data.layers_dragging.iter() {
|
||||
responses.push_back(
|
||||
responses.push_front(
|
||||
Operation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(),
|
||||
|
|
@ -173,7 +180,6 @@ impl Fsm for SelectToolFsmState {
|
|||
.into(),
|
||||
);
|
||||
}
|
||||
responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
data.drag_current = input.mouse.position;
|
||||
Dragging
|
||||
}
|
||||
|
|
@ -183,7 +189,7 @@ impl Fsm for SelectToolFsmState {
|
|||
let start = data.drag_start + half_pixel_offset;
|
||||
let size = data.drag_current - start + half_pixel_offset;
|
||||
|
||||
responses.push_back(
|
||||
responses.push_front(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: data.drag_box_id.clone().unwrap(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
|
|
@ -192,21 +198,27 @@ impl Fsm for SelectToolFsmState {
|
|||
);
|
||||
DrawingBox
|
||||
}
|
||||
(Dragging, DragStop) => Ready,
|
||||
(Dragging, DragStop) => {
|
||||
let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON {
|
||||
true => DocumentMessage::Undo,
|
||||
false => DocumentMessage::CommitTransaction,
|
||||
};
|
||||
responses.push_front(response.into());
|
||||
Ready
|
||||
}
|
||||
(DrawingBox, DragStop) => {
|
||||
let quad = data.selection_quad();
|
||||
responses.push_back(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
|
||||
responses.push_back(
|
||||
responses.push_front(DocumentMessage::AddSelectedLayers(document.document.intersects_quad_root(quad)).into());
|
||||
responses.push_front(
|
||||
Operation::DeleteLayer {
|
||||
path: data.drag_box_id.take().unwrap(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
data.drag_box_id = None;
|
||||
Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_back(Operation::DeleteLayer { path }.into()));
|
||||
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_front(Operation::DeleteLayer { path }.into()));
|
||||
delete(&mut data.drag_box_id);
|
||||
delete(&mut data.bounding_box_id);
|
||||
Ready
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const menuEntries: MenuListEntries = [
|
|||
children: [
|
||||
[
|
||||
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() },
|
||||
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"] },
|
||||
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => (await wasm).redo() },
|
||||
],
|
||||
[
|
||||
{ label: "Cut", shortcut: ["Ctrl", "X"] },
|
||||
|
|
|
|||
|
|
@ -212,6 +212,12 @@ pub fn undo() -> Result<(), JsValue> {
|
|||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Redo history one step
|
||||
#[wasm_bindgen]
|
||||
pub fn redo() -> Result<(), JsValue> {
|
||||
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Redo)).map_err(convert_error)
|
||||
}
|
||||
|
||||
/// Select all layers
|
||||
#[wasm_bindgen]
|
||||
pub fn select_all_layers() -> Result<(), JsValue> {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,24 @@ impl Document {
|
|||
self.set_transform_relative_to_scope(layer, None, transform)
|
||||
}
|
||||
|
||||
fn remove_overlays(&mut self, path: &mut Vec<LayerId>) {
|
||||
if self.layer(path).unwrap().overlay {
|
||||
self.delete(path).unwrap()
|
||||
}
|
||||
let ids = self.folder(path).map(|folder| folder.layer_ids.clone()).unwrap_or_default();
|
||||
for id in ids {
|
||||
path.push(id);
|
||||
self.remove_overlays(path);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_without_overlays(&self) -> Self {
|
||||
let mut document = self.clone();
|
||||
document.remove_overlays(&mut vec![]);
|
||||
document
|
||||
}
|
||||
|
||||
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
|
||||
/// reaction from the frontend, responses may be returned.
|
||||
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue