mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Delete selected layers (#173)
* Delete Layers * Fix backend selection * Fix shift selection * Add desired shape add selection behavior * Add X and Backspace as keybinds for layer deletion * Change key storage to u128 * Remove unhelpful trace logging * Reverse display direction for layers
This commit is contained in:
parent
981f138812
commit
0e488d99ff
10 changed files with 81 additions and 28 deletions
|
|
@ -144,9 +144,9 @@ export default defineComponent({
|
|||
async handleShiftClick(clickedLayer: LayerPanelEntry) {
|
||||
// The two paths of the range are stored in selectionRangeStartLayer and selectionRangeEndLayer
|
||||
// So for a new Shift+Click, select all layers between selectionRangeStartLayer and selectionRangeEndLayer(stored in prev Sft+C)
|
||||
this.selectionRangeEndLayer = clickedLayer;
|
||||
this.selectionRangeStartLayer = (this.selectionRangeStartLayer as LayerPanelEntry) || clickedLayer;
|
||||
this.clearSelection();
|
||||
this.selectionRangeEndLayer = clickedLayer;
|
||||
if (!this.selectionRangeStartLayer) this.selectionRangeStartLayer = clickedLayer;
|
||||
this.fillSelectionRange(this.selectionRangeStartLayer, this.selectionRangeEndLayer, true);
|
||||
this.updateSelection();
|
||||
},
|
||||
|
|
@ -159,11 +159,13 @@ export default defineComponent({
|
|||
this.updateSelection();
|
||||
},
|
||||
async fillSelectionRange(start: LayerPanelEntry, end: LayerPanelEntry, selected = true) {
|
||||
const startIndex = this.layers.indexOf(start);
|
||||
const endIndex = this.layers.indexOf(end);
|
||||
const startIndex = this.layers.findIndex((layer) => layer.path.join() === start.path.join());
|
||||
const endIndex = this.layers.findIndex((layer) => layer.path.join() === end.path.join());
|
||||
const [min, max] = [startIndex, endIndex].sort();
|
||||
for (let i = min; i <= max; i += 1) {
|
||||
this.layers[i].layer_data.selected = selected;
|
||||
if (min !== -1) {
|
||||
for (let i = min; i <= max; i += 1) {
|
||||
this.layers[i].layer_data.selected = selected;
|
||||
}
|
||||
}
|
||||
},
|
||||
async clearSelection() {
|
||||
|
|
@ -173,6 +175,7 @@ export default defineComponent({
|
|||
},
|
||||
async updateSelection() {
|
||||
const paths = this.layers.filter((layer) => layer.layer_data.selected).map((layer) => layer.path);
|
||||
if (paths.length === 0) return;
|
||||
const length = paths.reduce((acc, cur) => acc + cur.length, 0) + paths.length - 1;
|
||||
const output = new BigUint64Array(length);
|
||||
let i = 0;
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ pub fn translate_key(name: &str) -> Key {
|
|||
// When using linux + chrome + the neo keyboard layout, the shift key is recognized as caps
|
||||
"capslock" => KeyShift,
|
||||
"control" => KeyControl,
|
||||
"delete" => KeyDelete,
|
||||
"backspace" => KeyBackspace,
|
||||
"alt" => KeyAlt,
|
||||
"escape" => KeyEscape,
|
||||
_ => UnknownKey,
|
||||
|
|
|
|||
|
|
@ -172,9 +172,10 @@ impl Document {
|
|||
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
let responses = match &operation {
|
||||
Operation::AddCircle { path, insert_index, cx, cy, r, style } => {
|
||||
self.add_layer(&path, Layer::new(LayerDataTypes::Circle(layers::Circle::new((*cx, *cy), *r, *style))), *insert_index)?;
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Circle(layers::Circle::new((*cx, *cy), *r, *style))), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
|
||||
}
|
||||
Operation::AddEllipse {
|
||||
path,
|
||||
|
|
@ -186,9 +187,10 @@ impl Document {
|
|||
rot,
|
||||
style,
|
||||
} => {
|
||||
self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((*cx, *cy), (*rx, *ry), *rot, *style))), *insert_index)?;
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((*cx, *cy), (*rx, *ry), *rot, *style))), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
|
||||
}
|
||||
Operation::AddRect {
|
||||
path,
|
||||
|
|
@ -199,9 +201,10 @@ impl Document {
|
|||
y1,
|
||||
style,
|
||||
} => {
|
||||
self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
|
||||
}
|
||||
Operation::AddLine {
|
||||
path,
|
||||
|
|
@ -212,9 +215,10 @@ impl Document {
|
|||
y1,
|
||||
style,
|
||||
} => {
|
||||
self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
|
||||
}
|
||||
Operation::AddPen { path, insert_index, points, style } => {
|
||||
let points: Vec<kurbo::Point> = points.iter().map(|&it| it.into()).collect();
|
||||
|
|
@ -233,9 +237,10 @@ impl Document {
|
|||
style,
|
||||
} => {
|
||||
let s = Shape::new((*x0, *y0), (*x1, *y1), *sides, *style);
|
||||
self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), *insert_index)?;
|
||||
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), *insert_index)?;
|
||||
let path = [path.clone(), vec![id]].concat();
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged])
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
|
||||
}
|
||||
Operation::DeleteLayer { path } => {
|
||||
self.delete(&path)?;
|
||||
|
|
@ -281,8 +286,9 @@ impl Document {
|
|||
responses.append(&mut op_responses);
|
||||
}
|
||||
}
|
||||
responses.extend(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }]);
|
||||
|
||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
|
||||
Some(responses)
|
||||
}
|
||||
Operation::ToggleVisibility { path } => {
|
||||
let _ = self.layer_mut(&path).map(|layer| {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use std::fmt;
|
|||
pub enum DocumentResponse {
|
||||
DocumentChanged,
|
||||
FolderChanged { path: Vec<LayerId> },
|
||||
SelectLayer { path: Vec<LayerId> },
|
||||
}
|
||||
|
||||
impl fmt::Display for DocumentResponse {
|
||||
|
|
@ -14,6 +15,7 @@ impl fmt::Display for DocumentResponse {
|
|||
let name = match self {
|
||||
DocumentResponse::DocumentChanged { .. } => "DocumentChanged",
|
||||
DocumentResponse::FolderChanged { .. } => "FolderChanged",
|
||||
DocumentResponse::SelectLayer { .. } => "SelectLayer",
|
||||
};
|
||||
|
||||
formatter.write_str(name)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,16 @@ impl Dispatcher {
|
|||
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Result<(), EditorError> {
|
||||
let message = message.into();
|
||||
use Message::*;
|
||||
if !matches!(
|
||||
if !(matches!(
|
||||
message,
|
||||
Message::InputPreprocessor(_) | Message::InputMapper(_) | Message::Tool(ToolMessage::Rectangle(RectangleMessage::MouseMove))
|
||||
) {
|
||||
log::trace!("Message: {}", message.to_discriminant().global_name());
|
||||
Message::InputPreprocessor(_)
|
||||
| Message::InputMapper(_)
|
||||
| Message::Document(DocumentMessage::RenderDocument)
|
||||
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
|
||||
| Message::Document(DocumentMessage::DispatchOperation { .. })
|
||||
) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove"))
|
||||
{
|
||||
log::trace!("Message: {}", message.to_discriminant().local_name());
|
||||
}
|
||||
match message {
|
||||
NoOp => (),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ impl Document {
|
|||
.layers()
|
||||
.iter()
|
||||
.zip(folder.layer_ids.iter())
|
||||
.rev()
|
||||
.map(|(layer, id)| {
|
||||
let path = [path, &[*id]].concat();
|
||||
layer_panel_entry(layer_data(self_layer_data, &path), layer, path)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub enum DocumentMessage {
|
|||
DispatchOperation(DocumentOperation),
|
||||
SelectLayers(Vec<Vec<LayerId>>),
|
||||
DeleteLayer(Vec<LayerId>),
|
||||
DeleteSelectedLayers,
|
||||
AddFolder(Vec<LayerId>),
|
||||
RenameLayer(Vec<LayerId>, String),
|
||||
ToggleLayerVisibility(Vec<LayerId>),
|
||||
|
|
@ -56,6 +57,14 @@ impl DocumentMessageHandler {
|
|||
FrontendMessage::ExpandFolder { path, children }.into()
|
||||
})
|
||||
}
|
||||
fn clear_selection(&mut self) {
|
||||
self.active_document_mut().layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
|
||||
}
|
||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||
self.active_document_mut().layer_data(&path).selected = true;
|
||||
// TODO: Add deduplication
|
||||
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DocumentMessageHandler {
|
||||
|
|
@ -95,11 +104,18 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
|
|||
self.active_document_mut().layer_data(&path).expanded ^= true;
|
||||
responses.extend(self.handle_folder_changed(path));
|
||||
}
|
||||
SelectLayers(paths) => {
|
||||
DeleteSelectedLayers => {
|
||||
// TODO: Replace with drain_filter https://github.com/rust-lang/rust/issues/59618
|
||||
let paths: Vec<Vec<LayerId>> = self.active_document().layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())).collect();
|
||||
for path in paths {
|
||||
self.active_document_mut().layer_data(&path).selected ^= true;
|
||||
responses.extend(self.handle_folder_changed(path));
|
||||
// TODO: Add deduplication
|
||||
self.active_document_mut().layer_data.remove(&path);
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
|
||||
}
|
||||
}
|
||||
SelectLayers(paths) => {
|
||||
self.clear_selection();
|
||||
for path in paths {
|
||||
responses.extend(self.select_layer(&path));
|
||||
}
|
||||
}
|
||||
Undo => {
|
||||
|
|
@ -116,6 +132,14 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
|
|||
.into_iter()
|
||||
.map(|response| match response {
|
||||
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
|
||||
DocumentResponse::SelectLayer { path } => {
|
||||
if !self.active_document().document.work_mounted {
|
||||
self.clear_selection();
|
||||
self.select_layer(&path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
DocumentResponse::DocumentChanged => unreachable!(),
|
||||
})
|
||||
.flatten(),
|
||||
|
|
@ -134,5 +158,11 @@ impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
|
|||
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
|
||||
}
|
||||
}
|
||||
advertise_actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument);
|
||||
fn actions(&self) -> ActionList {
|
||||
if self.active_document().layer_data.values().any(|data| data.selected) {
|
||||
actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, RenderDocument, ExportDocument)
|
||||
} else {
|
||||
actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ impl FrontendMessageHandler {
|
|||
|
||||
impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
|
||||
fn process_action(&mut self, message: FrontendMessage, _data: (), _responses: &mut VecDeque<Message>) {
|
||||
log::trace!("Sending {} Response", message.to_discriminant().global_name());
|
||||
(self.callback)(message)
|
||||
}
|
||||
advertise_actions!(
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ impl Default for Mapping {
|
|||
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
|
||||
// Document Actions
|
||||
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||
// Tool Actions
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
|
||||
// Edit this to specify the storage type used
|
||||
// TODO: Increase size of type
|
||||
pub type StorageType = u8;
|
||||
pub type StorageType = u128;
|
||||
const STORAGE_SIZE: u32 = std::mem::size_of::<usize>() as u32 * 8 + 2 - std::mem::size_of::<StorageType>().leading_zeros();
|
||||
const STORAGE_SIZE_BITS: usize = 1 << STORAGE_SIZE;
|
||||
const KEY_MASK_STORAGE_LENGTH: usize = (NUMBER_OF_KEYS + STORAGE_SIZE_BITS - 1) >> STORAGE_SIZE;
|
||||
|
|
@ -55,6 +55,8 @@ pub enum Key {
|
|||
KeyEnter,
|
||||
KeyShift,
|
||||
KeyControl,
|
||||
KeyDelete,
|
||||
KeyBackspace,
|
||||
KeyAlt,
|
||||
KeyEscape,
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue