Merge pull request #14186 from thoys/feat/create/entityList-contextMenu

MS19056: [CreateApp/Entity List] Context Menu
This commit is contained in:
Brad Hefta-Gaub 2018-10-24 09:58:32 -07:00 committed by GitHub
commit 5e27a5930e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 331 additions and 13 deletions

View file

@ -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;
}

View file

@ -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">

View file

@ -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();
});
}

View 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));
}
};

View file

@ -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();
}
};

View file

@ -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;