Skip to content
Merged
3 changes: 2 additions & 1 deletion sass/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@
@import 'editor/editor-confirmation-dialog';
@import 'editor/editor-settings-launch-page';
@import 'editor/editor-engine-picker';
@import 'editor/editor-auditor-picker';
@import 'editor/editor-auditor-picker';
@import 'editor/editor-clipboard';
105 changes: 105 additions & 0 deletions sass/editor/_editor-clipboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
$highlight-color: #07f;

.pcui-contextmenu-clipboard {
> .pcui-menu-items {
> .pcui-menu-item {
> .pcui-menu-item-content {
> .pcui-menu-item-postfix {
flex-grow: 1;
text-align: right;

&.disabled {
display: none;
}
}
}
}
}
}

%pcui-highlight {
content: '\00a0';
position: absolute;
inset: -4px;
border: 1px dotted $highlight-color;
z-index: 1;
pointer-events: none;
}

.pcui-element.pcui-highlight::before {
@extend %pcui-highlight;
}

.pcui-element.pcui-highlight-flash::before {
@extend %pcui-highlight;

animation: pcui-clipboard-flash-animation 200ms ease-in-out forwards;
}

@keyframes pcui-clipboard-flash-animation {
from { border-color: $highlight-color; }
to { border-color: rgba($highlight-color, 0.0); }
}

.pcui-label-group,
.pcui-asset-input {
&:hover {
> .pcui-label > .pcui-clipboard-button {
display: block;

&.pcui-hidden {
display: none;
}
}
}
}

.pcui-label {
&:hover {
> .pcui-clipboard-button {
display: block;

&.pcui-hidden {
display: none;
}
}
}

> .pcui-clipboard-button {
display: none;
position: absolute;
top: 0;
right: 0;
width: 16px;
height: 16px;
line-height: 16px;
margin: 0;
padding: 0;
text-align: center;
border: 0;
background-color: transparent;

&::before {
display: inline-block;
line-height: 16px;
}

&:hover,
&:focus {
box-shadow: none;
background-color: transparent;
}

&.pcui-clipboard-button-copy {
right: 16px;
}

&.pcui-hidden {
display: none;
}
}
}

.ui-tooltip > .inner.pcui-tooltip-disabled {
color: #777;
}
2 changes: 1 addition & 1 deletion src/editor/assets/assets-context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ editor.once('load', () => {
menuItemPaste.disabled = true;
} else {
const clipboard = editor.call('clipboard:get');
menuItemPaste.disabled = !clipboard || clipboard.type !== 'asset';
menuItemPaste.disabled = !clipboard || clipboard.type !== 'asset' || !clipboard.branchId || !clipboard.projectId;
}

menuItemCopy.disabled = !currentAsset;
Expand Down
2 changes: 1 addition & 1 deletion src/editor/entities/entities-context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ editor.once('load', () => {
onIsEnabled: function () {
if (items.length <= 1) {
const clipboard = editor.call('clipboard:get');
if (clipboard && clipboard.type === 'entity') {
if (clipboard && clipboard.type === 'entity' && clipboard.branch && clipboard.scene && clipboard.hierarchy) {
return true;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ import './inspector/settings-panels/batchgroups-create.ts';
// attributes assets
import './attributes/attributes-asset.ts';

// clipboard
import './storage/clipboard-context-menu.ts';

// chat
import './chat/chat-connection.ts';
import './chat/chat-widget.ts';
Expand Down
148 changes: 147 additions & 1 deletion src/editor/inspector/attributes-inspector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Element, Container, LabelGroup, Panel, ArrayInput, BindingTwoWay } from '@playcanvas/pcui';
import { Element, Container, LabelGroup, Panel, Button, ArrayInput, BindingTwoWay } from '@playcanvas/pcui';

import { AssetInput } from '../../common/pcui/element/element-asset-input.ts';
import { tooltip, tooltipRefItem } from '../../common/tooltips.ts';
import { LegacyTooltip } from '../../common/ui/tooltip.ts';
import '../storage/clipboard-context-menu.ts';

const isEnabledAttribute = ({ label, type }) => label === 'enabled' && type === 'boolean';

Expand All @@ -19,6 +21,8 @@ class AttributesInspector extends Container {
*/
_templateOverridesInspector;

_clipboardTypes: Set<string> | null;

constructor(args) {
args = Object.assign({
flex: true
Expand Down Expand Up @@ -47,6 +51,8 @@ class AttributesInspector extends Container {
this._suspendChangeEvt = false;
this._onAttributeChangeHandler = this._onAttributeChange.bind(this);

this._clipboardTypes = editor.call('clipboard:types') ?? null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type editor call return values since type information is lost in event method

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I actually understand this comment.
I've added above the field: _clipboardTypes: Set<string> | null;, which helps to satisfy this?


// entity attributes
args.attributes.forEach((attr) => {
this.addAttribute(attr);
Expand Down Expand Up @@ -128,6 +134,146 @@ class AttributesInspector extends Container {
evtChange = null;
});

// if clipboard types are available
if (this._clipboardTypes) {
// check if attribute has type and path and is of known type
if (attr.type && attr.path && this._clipboardTypes.has(attr.type)) {
// once the field is parented
field.once('parent', (parent) => {
let target = field;

// if part of a label group, provide copying for the whole element
if (parent instanceof LabelGroup) {
target = parent;
}

// modify type based on various rules
let type = attr.type;

if (type === 'select') {
type = attr.args.type;
}
if (type === 'assets') {
type = 'array:asset';
}
if (type === 'slider') {
type = 'number';
}
if ((type === 'asset' || type === 'array:asset') && attr.args?.assetType) {
type += `:${attr.args.assetType}`;
}

// try to bring context menu
const onContextMenu = (evt) => {
// do not interfere with inputs and buttons
if (evt.target.tagName === 'BUTTON' ||
evt.target.tagName === 'INPUT') {

return;
}

// supress default context menu
evt.stopPropagation();
evt.preventDefault();

// call context menu
editor.call('clipboard:contextmenu:open', evt.clientX + 1, evt.clientY, attr.path, type, target.dom);
};

let element = target.dom;
element.addEventListener('contextmenu', onContextMenu);

// clean up on field destroy
field.once('destroy', () => {
element.removeEventListener('contextmenu', onContextMenu);
element = null;
});

if (((target instanceof LabelGroup) || (target instanceof AssetInput)) && type !== 'label') {
target.label.dom.style.position = 'relative';

// paste button
const btnPaste = new Button({
icon: 'E353',
enabled: false,
class: 'pcui-clipboard-button'
});
target.label.dom.appendChild(btnPaste.dom);

btnPaste.on('click', () => {
const pasted = editor.call('clipboard:paste', attr.path, type);
if (pasted) {
editor.call('clipboard:flashElement', target.dom);
}
});

// copy button
const btnCopy = new Button({
icon: 'E126',
enabled: false,
class: ['pcui-clipboard-button', 'pcui-clipboard-button-copy']
});
target.label.dom.appendChild(btnCopy.dom);

// when copy button clicked
btnCopy.on('click', () => {
const copied = editor.call('clipboard:copy', attr.path, type);
if (!copied) {
btnCopy.enabled = false;
btnPaste.enabled = false;
} else {
// toggle paste button
btnPaste.enabled = editor.call('clipboard:validPaste', attr.path, type);
editor.call('clipboard:flashElement', target.dom);
}
});

// tooltip on hover for copy
const tooltipCopy = LegacyTooltip.attach({
target: btnCopy.dom,
text: 'Copy',
align: 'bottom',
root: editor.call('layout.root')
});

// tooltip on hover for paste
const tooltipPaste = LegacyTooltip.attach({
target: btnPaste.dom,
text: 'Paste',
align: 'bottom',
root: editor.call('layout.root')
});

// enabled/disable buttons when hovering on field
target.on('hover', () => {
const canCopy = editor.call('clipboard:validCopy', attr.path, type);
if (!canCopy) {
btnCopy.hidden = true;
btnPaste.hidden = true;
} else {
btnCopy.hidden = false;
btnPaste.hidden = false;
btnCopy.enabled = true;
btnPaste.enabled = editor.call('clipboard:validPaste', attr.path, type);

const humanReadableType = editor.call('clipboard:typeToHuman', type);
tooltipCopy.text = `Copy ${humanReadableType}`;

const clipboardType = editor.call('clipboard:typeToHuman', editor.call('clipboard:type'));
tooltipPaste.text = `Paste${clipboardType ? ` ${clipboardType}` : ''}`;
tooltipPaste.innerElement.classList.toggle('pcui-tooltip-disabled', !btnPaste.enabled);
}
});
}

// TODO:
// extra rule for array of assets, to copy individual assets within the group
});
} else if (attr.type && attr.paths) {
// TODO:
// implement copy-paste for multi-path fields
}
}

const key = this._getFieldKey(attr);
if (key) {
Expand Down
Loading