mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 06:58:56 +02:00
Merge pull request #14186 from thoys/feat/create/entityList-contextMenu
MS19056: [CreateApp/Entity List] Context Menu
This commit is contained in:
commit
5e27a5930e
6 changed files with 331 additions and 13 deletions
|
@ -1801,3 +1801,43 @@ input[type=button]#export {
|
||||||
body#entity-list-body {
|
body#entity-list-body {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
color: #000000;
|
||||||
|
background-color: #afafaf;
|
||||||
|
padding: 5px 0 5px 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.context-menu li {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 4px 18px 4px 18px;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.context-menu li:hover {
|
||||||
|
background-color: #e3e3e3;
|
||||||
|
}
|
||||||
|
.context-menu li.separator {
|
||||||
|
border-top: 1px solid #333333;
|
||||||
|
margin: 5px 5px;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
.context-menu li.disabled {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.context-menu li.separator:hover, .context-menu li.disabled:hover {
|
||||||
|
background-color: #afafaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.rename-entity {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
font-family: FiraSans-SemiBold;
|
||||||
|
font-size: 15px;
|
||||||
|
/* need this to show the text cursor when the input field is empty */
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
||||||
<script type="text/javascript" src="js/spinButtons.js"></script>
|
<script type="text/javascript" src="js/spinButtons.js"></script>
|
||||||
<script type="text/javascript" src="js/listView.js"></script>
|
<script type="text/javascript" src="js/listView.js"></script>
|
||||||
|
<script type="text/javascript" src="js/entityListContextMenu.js"></script>
|
||||||
<script type="text/javascript" src="js/entityList.js"></script>
|
<script type="text/javascript" src="js/entityList.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body onload='loaded();' id="entity-list-body">
|
<body onload='loaded();' id="entity-list-body">
|
||||||
|
|
|
@ -70,6 +70,11 @@ var selectedEntities = [];
|
||||||
|
|
||||||
var entityList = null; // The ListView
|
var entityList = null; // The ListView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type EntityListContextMenu
|
||||||
|
*/
|
||||||
|
var entityListContextMenu = null;
|
||||||
|
|
||||||
var currentSortColumn = 'type';
|
var currentSortColumn = 'type';
|
||||||
var currentSortOrder = ASCENDING_SORT;
|
var currentSortOrder = ASCENDING_SORT;
|
||||||
var isFilterInView = false;
|
var isFilterInView = false;
|
||||||
|
@ -184,11 +189,99 @@ function loaded() {
|
||||||
|
|
||||||
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
|
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
|
||||||
createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT);
|
createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT);
|
||||||
|
|
||||||
|
entityListContextMenu = new EntityListContextMenu();
|
||||||
|
|
||||||
|
|
||||||
|
function startRenamingEntity(entityID) {
|
||||||
|
let entity = entitiesByID[entityID];
|
||||||
|
if (!entity || entity.locked || !entity.elRow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elCell = entity.elRow.childNodes[COLUMN_INDEX.NAME];
|
||||||
|
let elRenameInput = document.createElement("input");
|
||||||
|
elRenameInput.setAttribute('class', 'rename-entity');
|
||||||
|
elRenameInput.value = entity.name;
|
||||||
|
let ignoreClicks = function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
elRenameInput.onclick = ignoreClicks;
|
||||||
|
elRenameInput.ondblclick = ignoreClicks;
|
||||||
|
elRenameInput.onkeyup = function(keyEvent) {
|
||||||
|
if (keyEvent.key === "Enter") {
|
||||||
|
elRenameInput.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
elRenameInput.onblur = function(event) {
|
||||||
|
let value = elRenameInput.value;
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({
|
||||||
|
type: 'rename',
|
||||||
|
entityID: entityID,
|
||||||
|
name: value
|
||||||
|
}));
|
||||||
|
entity.name = value;
|
||||||
|
elCell.innerText = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
elCell.innerHTML = "";
|
||||||
|
elCell.appendChild(elRenameInput);
|
||||||
|
|
||||||
|
elRenameInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) {
|
||||||
|
switch (optionName) {
|
||||||
|
case "Cut":
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'cut' }));
|
||||||
|
break;
|
||||||
|
case "Copy":
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' }));
|
||||||
|
break;
|
||||||
|
case "Paste":
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'paste' }));
|
||||||
|
break;
|
||||||
|
case "Rename":
|
||||||
|
startRenamingEntity(selectedEntityID);
|
||||||
|
break;
|
||||||
|
case "Duplicate":
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'duplicate' }));
|
||||||
|
break;
|
||||||
|
case "Delete":
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onRowContextMenu(clickEvent) {
|
||||||
|
let entityID = this.dataset.entityID;
|
||||||
|
|
||||||
|
if (!selectedEntities.includes(entityID)) {
|
||||||
|
let selection = [entityID];
|
||||||
|
updateSelectedEntities(selection);
|
||||||
|
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({
|
||||||
|
type: "selectionUpdate",
|
||||||
|
focus: false,
|
||||||
|
entityIds: selection,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabledContextMenuItems = ['Copy', 'Paste', 'Duplicate'];
|
||||||
|
if (entitiesByID[entityID] && !entitiesByID[entityID].locked) {
|
||||||
|
enabledContextMenuItems.push('Cut');
|
||||||
|
enabledContextMenuItems.push('Rename');
|
||||||
|
enabledContextMenuItems.push('Delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems);
|
||||||
|
}
|
||||||
|
|
||||||
function onRowClicked(clickEvent) {
|
function onRowClicked(clickEvent) {
|
||||||
let entityID = this.dataset.entityID;
|
let entityID = this.dataset.entityID;
|
||||||
let selection = [entityID];
|
let selection = [entityID];
|
||||||
|
|
||||||
if (clickEvent.ctrlKey) {
|
if (clickEvent.ctrlKey) {
|
||||||
let selectedIndex = selectedEntities.indexOf(entityID);
|
let selectedIndex = selectedEntities.indexOf(entityID);
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
|
@ -221,9 +314,9 @@ function loaded() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) {
|
} else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) {
|
||||||
// if reselecting the same entity then deselect it
|
// if reselecting the same entity then start renaming it
|
||||||
if (selectedEntities[0] === entityID) {
|
if (selectedEntities[0] === entityID) {
|
||||||
selection = [];
|
startRenamingEntity(entityID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,6 +595,7 @@ function loaded() {
|
||||||
}
|
}
|
||||||
row.appendChild(column);
|
row.appendChild(column);
|
||||||
}
|
}
|
||||||
|
row.oncontextmenu = onRowContextMenu;
|
||||||
row.onclick = onRowClicked;
|
row.onclick = onRowClicked;
|
||||||
row.ondblclick = onRowDoubleClicked;
|
row.ondblclick = onRowDoubleClicked;
|
||||||
return row;
|
return row;
|
||||||
|
@ -672,8 +766,15 @@ function loaded() {
|
||||||
|
|
||||||
augmentSpinButtons();
|
augmentSpinButtons();
|
||||||
|
|
||||||
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
|
|
||||||
document.addEventListener("contextmenu", function (event) {
|
document.addEventListener("contextmenu", function (event) {
|
||||||
|
entityListContextMenu.close();
|
||||||
|
|
||||||
|
// Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
// close context menu when switching focus to another window
|
||||||
|
$(window).blur(function() {
|
||||||
|
entityListContextMenu.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
163
scripts/system/html/js/entityListContextMenu.js
Normal file
163
scripts/system/html/js/entityListContextMenu.js
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
//
|
||||||
|
// entityListContextMenu.js
|
||||||
|
//
|
||||||
|
// exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018.
|
||||||
|
// Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 2018
|
||||||
|
// Copyright 2018 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
/* eslint-env browser */
|
||||||
|
const CONTEXT_MENU_CLASS = "context-menu";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContextMenu class for EntityList
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function EntityListContextMenu() {
|
||||||
|
this._elContextMenu = null;
|
||||||
|
this._onSelectedCallback = null;
|
||||||
|
this._listItems = [];
|
||||||
|
this._initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityListContextMenu.prototype = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_elContextMenu: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onSelectedCallback: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_selectedEntityID: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_listItems: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the context menu
|
||||||
|
*/
|
||||||
|
close: function() {
|
||||||
|
if (this.isContextMenuOpen()) {
|
||||||
|
this._elContextMenu.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isContextMenuOpen: function() {
|
||||||
|
return this._elContextMenu.style.display === "block";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the context menu
|
||||||
|
* @param clickEvent
|
||||||
|
* @param selectedEntityID
|
||||||
|
* @param enabledOptions
|
||||||
|
*/
|
||||||
|
open: function(clickEvent, selectedEntityID, enabledOptions) {
|
||||||
|
this._selectedEntityID = selectedEntityID;
|
||||||
|
|
||||||
|
this._listItems.forEach(function(listItem) {
|
||||||
|
let enabled = enabledOptions.includes(listItem.label);
|
||||||
|
listItem.enabled = enabled;
|
||||||
|
listItem.element.setAttribute('class', enabled ? '' : 'disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
this._elContextMenu.style.display = "block";
|
||||||
|
this._elContextMenu.style.left
|
||||||
|
= Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px";
|
||||||
|
this._elContextMenu.style.top
|
||||||
|
= Math.min(clickEvent.pageY, document.body.clientHeight - this._elContextMenu.offsetHeight).toString() + "px";
|
||||||
|
clickEvent.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the callback for when a menu item is selected
|
||||||
|
* @param onSelectedCallback
|
||||||
|
*/
|
||||||
|
setOnSelectedCallback: function(onSelectedCallback) {
|
||||||
|
this._onSelectedCallback = onSelectedCallback;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a labeled item to the context menu
|
||||||
|
* @param itemLabel
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_addListItem: function(itemLabel) {
|
||||||
|
let elListItem = document.createElement("li");
|
||||||
|
elListItem.innerText = itemLabel;
|
||||||
|
|
||||||
|
let listItem = {
|
||||||
|
label: itemLabel,
|
||||||
|
element: elListItem,
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
elListItem.addEventListener("click", function () {
|
||||||
|
if (listItem.enabled && this._onSelectedCallback) {
|
||||||
|
this._onSelectedCallback.call(this, itemLabel, this._selectedEntityID);
|
||||||
|
}
|
||||||
|
}.bind(this), false);
|
||||||
|
|
||||||
|
elListItem.setAttribute('class', 'disabled');
|
||||||
|
|
||||||
|
this._listItems.push(listItem);
|
||||||
|
this._elContextMenu.appendChild(elListItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a separator item to the context menu
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_addListSeparator: function() {
|
||||||
|
let elListItem = document.createElement("li");
|
||||||
|
elListItem.setAttribute('class', 'separator');
|
||||||
|
this._elContextMenu.appendChild(elListItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the context menu.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initialize: function() {
|
||||||
|
this._elContextMenu = document.createElement("ul");
|
||||||
|
this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS);
|
||||||
|
document.body.appendChild(this._elContextMenu);
|
||||||
|
|
||||||
|
this._addListItem("Cut");
|
||||||
|
this._addListItem("Copy");
|
||||||
|
this._addListItem("Paste");
|
||||||
|
this._addListSeparator();
|
||||||
|
this._addListItem("Rename");
|
||||||
|
this._addListItem("Duplicate");
|
||||||
|
this._addListItem("Delete");
|
||||||
|
|
||||||
|
// Ignore clicks on context menu background or separator.
|
||||||
|
this._elContextMenu.addEventListener("click", function(event) {
|
||||||
|
// Sink clicks on context menu background or separator but let context menu item clicks through.
|
||||||
|
if (event.target.classList.contains(CONTEXT_MENU_CLASS)) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide means to close context menu without clicking menu item.
|
||||||
|
document.body.addEventListener("click", this.close.bind(this));
|
||||||
|
document.body.addEventListener("keydown", function(event) {
|
||||||
|
// Close context menu with Esc key.
|
||||||
|
if (this.isContextMenuOpen() && event.key === "Escape") {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
};
|
|
@ -15,7 +15,7 @@ var PROFILING_ENABLED = false;
|
||||||
var profileIndent = '';
|
var profileIndent = '';
|
||||||
const PROFILE_NOOP = function(_name, fn, args) {
|
const PROFILE_NOOP = function(_name, fn, args) {
|
||||||
fn.apply(this, args);
|
fn.apply(this, args);
|
||||||
} ;
|
};
|
||||||
PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
|
PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
|
||||||
console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
|
console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
|
||||||
var previousIndent = profileIndent;
|
var previousIndent = profileIndent;
|
||||||
|
@ -245,7 +245,7 @@ EntityListTool = function(shouldUseEditTabletApp) {
|
||||||
Window.saveAsync("Select Where to Save", "", "*.json");
|
Window.saveAsync("Select Where to Save", "", "*.json");
|
||||||
}
|
}
|
||||||
} else if (data.type === "pal") {
|
} else if (data.type === "pal") {
|
||||||
var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates.
|
var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates.
|
||||||
selectionManager.selections.forEach(function (id) {
|
selectionManager.selections.forEach(function (id) {
|
||||||
var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy;
|
var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy;
|
||||||
if (lastEditedBy) {
|
if (lastEditedBy) {
|
||||||
|
@ -271,6 +271,19 @@ EntityListTool = function(shouldUseEditTabletApp) {
|
||||||
filterInView = data.filterInView === true;
|
filterInView = data.filterInView === true;
|
||||||
} else if (data.type === "radius") {
|
} else if (data.type === "radius") {
|
||||||
searchRadius = data.radius;
|
searchRadius = data.radius;
|
||||||
|
} else if (data.type === "cut") {
|
||||||
|
SelectionManager.cutSelectedEntities();
|
||||||
|
} else if (data.type === "copy") {
|
||||||
|
SelectionManager.copySelectedEntities();
|
||||||
|
} else if (data.type === "paste") {
|
||||||
|
SelectionManager.pasteEntities();
|
||||||
|
} else if (data.type === "duplicate") {
|
||||||
|
SelectionManager.duplicateSelection();
|
||||||
|
that.sendUpdate();
|
||||||
|
} else if (data.type === "rename") {
|
||||||
|
Entities.editEntity(data.entityID, {name: data.name});
|
||||||
|
// make sure that the name also gets updated in the properties window
|
||||||
|
SelectionManager._update();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -353,12 +353,12 @@ SelectionManager = (function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdEntityIDs;
|
return createdEntityIDs;
|
||||||
}
|
};
|
||||||
|
|
||||||
that.cutSelectedEntities = function() {
|
that.cutSelectedEntities = function() {
|
||||||
copySelectedEntities();
|
that.copySelectedEntities();
|
||||||
deleteSelectedEntities();
|
deleteSelectedEntities();
|
||||||
}
|
};
|
||||||
|
|
||||||
that.copySelectedEntities = function() {
|
that.copySelectedEntities = function() {
|
||||||
var entityProperties = Entities.getMultipleEntityProperties(that.selections);
|
var entityProperties = Entities.getMultipleEntityProperties(that.selections);
|
||||||
|
@ -434,7 +434,7 @@ SelectionManager = (function() {
|
||||||
z: brn.z + entityClipboard.dimensions.z / 2
|
z: brn.z + entityClipboard.dimensions.z / 2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
that.pasteEntities = function() {
|
that.pasteEntities = function() {
|
||||||
var dimensions = entityClipboard.dimensions;
|
var dimensions = entityClipboard.dimensions;
|
||||||
|
@ -442,7 +442,7 @@ SelectionManager = (function() {
|
||||||
var pastePosition = getPositionToCreateEntity(maxDimension);
|
var pastePosition = getPositionToCreateEntity(maxDimension);
|
||||||
var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position);
|
var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position);
|
||||||
|
|
||||||
var copiedProperties = []
|
var copiedProperties = [];
|
||||||
var ids = [];
|
var ids = [];
|
||||||
entityClipboard.entities.forEach(function(originalProperties) {
|
entityClipboard.entities.forEach(function(originalProperties) {
|
||||||
var properties = deepCopy(originalProperties);
|
var properties = deepCopy(originalProperties);
|
||||||
|
@ -475,7 +475,7 @@ SelectionManager = (function() {
|
||||||
|
|
||||||
redo(copiedProperties);
|
redo(copiedProperties);
|
||||||
undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties);
|
undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties);
|
||||||
}
|
};
|
||||||
|
|
||||||
that._update = function(selectionUpdated) {
|
that._update = function(selectionUpdated) {
|
||||||
var properties = null;
|
var properties = null;
|
||||||
|
|
Loading…
Reference in a new issue