mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 15:28:04 +08:00
Rearrange layers refactor (#281)
* Keep selection during reordering * Fix paste layer selection * Remove junk from layer matadata * Add function to get non_selected_layers * Cleanup * Add tests
This commit is contained in:
@@ -82,6 +82,7 @@ impl Dispatcher {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
communication::DocumentMessageHandler,
|
||||
message_prelude::{DocumentMessage, Message},
|
||||
misc::test_utils::EditorTestUtils,
|
||||
Editor,
|
||||
@@ -123,7 +124,7 @@ mod test {
|
||||
|
||||
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
|
||||
@@ -156,7 +157,7 @@ mod test {
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![shape_id]]))).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
@@ -220,8 +221,8 @@ mod test {
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
@@ -282,8 +283,8 @@ mod test {
|
||||
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
|
||||
editor.draw_rect(0, 800, 12, 200);
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers)).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
|
||||
|
||||
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
|
||||
|
||||
@@ -302,4 +303,28 @@ mod test {
|
||||
assert_eq!(&layers_after_copy[4], rect_before_copy);
|
||||
assert_eq!(&layers_after_copy[5], ellipse_before_copy);
|
||||
}
|
||||
#[test]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - select ellipse and rect
|
||||
/// - move them down and back up again
|
||||
fn move_seletion() {
|
||||
init_logger();
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
|
||||
let verify_order = |handler: &mut DocumentMessageHandler| (handler.all_layers_sorted(), handler.non_selected_layers_sorted(), handler.selected_layers_sorted());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![0], vec![2]]))).unwrap();
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(1))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(-1))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
|
||||
|
||||
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(i32::MAX))).unwrap();
|
||||
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub enum DocumentMessage {
|
||||
DuplicateSelectedLayers,
|
||||
CopySelectedLayers,
|
||||
SetBlendModeForSelectedLayers(BlendMode),
|
||||
PasteLayers,
|
||||
PasteLayers { path: Vec<LayerId>, insert_index: isize },
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
@@ -56,8 +56,9 @@ pub enum DocumentMessage {
|
||||
WheelCanvasZoom,
|
||||
SetCanvasRotation(f64),
|
||||
NudgeSelectedLayers(f64, f64),
|
||||
ReorderSelectedLayers(i32),
|
||||
FlipLayer(Vec<LayerId>, bool, bool),
|
||||
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
|
||||
ReorderSelectedLayers(i32), // relatve_positon,
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for DocumentMessage {
|
||||
@@ -141,7 +142,7 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order, optionally including only selected layers
|
||||
fn layers_sorted(&self, only_selected: bool) -> Vec<Vec<LayerId>> {
|
||||
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
|
||||
// Compute the indices for each layer to be able to sort them
|
||||
// TODO: Replace with drain_filter https://github.com/rust-lang/rust/issues/59618
|
||||
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
|
||||
@@ -149,7 +150,7 @@ impl DocumentMessageHandler {
|
||||
.layer_data
|
||||
.iter()
|
||||
// 'path.len() > 0' filters out root layer since it has no indices
|
||||
.filter_map(|(path, data)| (!path.is_empty() && !only_selected || data.selected).then(|| path.clone()))
|
||||
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
|
||||
.filter_map(|path| {
|
||||
// Currently it is possible that layer_data contains layers that are don't actually exist
|
||||
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
|
||||
@@ -169,13 +170,18 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
/// Returns the paths to all layers in order
|
||||
fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(false)
|
||||
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(None)
|
||||
}
|
||||
|
||||
/// Returns the paths to all selected layers in order
|
||||
fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(true)
|
||||
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(true))
|
||||
}
|
||||
|
||||
/// Returns the paths to all selected layers in order
|
||||
pub fn non_selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
|
||||
self.layers_sorted(Some(false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +367,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
DeleteSelectedLayers => {
|
||||
let paths = self.selected_layers_sorted();
|
||||
for path in paths {
|
||||
self.active_document_mut().layer_data.remove(&path);
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
}
|
||||
@@ -382,10 +387,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
}
|
||||
}
|
||||
}
|
||||
PasteLayers => {
|
||||
for layer in self.copy_buffer.iter() {
|
||||
//TODO: Should be the path to the current folder instead of root
|
||||
responses.push_back(DocumentOperation::PasteLayer { layer: layer.clone(), path: vec![] }.into())
|
||||
PasteLayers { path, insert_index } => {
|
||||
let paste = |layer: &Layer, responses: &mut VecDeque<_>| {
|
||||
log::trace!("pasting into folder {:?} as index: {}", path, insert_index);
|
||||
responses.push_back(
|
||||
DocumentOperation::PasteLayer {
|
||||
layer: layer.clone(),
|
||||
path: path.clone(),
|
||||
insert_index,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
if insert_index == -1 {
|
||||
for layer in self.copy_buffer.iter() {
|
||||
paste(layer, responses)
|
||||
}
|
||||
} else {
|
||||
for layer in self.copy_buffer.iter().rev() {
|
||||
paste(layer, responses)
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectLayers(paths) => {
|
||||
@@ -421,9 +442,12 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
.into_iter()
|
||||
.map(|response| match response {
|
||||
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
|
||||
DocumentResponse::SelectLayer { path } => {
|
||||
DocumentResponse::DeletedLayer { path } => {
|
||||
self.active_document_mut().layer_data.remove(&path);
|
||||
None
|
||||
}
|
||||
DocumentResponse::CreatedLayer { path } => {
|
||||
if !self.active_document().document.work_mounted {
|
||||
self.clear_selection();
|
||||
self.select_layer(&path)
|
||||
} else {
|
||||
None
|
||||
@@ -576,33 +600,40 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||
responses.push_back(operation.into());
|
||||
}
|
||||
}
|
||||
ReorderSelectedLayers(delta) => {
|
||||
let selected_layer_paths: Vec<Vec<LayerId>> = self.selected_layers_sorted();
|
||||
MoveSelectedLayersTo { path, insert_index } => {
|
||||
responses.push_back(DocumentMessage::CopySelectedLayers.into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(DocumentMessage::PasteLayers { path, insert_index }.into());
|
||||
}
|
||||
ReorderSelectedLayers(relative_positon) => {
|
||||
let all_layer_paths = self.all_layers_sorted();
|
||||
|
||||
let max_index = all_layer_paths.len() as i64 - 1;
|
||||
let num_layers_selected = selected_layer_paths.len() as i64;
|
||||
|
||||
let mut selected_layer_index = -1;
|
||||
let mut next_layer_index = -1;
|
||||
for (i, path) in all_layer_paths.iter().enumerate() {
|
||||
if *path == selected_layer_paths[0] {
|
||||
selected_layer_index = i as i32;
|
||||
// Skip past selection length when moving up
|
||||
let offset = if delta > 0 { num_layers_selected - 1 } else { 0 };
|
||||
next_layer_index = (selected_layer_index as i64 + delta as i64 + offset).clamp(0, max_index) as i32;
|
||||
break;
|
||||
let selected_layers = self.selected_layers_sorted();
|
||||
if let Some(pivot) = match relative_positon.signum() {
|
||||
-1 => selected_layers.first(),
|
||||
1 => selected_layers.last(),
|
||||
_ => unreachable!(),
|
||||
} {
|
||||
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
|
||||
let max = all_layer_paths.len() as i64 - 1;
|
||||
let insert_pos = (pos as i64 + relative_positon as i64).clamp(0, max) as usize;
|
||||
let insert = all_layer_paths.get(insert_pos);
|
||||
if let Some(insert_path) = insert {
|
||||
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
||||
if let Some(folder) = self.active_document().document.document_layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
|
||||
let selected: Vec<_> = selected_layers
|
||||
.iter()
|
||||
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
||||
.map(|x| x.last().unwrap())
|
||||
.collect();
|
||||
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
|
||||
let offset = if relative_positon < 0 || non_selected.is_empty() { 0 } else { 1 };
|
||||
let fallback = offset * (non_selected.len());
|
||||
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
|
||||
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if next_layer_index != -1 && next_layer_index != selected_layer_index {
|
||||
let operation = DocumentOperation::ReorderLayers {
|
||||
source_paths: selected_layer_paths.clone(),
|
||||
target_path: all_layer_paths[next_layer_index as usize].to_vec(),
|
||||
};
|
||||
responses.push_back(operation.into());
|
||||
responses.push_back(DocumentMessage::SelectLayers(selected_layer_paths).into());
|
||||
}
|
||||
}
|
||||
FlipLayer(path, flip_horizontal, flip_vertical) => {
|
||||
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
|
||||
|
||||
@@ -111,7 +111,7 @@ macro_rules! mapping {
|
||||
impl Default for Mapping {
|
||||
fn default() -> Self {
|
||||
let mappings = mapping![
|
||||
entry! {action=DocumentMessage::PasteLayers, key_down=KeyV, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::EnableSnapping, key_down=KeyShift},
|
||||
entry! {action=DocumentMessage::DisableSnapping, key_up=KeyShift},
|
||||
// Select
|
||||
|
||||
@@ -86,6 +86,7 @@ impl Fsm for EllipseToolFsmState {
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ impl Fsm for LineToolFsmState {
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ impl Fsm for PenToolFsmState {
|
||||
|
||||
if data.points.len() >= 2 {
|
||||
responses.push_back(make_operation(data, tool_data, false));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(Operation::DiscardWorkingFolder.into());
|
||||
|
||||
@@ -85,6 +85,7 @@ impl Fsm for RectangleToolFsmState {
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||
if data.drag_start != data.drag_current {
|
||||
responses.push_back(make_operation(data, tool_data, transform));
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(Operation::CommitTransaction.into());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user