mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 11:28:30 +08:00
Drag to rearrange layers in the layer panel (#434)
* Add insert line * Implement dragging * Improve CSS and variable naming consistency * Resolved folder crash, added Undo/Redo support * Removed marker TODO leftover * JS cleanup * WIP preserving expanded state (via LayerData) when copy/pasting and moving layers in layer panel * Finish making folders copy/paste preserving expanded state, but not recursively yet * Add cut, copy, and paste to the Edit menu (#440) * Add cut * Hook up edit dropdown * Use copy message Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: otdavies <oliver@psyfer.io>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
otdavies
parent
b87b527566
commit
1a70667f63
@@ -15,8 +15,8 @@
|
||||
</PopoverButton>
|
||||
</LayoutRow>
|
||||
<LayoutRow :class="'layer-tree scrollable-y'">
|
||||
<LayoutCol :class="'list'" @click="deselectAllLayers">
|
||||
<div class="layer-row" v-for="layer in layers" :key="layer.path">
|
||||
<LayoutCol :class="'list'" ref="layerTreeList" @click="deselectAllLayers" @dragover="updateLine($event)" @dragend="drop()">
|
||||
<div class="layer-row" v-for="(layer, index) in layers" :key="layer.path">
|
||||
<div class="layer-visibility">
|
||||
<IconButton
|
||||
:action="(e) => (toggleLayerVisibility(layer.path), e.stopPropagation())"
|
||||
@@ -40,6 +40,9 @@
|
||||
@click.shift.ctrl.exact.stop="selectLayer(layer, true, true)"
|
||||
@click.ctrl.exact.stop="selectLayer(layer, true, false)"
|
||||
@click.exact.stop="selectLayer(layer, false, false)"
|
||||
:data-index="index"
|
||||
draggable="true"
|
||||
@dragstart="dragStart($event, layer)"
|
||||
>
|
||||
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
|
||||
<div class="layer-type-icon">
|
||||
@@ -85,7 +88,8 @@
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
|
||||
& + .layer-row {
|
||||
& + .layer-row,
|
||||
& + .insert-mark + .layer-row {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
@@ -190,6 +194,33 @@
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.insert-mark {
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
height: 0;
|
||||
z-index: 2;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--color-accent-hover);
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&:not(:first-child, :last-child) {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&:first-child::after {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:last-child::after {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -250,6 +281,10 @@ const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
||||
],
|
||||
];
|
||||
|
||||
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 40;
|
||||
const LAYER_LEFT_MARGIN_OFFSET = 28;
|
||||
const LAYER_LEFT_INDENT_OFFSET = 16;
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
data() {
|
||||
@@ -265,6 +300,7 @@ export default defineComponent({
|
||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||
opacity: 100,
|
||||
draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement },
|
||||
MenuDirection,
|
||||
SeparatorType,
|
||||
LayerTypeOptions,
|
||||
@@ -303,6 +339,112 @@ export default defineComponent({
|
||||
layer.layer_data.selected = false;
|
||||
});
|
||||
},
|
||||
closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, Node] {
|
||||
const treeChildren = tree.children;
|
||||
|
||||
// Closest distance to the middle of the row along the Y axis
|
||||
let closest = Infinity;
|
||||
|
||||
// The nearest row parent (element of the tree)
|
||||
let nearestElement = tree.lastChild as Node;
|
||||
|
||||
// The nearest element in the path to the mouse
|
||||
let nearestPath = new BigUint64Array();
|
||||
|
||||
// Item goes above or below the mouse
|
||||
let above = false;
|
||||
|
||||
Array.from(treeChildren).forEach((treeChild) => {
|
||||
if (treeChild.childElementCount <= 2) return;
|
||||
|
||||
const child = treeChild.children[2] as HTMLElement;
|
||||
|
||||
const indexAttribute = child.getAttribute("data-index");
|
||||
if (!indexAttribute) return;
|
||||
const layer = this.layers[parseInt(indexAttribute, 10)];
|
||||
|
||||
const rect = child.getBoundingClientRect();
|
||||
const position = rect.top + rect.height / 2;
|
||||
const distance = position - clientY;
|
||||
|
||||
// Inserting above current row
|
||||
if (distance > 0 && distance < closest) {
|
||||
closest = distance;
|
||||
nearestPath = layer.path;
|
||||
above = true;
|
||||
if (child.parentNode) {
|
||||
nearestElement = child.parentNode;
|
||||
}
|
||||
}
|
||||
// Inserting below current row
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0 && layer.layer_type !== LayerTypeOptions.Folder) {
|
||||
closest = -distance;
|
||||
nearestPath = layer.path;
|
||||
if (child.parentNode && child.parentNode.nextSibling) {
|
||||
nearestElement = child.parentNode.nextSibling;
|
||||
}
|
||||
}
|
||||
// Inserting with no nesting at the end of the panel
|
||||
else if (closest === Infinity) {
|
||||
nearestPath = layer.path.slice(0, 1);
|
||||
}
|
||||
});
|
||||
|
||||
return [nearestPath, above, nearestElement];
|
||||
},
|
||||
async dragStart(event: DragEvent, layer: LayerPanelEntry) {
|
||||
// Set style of cursor for drag
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
|
||||
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el;
|
||||
|
||||
// Create the insert line
|
||||
const insertLine = document.createElement("div") as HTMLDivElement;
|
||||
insertLine.classList.add("insert-mark");
|
||||
tree.appendChild(insertLine);
|
||||
|
||||
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
|
||||
|
||||
// Set the initial state of the line
|
||||
if (nearestElement.parentNode) {
|
||||
insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
|
||||
tree.insertBefore(insertLine, nearestElement);
|
||||
}
|
||||
|
||||
this.draggingData = { path: layer.path, above, nearestPath, insertLine };
|
||||
},
|
||||
updateLine(event: DragEvent) {
|
||||
// Stop the drag from being shown as cancelled
|
||||
event.preventDefault();
|
||||
|
||||
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
|
||||
|
||||
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
|
||||
|
||||
if (this.draggingData) {
|
||||
this.draggingData.nearestPath = nearestPath;
|
||||
this.draggingData.above = above;
|
||||
|
||||
if (nearestElement.parentNode) {
|
||||
this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
|
||||
tree.insertBefore(this.draggingData.insertLine, nearestElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeLine() {
|
||||
if (this.draggingData) {
|
||||
this.draggingData.insertLine.remove();
|
||||
}
|
||||
},
|
||||
async drop() {
|
||||
this.removeLine();
|
||||
if (this.draggingData) {
|
||||
this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath);
|
||||
}
|
||||
},
|
||||
setBlendModeForSelectedLayers() {
|
||||
const selected = this.layers.filter((layer) => layer.layer_data.selected);
|
||||
|
||||
|
||||
@@ -197,6 +197,8 @@ export enum MenuType {
|
||||
Dialog = "Dialog",
|
||||
}
|
||||
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
@@ -313,7 +315,6 @@ export default defineComponent({
|
||||
floatingMenuContent.style.minWidth = minWidth;
|
||||
},
|
||||
pointerMoveHandler(e: PointerEvent) {
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
const target = e.target as HTMLElement;
|
||||
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
|
||||
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);
|
||||
|
||||
Reference in New Issue
Block a user