mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-02 12:32:17 +00:00
Implement layer renaming (#501)
* Implement layer renaming * Fix sneaky typo in CSS Co-authored-by: Moritz Vetter <16950410+HansAuger@users.noreply.github.com> Co-authored-by: Moritz Vetter <16950410+HansAuger@users.noreply.github.com>
This commit is contained in:
parent
668d42371c
commit
434970aa16
11 changed files with 144 additions and 42 deletions
|
@ -99,6 +99,10 @@ pub enum DocumentMessage {
|
|||
layer_path: Vec<LayerId>,
|
||||
set_expanded: bool,
|
||||
},
|
||||
SetLayerName {
|
||||
layer_path: Vec<LayerId>,
|
||||
name: String,
|
||||
},
|
||||
SetOpacityForSelectedLayers {
|
||||
opacity: f64,
|
||||
},
|
||||
|
|
|
@ -401,6 +401,7 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: This should probably take a slice not a vec, also why does this even exist when `layer_panel_entry_from_path` also exists?
|
||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||
let data: LayerMetadata = *self
|
||||
.layer_metadata
|
||||
|
@ -927,6 +928,15 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
|
|||
responses.push_back(DocumentStructureChanged.into());
|
||||
responses.push_back(LayerChanged { affected_layer_path: layer_path }.into())
|
||||
}
|
||||
SetLayerName { layer_path, name } => {
|
||||
if let Some(layer) = self.layer_panel_entry_from_path(&layer_path) {
|
||||
// Only save the history state if the name actually changed to something different
|
||||
if layer.name != name {
|
||||
self.backup(responses);
|
||||
responses.push_back(DocumentOperation::SetLayerName { path: layer_path, name }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
SetOpacityForSelectedLayers { opacity } => {
|
||||
self.backup(responses);
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
|
|
|
@ -21,8 +21,7 @@ impl LayerMetadata {
|
|||
}
|
||||
|
||||
pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
|
||||
let layer_type: LayerDataTypeDiscriminant = (&layer.data).into();
|
||||
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
|
||||
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
|
||||
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ impl SnapHandler {
|
|||
overlay_paths: &mut Vec<Vec<LayerId>>,
|
||||
responses: &mut VecDeque<Message>,
|
||||
viewport_bounds: DVec2,
|
||||
(positions_and_distances): (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
||||
positions_and_distances: (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
||||
closest_distance: DVec2,
|
||||
) {
|
||||
/// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available.
|
||||
|
|
|
@ -87,6 +87,10 @@ button {
|
|||
color: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
display: block;
|
||||
|
|
|
@ -29,21 +29,21 @@
|
|||
</PopoverButton>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="layer-tree" :scrollableY="true">
|
||||
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop()">
|
||||
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="draggable && drop()">
|
||||
<LayoutRow
|
||||
class="layer-row"
|
||||
v-for="(listing, index) in layers"
|
||||
:key="String(listing.entry.path.slice(-1))"
|
||||
:class="{ 'insert-folder': draggingData && draggingData.highlightFolder && draggingData.insertFolder === listing.entry.path }"
|
||||
>
|
||||
<div class="visibility">
|
||||
<LayoutRow class="visibility">
|
||||
<IconButton
|
||||
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())"
|
||||
:size="24"
|
||||
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
|
||||
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
|
||||
/>
|
||||
</div>
|
||||
</LayoutRow>
|
||||
|
||||
<div class="indent" :style="{ marginLeft: layerIndent(listing.entry) }"></div>
|
||||
|
||||
|
@ -53,27 +53,36 @@
|
|||
:class="{ expanded: listing.entry.layer_metadata.expanded }"
|
||||
@click.stop="handleExpandArrowClick(listing.entry.path)"
|
||||
></button>
|
||||
<div
|
||||
<LayoutRow
|
||||
class="layer"
|
||||
:class="{ selected: listing.entry.layer_metadata.selected }"
|
||||
@click.shift.exact.stop="selectLayer(listing.entry, false, true)"
|
||||
@click.shift.ctrl.exact.stop="selectLayer(listing.entry, true, true)"
|
||||
@click.ctrl.exact.stop="selectLayer(listing.entry, true, false)"
|
||||
@click.exact.stop="selectLayer(listing.entry, false, false)"
|
||||
@click.shift.exact.stop="!listing.editingName && selectLayer(listing.entry, false, true)"
|
||||
@click.shift.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, true)"
|
||||
@click.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, false)"
|
||||
@click.exact.stop="!listing.editingName && selectLayer(listing.entry, false, false)"
|
||||
:data-index="index"
|
||||
draggable="true"
|
||||
@dragstart="dragStart($event, listing.entry)"
|
||||
:title="String(listing.entry.path)"
|
||||
:draggable="draggable"
|
||||
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
|
||||
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`"
|
||||
>
|
||||
<div class="layer-type-icon">
|
||||
<LayoutRow class="layer-type-icon">
|
||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeTypeFolder'" title="Folder" />
|
||||
<IconLabel v-else :icon="'NodeTypePath'" title="Path" />
|
||||
</div>
|
||||
<div class="layer-name">
|
||||
<span>{{ listing.entry.name }}</span>
|
||||
</div>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="layer-name" @dblclick="onEditLayerName(listing)">
|
||||
<input
|
||||
type="text"
|
||||
:value="listing.entry.name"
|
||||
:placeholder="listing.entry.layer_type"
|
||||
:disabled="!listing.editingName"
|
||||
@change="(e) => onEditLayerNameChange(listing, e.target)"
|
||||
@blur="onEditLayerNameDeselect(listing)"
|
||||
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target)"
|
||||
@keydown.escape="onEditLayerNameDeselect(listing)"
|
||||
/>
|
||||
</LayoutRow>
|
||||
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
|
||||
</div>
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
<div class="insert-mark" v-if="draggingData && !draggingData.highlightFolder" :style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"></div>
|
||||
|
@ -115,9 +124,8 @@
|
|||
border-bottom: 1px solid var(--color-4-dimgray);
|
||||
|
||||
.visibility {
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.icon-button {
|
||||
|
@ -170,14 +178,12 @@
|
|||
}
|
||||
|
||||
.layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
|
||||
&.selected {
|
||||
|
@ -192,14 +198,41 @@
|
|||
|
||||
.layer-name {
|
||||
flex: 1 1 100%;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
margin: 0 4px;
|
||||
|
||||
span {
|
||||
input {
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
|
||||
&:disabled {
|
||||
user-select: none;
|
||||
// Workaround for `user-select: none` not working on <input> elements
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: var(--color-1-nearblack);
|
||||
padding: 0 4px;
|
||||
|
||||
&::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +287,7 @@ import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
|||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||
|
||||
type LayerListingInfo = { entry: LayerPanelEntry; bottomLayer: boolean; folderIndex: number };
|
||||
type LayerListingInfo = { folderIndex: number; bottomLayer: boolean; editingName: boolean; entry: LayerPanelEntry };
|
||||
|
||||
const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
||||
[{ label: "Normal", value: "Normal" }],
|
||||
|
@ -317,7 +350,9 @@ export default defineComponent({
|
|||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||
opacity: 100,
|
||||
draggable: true,
|
||||
draggingData: undefined as undefined | DraggingData,
|
||||
devMode: process.env.NODE_ENV === "development",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -336,6 +371,36 @@ export default defineComponent({
|
|||
async handleExpandArrowClick(path: BigUint64Array) {
|
||||
this.editor.instance.toggle_layer_expansion(path);
|
||||
},
|
||||
onEditLayerName(listing: LayerListingInfo) {
|
||||
if (listing.editingName) return;
|
||||
|
||||
this.draggable = false;
|
||||
|
||||
listing.editingName = true;
|
||||
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
|
||||
this.$nextTick(() => {
|
||||
(tree.querySelector("input:not([disabled])") as HTMLInputElement).select();
|
||||
});
|
||||
},
|
||||
async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
|
||||
// Eliminate duplicate events
|
||||
if (!listing.editingName) return;
|
||||
|
||||
this.draggable = true;
|
||||
|
||||
const name = (inputElement as HTMLInputElement).value;
|
||||
listing.editingName = false;
|
||||
this.editor.instance.set_layer_name(listing.entry.path, name);
|
||||
},
|
||||
onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
this.draggable = true;
|
||||
|
||||
listing.editingName = false;
|
||||
this.$nextTick(() => {
|
||||
const selection = window.getSelection();
|
||||
if (selection) selection.removeAllRanges();
|
||||
});
|
||||
},
|
||||
async setLayerBlendMode(newSelectedIndex: number) {
|
||||
const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value;
|
||||
if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
|
||||
|
@ -446,6 +511,7 @@ export default defineComponent({
|
|||
this.draggingData = undefined;
|
||||
}
|
||||
},
|
||||
// TODO: Move blend mode setting logic to backend based on the layers it knows are selected
|
||||
setBlendModeForSelectedLayers() {
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
|
@ -466,8 +532,8 @@ export default defineComponent({
|
|||
this.blendModeSelectedIndex = NaN;
|
||||
}
|
||||
},
|
||||
// TODO: Move opacity setting logic to backend based on the layers it knows are selected
|
||||
setOpacityForSelectedLayers() {
|
||||
// todo figure out why this is here
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
if (selected.length < 1) {
|
||||
|
@ -490,15 +556,21 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
|
||||
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
|
||||
const layerPathWithNameBeingEdited = layerWithNameBeingEdited && layerWithNameBeingEdited.entry.path;
|
||||
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited && layerPathWithNameBeingEdited.slice(-1)[0];
|
||||
const path = [] as bigint[];
|
||||
this.layers = [] as { folderIndex: number; bottomLayer: boolean; entry: LayerPanelEntry }[];
|
||||
this.layers = [] as LayerListingInfo[];
|
||||
|
||||
const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: { folderIndex: number; bottomLayer: boolean; entry: LayerPanelEntry }[], cache: Map<string, LayerPanelEntry>): void => {
|
||||
const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
|
||||
folder.children.forEach((item, index) => {
|
||||
// TODO: fix toString
|
||||
path.push(BigInt(item.layerId.toString()));
|
||||
const layerId = BigInt(item.layerId.toString());
|
||||
path.push(layerId);
|
||||
|
||||
const mapping = cache.get(path.toString());
|
||||
if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping });
|
||||
if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping, editingName: layerIdWithNameBeingEdited === layerId });
|
||||
|
||||
if (item.children.length >= 1) recurse(item, layers, cache);
|
||||
path.pop();
|
||||
});
|
||||
|
|
|
@ -52,8 +52,6 @@
|
|||
border: none;
|
||||
background: none;
|
||||
color: var(--color-e-nearwhite);
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
text-align: center;
|
||||
|
||||
&:not(:focus).has-label {
|
||||
|
@ -62,10 +60,6 @@
|
|||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&::selection {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-align: left;
|
||||
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
ref="documentsPanel"
|
||||
/>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="resizePanel($event)"></LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutCol>
|
||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
|
||||
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="resizePanel($event)"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
|
||||
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
|
|
|
@ -364,6 +364,7 @@ impl JsEditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Modify the layer selection based on the layer which is clicked while holding down the <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> modifier keys used for range selection behavior
|
||||
pub fn select_layer(&self, layer_path: Vec<LayerId>, ctrl: bool, shift: bool) {
|
||||
let message = DocumentMessage::SelectLayer { layer_path, ctrl, shift };
|
||||
self.dispatch(message);
|
||||
|
@ -397,6 +398,12 @@ impl JsEditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Set the name for the layer
|
||||
pub fn set_layer_name(&self, layer_path: Vec<LayerId>, name: String) {
|
||||
let message = DocumentMessage::SetLayerName { layer_path, name };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Set the blend mode for the selected layers
|
||||
pub fn set_blend_mode_for_selected_layers(&self, blend_mode_svg_style_name: String) -> Result<(), JsValue> {
|
||||
if let Some(blend_mode) = translate_blend_mode(blend_mode_svg_style_name.as_str()) {
|
||||
|
|
|
@ -641,6 +641,13 @@ impl Document {
|
|||
layer.visible = *visible;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat())
|
||||
}
|
||||
Operation::SetLayerName { path, name } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
let mut layer = self.layer_mut(path)?;
|
||||
layer.name = if name.as_str() == "" { None } else { Some(name.clone()) };
|
||||
|
||||
Some(vec![LayerChanged { path: path.clone() }])
|
||||
}
|
||||
Operation::SetLayerBlendMode { path, blend_mode } => {
|
||||
self.mark_as_dirty(path)?;
|
||||
self.layer_mut(path)?.blend_mode = *blend_mode;
|
||||
|
|
|
@ -10,6 +10,7 @@ use std::hash::{Hash, Hasher};
|
|||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
// TODO: Rename all instances of `path` to `layer_path`
|
||||
pub enum Operation {
|
||||
AddEllipse {
|
||||
path: Vec<LayerId>,
|
||||
|
@ -124,6 +125,10 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
visible: bool,
|
||||
},
|
||||
SetLayerName {
|
||||
path: Vec<LayerId>,
|
||||
name: String,
|
||||
},
|
||||
SetLayerBlendMode {
|
||||
path: Vec<LayerId>,
|
||||
blend_mode: BlendMode,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue