mirror of
https://github.com/overte-org/overte.git
synced 2025-04-20 03:44:02 +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 {
|
||||
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/spinButtons.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>
|
||||
</head>
|
||||
<body onload='loaded();' id="entity-list-body">
|
||||
|
|
|
@ -70,6 +70,11 @@ var selectedEntities = [];
|
|||
|
||||
var entityList = null; // The ListView
|
||||
|
||||
/**
|
||||
* @type EntityListContextMenu
|
||||
*/
|
||||
var entityListContextMenu = null;
|
||||
|
||||
var currentSortColumn = 'type';
|
||||
var currentSortOrder = ASCENDING_SORT;
|
||||
var isFilterInView = false;
|
||||
|
@ -184,11 +189,99 @@ function loaded() {
|
|||
|
||||
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
|
||||
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) {
|
||||
let entityID = this.dataset.entityID;
|
||||
let selection = [entityID];
|
||||
|
||||
|
||||
if (clickEvent.ctrlKey) {
|
||||
let selectedIndex = selectedEntities.indexOf(entityID);
|
||||
if (selectedIndex >= 0) {
|
||||
|
@ -221,9 +314,9 @@ function loaded() {
|
|||
}
|
||||
}
|
||||
} 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) {
|
||||
selection = [];
|
||||
startRenamingEntity(entityID);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,6 +595,7 @@ function loaded() {
|
|||
}
|
||||
row.appendChild(column);
|
||||
}
|
||||
row.oncontextmenu = onRowContextMenu;
|
||||
row.onclick = onRowClicked;
|
||||
row.ondblclick = onRowDoubleClicked;
|
||||
return row;
|
||||
|
@ -672,8 +766,15 @@ function loaded() {
|
|||
|
||||
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) {
|
||||
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();
|
||||
}, 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 = '';
|
||||
const PROFILE_NOOP = function(_name, fn, args) {
|
||||
fn.apply(this, args);
|
||||
} ;
|
||||
};
|
||||
PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
|
||||
console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
|
||||
var previousIndent = profileIndent;
|
||||
|
@ -245,7 +245,7 @@ EntityListTool = function(shouldUseEditTabletApp) {
|
|||
Window.saveAsync("Select Where to Save", "", "*.json");
|
||||
}
|
||||
} 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) {
|
||||
var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy;
|
||||
if (lastEditedBy) {
|
||||
|
@ -271,6 +271,19 @@ EntityListTool = function(shouldUseEditTabletApp) {
|
|||
filterInView = data.filterInView === true;
|
||||
} else if (data.type === "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;
|
||||
}
|
||||
};
|
||||
|
||||
that.cutSelectedEntities = function() {
|
||||
copySelectedEntities();
|
||||
that.copySelectedEntities();
|
||||
deleteSelectedEntities();
|
||||
}
|
||||
};
|
||||
|
||||
that.copySelectedEntities = function() {
|
||||
var entityProperties = Entities.getMultipleEntityProperties(that.selections);
|
||||
|
@ -434,7 +434,7 @@ SelectionManager = (function() {
|
|||
z: brn.z + entityClipboard.dimensions.z / 2
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
that.pasteEntities = function() {
|
||||
var dimensions = entityClipboard.dimensions;
|
||||
|
@ -442,7 +442,7 @@ SelectionManager = (function() {
|
|||
var pastePosition = getPositionToCreateEntity(maxDimension);
|
||||
var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position);
|
||||
|
||||
var copiedProperties = []
|
||||
var copiedProperties = [];
|
||||
var ids = [];
|
||||
entityClipboard.entities.forEach(function(originalProperties) {
|
||||
var properties = deepCopy(originalProperties);
|
||||
|
@ -475,7 +475,7 @@ SelectionManager = (function() {
|
|||
|
||||
redo(copiedProperties);
|
||||
undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties);
|
||||
}
|
||||
};
|
||||
|
||||
that._update = function(selectionUpdated) {
|
||||
var properties = null;
|
||||
|
|
Loading…
Reference in a new issue