Migrate tool window to overlay/QML

This commit is contained in:
Brad Davis 2016-01-12 20:41:57 -08:00
parent e8adcd9f1f
commit b6272b7824
37 changed files with 2313 additions and 1694 deletions

View file

@ -1502,7 +1502,11 @@ PropertiesTool = function(opts) {
var that = {};
var url = Script.resolvePath('html/entityProperties.html');
var webView = new WebWindow('Entity Properties', url, 200, 280, true);
var webView = new OverlayWebWindow({
title: 'Entity Properties',
source: url,
toolWindow: true
});
var visible = false;

View file

@ -51,7 +51,7 @@ SettingsWindow = function() {
this.plankyStack = null;
this.webWindow = null;
this.init = function(plankyStack) {
_this.webWindow = new WebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true);
_this.webWindow = new OverlayWebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true);
_this.webWindow.setVisible(false);
_this.webWindow.eventBridge.webEventReceived.connect(_this.onWebEventReceived);
_this.plankyStack = plankyStack;

View file

@ -2,6 +2,8 @@
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="list.min.js"></script>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="eventBridgeLoader.js"></script>
<script>
var entities = {};
var selectedEntities = [];
@ -15,217 +17,219 @@
const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities.
function loaded() {
entityList = new List('entity-list', { valueNames: ['name', 'type', 'url'], page: MAX_ITEMS});
entityList.clear();
elEntityTable = document.getElementById("entity-table");
elEntityTableBody = document.getElementById("entity-table-body");
elRefresh = document.getElementById("refresh");
elDelete = document.getElementById("delete");
elTeleport = document.getElementById("teleport");
elRadius = document.getElementById("radius");
elNoEntitiesMessage = document.getElementById("no-entities");
elNoEntitiesRadius = document.getElementById("no-entities-radius");
document.getElementById("entity-name").onclick = function() {
setSortColumn('name');
};
document.getElementById("entity-type").onclick = function() {
setSortColumn('type');
};
document.getElementById("entity-url").onclick = function() {
setSortColumn('url');
};
function onRowClicked(clickEvent) {
var id = this.dataset.entityId;
var selection = [this.dataset.entityId];
if (clickEvent.ctrlKey) {
selection = selection.concat(selectedEntities);
} else if (clickEvent.shiftKey && selectedEntities.length > 0) {
var previousItemFound = -1;
var clickedItemFound = -1;
for (var entity in entityList.visibleItems) {
if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) {
clickedItemFound = entity;
} else if(previousItemFound === -1 && selectedEntities[0] == entityList.visibleItems[entity].values().id) {
previousItemFound = entity;
}
}
if (previousItemFound !== -1 && clickedItemFound !== -1) {
var betweenItems = [];
var toItem = Math.max(previousItemFound, clickedItemFound);
// skip first and last item in this loop, we add them to selection after the loop
for (var i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) {
entityList.visibleItems[i].elm.className = 'selected';
betweenItems.push(entityList.visibleItems[i].values().id);
}
if (previousItemFound > clickedItemFound) {
// always make sure that we add the items in the right order
betweenItems.reverse();
}
selection = selection.concat(betweenItems, selectedEntities);
}
}
selectedEntities = selection;
this.className = 'selected';
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: false,
entityIds: selection,
}));
}
function onRowDoubleClicked() {
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: true,
entityIds: [this.dataset.entityId],
}));
}
function addEntity(id, name, type, url) {
if (entities[id] === undefined) {
var urlParts = url.split('/');
var filename = urlParts[urlParts.length - 1];
entityList.add([{ id: id, name: name, type: type, url: filename }], function(items) {
var currentElement = items[0].elm;
var id = items[0]._values.id;
entities[id] = {
id: id,
name: name,
el: currentElement,
item: items[0],
};
currentElement.setAttribute('id', 'entity_' + id);
currentElement.setAttribute('title', url);
currentElement.dataset.entityId = id;
currentElement.onclick = onRowClicked;
currentElement.ondblclick = onRowDoubleClicked;
});
if (refreshEntityListTimer) {
clearTimeout(refreshEntityListTimer);
}
refreshEntityListTimer = setTimeout(refreshEntityListObject, 50);
} else {
var item = entities[id].item;
item.values({ name: name, url: url });
}
}
function clearEntities() {
entities = {};
entityList.clear();
}
var elSortOrder = {
name: document.querySelector('#entity-name .sort-order'),
type: document.querySelector('#entity-type .sort-order'),
url: document.querySelector('#entity-url .sort-order'),
}
function setSortColumn(column) {
if (currentSortColumn == column) {
currentSortOrder = currentSortOrder == "asc" ? "desc" : "asc";
} else {
elSortOrder[currentSortColumn].style.display = 'none';
elSortOrder[column].style.display = 'inline';
currentSortColumn = column;
currentSortOrder = "asc";
}
elSortOrder[column].innerHTML = currentSortOrder == "asc" ? ASCENDING_STRING : DESCENDING_STRING;
entityList.sort(currentSortColumn, { order: currentSortOrder });
}
function refreshEntities() {
clearEntities();
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
}
function refreshEntityListObject() {
refreshEntityListTimer = null;
entityList.sort(currentSortColumn, { order: currentSortOrder });
entityList.search(document.getElementById("filter").value);
}
function updateSelectedEntities(selectedEntities) {
var notFound = false;
for (var id in entities) {
entities[id].el.className = '';
}
for (var i = 0; i < selectedEntities.length; i++) {
var id = selectedEntities[i];
if (id in entities) {
var entity = entities[id];
entity.el.className = 'selected';
} else {
notFound = true;
}
}
return notFound;
}
elRefresh.onclick = function() {
refreshEntities();
}
elTeleport.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'teleport' }));
}
elDelete.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities();
}
document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") {
return;
}
var keyCode = keyDownEvent.keyCode;
if (keyCode === DELETE) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities();
}
}, false);
elRadius.onchange = function () {
elRadius.value = Math.max(elRadius.value, 0);
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
refreshEntities();
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
}
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type === "clearEntityList") {
clearEntities();
} else if (data.type == "selectionUpdate") {
var notFound = updateSelectedEntities(data.selectedIDs);
if (notFound) {
refreshEntities();
}
} else if (data.type == "update") {
var newEntities = data.entities;
if (newEntities.length == 0) {
elEntityTable.style.display = "none";
elNoEntitiesMessage.style.display = "block";
} else {
elEntityTable.style.display = "table";
elNoEntitiesMessage.style.display = "none";
for (var i = 0; i < newEntities.length; i++) {
var id = newEntities[i].id;
addEntity(id, newEntities[i].name, newEntities[i].type, newEntities[i].url);
}
updateSelectedEntities(data.selectedIDs);
}
}
});
setTimeout(refreshEntities, 1000);
}
openEventBridge(function() {
entityList = new List('entity-list', { valueNames: ['name', 'type', 'url'], page: MAX_ITEMS});
entityList.clear();
elEntityTable = document.getElementById("entity-table");
elEntityTableBody = document.getElementById("entity-table-body");
elRefresh = document.getElementById("refresh");
elDelete = document.getElementById("delete");
elTeleport = document.getElementById("teleport");
elRadius = document.getElementById("radius");
elNoEntitiesMessage = document.getElementById("no-entities");
elNoEntitiesRadius = document.getElementById("no-entities-radius");
document.getElementById("entity-name").onclick = function() {
setSortColumn('name');
};
document.getElementById("entity-type").onclick = function() {
setSortColumn('type');
};
document.getElementById("entity-url").onclick = function() {
setSortColumn('url');
};
function onRowClicked(clickEvent) {
var id = this.dataset.entityId;
var selection = [this.dataset.entityId];
if (clickEvent.ctrlKey) {
selection = selection.concat(selectedEntities);
} else if (clickEvent.shiftKey && selectedEntities.length > 0) {
var previousItemFound = -1;
var clickedItemFound = -1;
for (var entity in entityList.visibleItems) {
if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) {
clickedItemFound = entity;
} else if(previousItemFound === -1 && selectedEntities[0] == entityList.visibleItems[entity].values().id) {
previousItemFound = entity;
}
}
if (previousItemFound !== -1 && clickedItemFound !== -1) {
var betweenItems = [];
var toItem = Math.max(previousItemFound, clickedItemFound);
// skip first and last item in this loop, we add them to selection after the loop
for (var i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) {
entityList.visibleItems[i].elm.className = 'selected';
betweenItems.push(entityList.visibleItems[i].values().id);
}
if (previousItemFound > clickedItemFound) {
// always make sure that we add the items in the right order
betweenItems.reverse();
}
selection = selection.concat(betweenItems, selectedEntities);
}
}
selectedEntities = selection;
this.className = 'selected';
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: false,
entityIds: selection,
}));
}
function onRowDoubleClicked() {
EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate",
focus: true,
entityIds: [this.dataset.entityId],
}));
}
function addEntity(id, name, type, url) {
if (entities[id] === undefined) {
var urlParts = url.split('/');
var filename = urlParts[urlParts.length - 1];
entityList.add([{ id: id, name: name, type: type, url: filename }], function(items) {
var currentElement = items[0].elm;
var id = items[0]._values.id;
entities[id] = {
id: id,
name: name,
el: currentElement,
item: items[0],
};
currentElement.setAttribute('id', 'entity_' + id);
currentElement.setAttribute('title', url);
currentElement.dataset.entityId = id;
currentElement.onclick = onRowClicked;
currentElement.ondblclick = onRowDoubleClicked;
});
if (refreshEntityListTimer) {
clearTimeout(refreshEntityListTimer);
}
refreshEntityListTimer = setTimeout(refreshEntityListObject, 50);
} else {
var item = entities[id].item;
item.values({ name: name, url: url });
}
}
function clearEntities() {
entities = {};
entityList.clear();
}
var elSortOrder = {
name: document.querySelector('#entity-name .sort-order'),
type: document.querySelector('#entity-type .sort-order'),
url: document.querySelector('#entity-url .sort-order'),
}
function setSortColumn(column) {
if (currentSortColumn == column) {
currentSortOrder = currentSortOrder == "asc" ? "desc" : "asc";
} else {
elSortOrder[currentSortColumn].style.display = 'none';
elSortOrder[column].style.display = 'inline';
currentSortColumn = column;
currentSortOrder = "asc";
}
elSortOrder[column].innerHTML = currentSortOrder == "asc" ? ASCENDING_STRING : DESCENDING_STRING;
entityList.sort(currentSortColumn, { order: currentSortOrder });
}
function refreshEntities() {
clearEntities();
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
}
function refreshEntityListObject() {
refreshEntityListTimer = null;
entityList.sort(currentSortColumn, { order: currentSortOrder });
entityList.search(document.getElementById("filter").value);
}
function updateSelectedEntities(selectedEntities) {
var notFound = false;
for (var id in entities) {
entities[id].el.className = '';
}
for (var i = 0; i < selectedEntities.length; i++) {
var id = selectedEntities[i];
if (id in entities) {
var entity = entities[id];
entity.el.className = 'selected';
} else {
notFound = true;
}
}
return notFound;
}
elRefresh.onclick = function() {
refreshEntities();
}
elTeleport.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'teleport' }));
}
elDelete.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities();
}
document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") {
return;
}
var keyCode = keyDownEvent.keyCode;
if (keyCode === DELETE) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities();
}
}, false);
elRadius.onchange = function () {
elRadius.value = Math.max(elRadius.value, 0);
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
refreshEntities();
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
}
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type === "clearEntityList") {
clearEntities();
} else if (data.type == "selectionUpdate") {
var notFound = updateSelectedEntities(data.selectedIDs);
if (notFound) {
refreshEntities();
}
} else if (data.type == "update") {
var newEntities = data.entities;
if (newEntities.length == 0) {
elEntityTable.style.display = "none";
elNoEntitiesMessage.style.display = "block";
} else {
elEntityTable.style.display = "table";
elNoEntitiesMessage.style.display = "none";
for (var i = 0; i < newEntities.length; i++) {
var id = newEntities[i].id;
addEntity(id, newEntities[i].name, newEntities[i].type, newEntities[i].url);
}
updateSelectedEntities(data.selectedIDs);
}
}
});
setTimeout(refreshEntities, 1000);
}
});
}
</script>
</head>

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,8 @@
// void scriptEventReceived(const QString& data);
//
var EventBridge;
EventBridgeConnectionProxy = function(parent) {
this.parent = parent;
this.realSignal = this.parent.realBridge.scriptEventReceived
@ -46,12 +48,10 @@ openEventBridge = function(callback) {
socket.onopen = function() {
channel = new QWebChannel(socket, function(channel) {
console.log("Document url is " + document.URL);
for(var key in channel.objects){
console.log("registered object: " + key);
}
var webWindow = channel.objects[document.URL.toLowerCase()];
console.log("WebWindow is " + webWindow)
eventBridgeProxy = new EventBridgeProxy(webWindow);
EventBridge = eventBridgeProxy;
if (callback) { callback(eventBridgeProxy); }
});
}

View file

@ -1,110 +1,114 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="eventBridgeLoader.js"></script>
<script>
function loaded() {
var gridColor = { red: 0, green: 0, blue: 0 };
var gridColors = [
{ red: 0, green: 0, blue: 0 },
{ red: 255, green: 255, blue: 255 },
{ red: 255, green: 0, blue: 0 },
{ red: 0, green: 255, blue: 0},
{ red: 0, green: 0, blue: 255 },
];
var gridColorIndex = 0;
elPosY = document.getElementById("horiz-y");
elMinorSpacing = document.getElementById("minor-spacing");
elMajorSpacing = document.getElementById("major-spacing");
elSnapToGrid = document.getElementById("snap-to-grid");
elHorizontalGridVisible = document.getElementById("horiz-grid-visible");
elMoveToSelection = document.getElementById("move-to-selection");
elMoveToAvatar = document.getElementById("move-to-avatar");
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.origin) {
var origin = data.origin;
elPosY.value = origin.y.toFixed(2);
openEventBridge(function() {
var gridColor = { red: 0, green: 0, blue: 0 };
var gridColors = [
{ red: 0, green: 0, blue: 0 },
{ red: 255, green: 255, blue: 255 },
{ red: 255, green: 0, blue: 0 },
{ red: 0, green: 255, blue: 0},
{ red: 0, green: 0, blue: 255 },
];
var gridColorIndex = 0;
elPosY = document.getElementById("horiz-y");
elMinorSpacing = document.getElementById("minor-spacing");
elMajorSpacing = document.getElementById("major-spacing");
elSnapToGrid = document.getElementById("snap-to-grid");
elHorizontalGridVisible = document.getElementById("horiz-grid-visible");
elMoveToSelection = document.getElementById("move-to-selection");
elMoveToAvatar = document.getElementById("move-to-avatar");
if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.origin) {
var origin = data.origin;
elPosY.value = origin.y.toFixed(2);
}
if (data.minorGridWidth !== undefined) {
elMinorSpacing.value = data.minorGridWidth;
}
if (data.majorGridEvery !== undefined) {
elMajorSpacing.value = data.majorGridEvery;
}
if (data.gridColor) {
gridColor = data.gridColor;
}
if (data.snapToGrid !== undefined) {
elSnapToGrid.checked = data.snapToGrid == true;
}
if (data.visible !== undefined) {
elHorizontalGridVisible.checked = data.visible == true;
}
});
function emitUpdate() {
EventBridge.emitWebEvent(JSON.stringify({
type: "update",
origin: {
y: elPosY.value,
},
minorGridWidth: elMinorSpacing.value,
majorGridEvery: elMajorSpacing.value,
gridColor: gridColor,
colorIndex: gridColorIndex,
snapToGrid: elSnapToGrid.checked,
visible: elHorizontalGridVisible.checked,
}));
}
if (data.minorGridWidth !== undefined) {
elMinorSpacing.value = data.minorGridWidth;
}
if (data.majorGridEvery !== undefined) {
elMajorSpacing.value = data.majorGridEvery;
}
if (data.gridColor) {
gridColor = data.gridColor;
}
if (data.snapToGrid !== undefined) {
elSnapToGrid.checked = data.snapToGrid == true;
}
if (data.visible !== undefined) {
elHorizontalGridVisible.checked = data.visible == true;
}
});
function emitUpdate() {
EventBridge.emitWebEvent(JSON.stringify({
type: "update",
origin: {
y: elPosY.value,
},
minorGridWidth: elMinorSpacing.value,
majorGridEvery: elMajorSpacing.value,
gridColor: gridColor,
colorIndex: gridColorIndex,
snapToGrid: elSnapToGrid.checked,
visible: elHorizontalGridVisible.checked,
}));
}
}
elPosY.addEventListener("change", emitUpdate);
elMinorSpacing.addEventListener("change", emitUpdate);
elMajorSpacing.addEventListener("change", emitUpdate);
elSnapToGrid.addEventListener("change", emitUpdate);
elHorizontalGridVisible.addEventListener("change", emitUpdate);
elMoveToAvatar.addEventListener("click", function() {
EventBridge.emitWebEvent(JSON.stringify({
type: "action",
action: "moveToAvatar",
}));
elPosY.addEventListener("change", emitUpdate);
elMinorSpacing.addEventListener("change", emitUpdate);
elMajorSpacing.addEventListener("change", emitUpdate);
elSnapToGrid.addEventListener("change", emitUpdate);
elHorizontalGridVisible.addEventListener("change", emitUpdate);
elMoveToAvatar.addEventListener("click", function() {
EventBridge.emitWebEvent(JSON.stringify({
type: "action",
action: "moveToAvatar",
}));
});
elMoveToSelection.addEventListener("click", function() {
EventBridge.emitWebEvent(JSON.stringify({
type: "action",
action: "moveToSelection",
}));
});
var gridColorBox = document.getElementById('grid-color');
for (var i = 0; i < gridColors.length; i++) {
var colors = gridColors[i];
var box = document.createElement('div');
box.setAttribute('class', 'color-box');
box.style.background = 'rgb(' + colors.red + ', ' + colors.green + ', ' + colors.blue + ')';
document.getElementById("grid-colors").appendChild(box);
box.addEventListener("click", function(color, index) {
return function() {
gridColor = color;
gridColorIndex = index;
emitUpdate();
}
}({ red: colors.red, green: colors.green, blue: colors.blue }, i));
}
EventBridge.emitWebEvent(JSON.stringify({ type: 'init' }));
});
elMoveToSelection.addEventListener("click", function() {
EventBridge.emitWebEvent(JSON.stringify({
type: "action",
action: "moveToSelection",
}));
});
var gridColorBox = document.getElementById('grid-color');
for (var i = 0; i < gridColors.length; i++) {
var colors = gridColors[i];
var box = document.createElement('div');
box.setAttribute('class', 'color-box');
box.style.background = 'rgb(' + colors.red + ', ' + colors.green + ', ' + colors.blue + ')';
document.getElementById("grid-colors").appendChild(box);
box.addEventListener("click", function(color, index) {
return function() {
gridColor = color;
gridColorIndex = index;
emitUpdate();
}
}({ red: colors.red, green: colors.green, blue: colors.blue }, i));
}
EventBridge.emitWebEvent(JSON.stringify({ type: 'init' }));
}
</script>
</head>

View file

@ -4,7 +4,9 @@ EntityListTool = function(opts) {
var that = {};
var url = ENTITY_LIST_HTML_URL;
var webView = new WebWindow('Entities', url, 200, 280, true);
var webView = new OverlayWebWindow({
title: 'Entities', source: url, toolWindow: true
});
var searchRadius = 100;

View file

@ -231,7 +231,9 @@ GridTool = function(opts) {
var listeners = [];
var url = GRID_CONTROLS_HTML_URL;
var webView = new WebWindow('Grid', url, 200, 280, true);
var webView = new OverlayWebWindow({
title: 'Grid', source: url, toolWindow: true
});
horizontalGrid.addListener(function(data) {
webView.eventBridge.emitScriptEvent(JSON.stringify(data));

View file

@ -10,18 +10,22 @@ function findChild(item, name) {
return null;
}
function findParent(item, name) {
function findParentMatching(item, predicate) {
while (item) {
if (item.objectName === name) {
return item;
if (predicate(item)) {
break;
}
item = item.parent;
}
return null;
return item;
}
function getDesktop(item) {
return findParent(item, OFFSCREEN_ROOT_OBJECT_NAME);
function findParentByName(item, name) {
return findParentMatching(item, function(item) {
var testName = name;
var result = (item.name === testName);
return result;
});
}
function findRootMenu(item) {
@ -29,6 +33,13 @@ function findRootMenu(item) {
return item ? item.rootMenu : null;
}
function isDesktop(item) {
return item.desktopRoot;
}
function isTopLevelWindow(item) {
return item.topLevelWindow;
}
function getTopLevelWindows(item) {
var desktop = getDesktop(item);
@ -40,8 +51,7 @@ function getTopLevelWindows(item) {
for (var i = 0; i < desktop.children.length; ++i) {
var child = desktop.children[i];
if ((Global.OFFSCREEN_WINDOW_OBJECT_NAME === child.objectName) ||
child[Global.OFFSCREEN_WINDOW_OBJECT_NAME]) {
if (isTopLevelWindow(child)) {
var windowId = child.toString();
currentWindows.push(child)
}
@ -50,9 +60,12 @@ function getTopLevelWindows(item) {
}
function getDesktop(item) {
return findParentMatching(item, isDesktop);
}
function getDesktopWindow(item) {
item = findParent(item, OFFSCREEN_WINDOW_OBJECT_NAME);
return item;
return findParentMatching(item, isTopLevelWindow)
}
function closeWindow(item) {
@ -142,7 +155,7 @@ function raiseWindow(item) {
var desktop = getDesktop(targetWindow);
if (!desktop) {
//console.warn("Could not find desktop for window " + targetWindow);
console.warn("Could not find desktop for window " + targetWindow);
return;
}

View file

@ -2,86 +2,26 @@ import QtQuick 2.3
import QtQuick.Controls 1.2
import QtWebEngine 1.1
import "controls"
import "windows" as Windows
import "controls" as Controls
import "styles"
VrDialog {
Windows.Window {
id: root
HifiConstants { id: hifi }
title: "WebWindow"
resizable: true
enabled: false
visible: false
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false
contentImplicitWidth: clientArea.implicitWidth
contentImplicitHeight: clientArea.implicitHeight
backgroundColor: "#7f000000"
property url source: "about:blank"
property alias source: webview.url
signal navigating(string url)
function stop() {
webview.stop();
function raiseWindow() { Desktop.raise(root) }
Controls.WebView {
id: webview
url: "about:blank"
anchors.fill: parent
focus: true
}
Component.onCompleted: {
// Ensure the JS from the web-engine makes it to our logging
webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message);
});
}
Item {
id: clientArea
implicitHeight: 600
implicitWidth: 800
x: root.clientX
y: root.clientY
width: root.clientWidth
height: root.clientHeight
WebEngineView {
id: webview
url: root.source
anchors.fill: parent
focus: true
property var originalUrl
property var lastFixupTime: 0
onUrlChanged: {
var currentUrl = url.toString();
var newUrl = urlHandler.fixupUrl(currentUrl).toString();
if (newUrl != currentUrl) {
var now = new Date().valueOf();
if (url === originalUrl && (now - lastFixupTime < 100)) {
console.warn("URL fixup loop detected")
return;
}
originalUrl = url
lastFixupTime = now
url = newUrl;
}
}
onLoadingChanged: {
// Required to support clicking on "hifi://" links
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
var url = loadRequest.url.toString();
if (urlHandler.canHandleUrl(url)) {
if (urlHandler.handleUrl(url)) {
webview.stop();
}
}
}
}
profile: WebEngineProfile {
id: webviewProfile
httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)"
storageName: "qmlWebEngine"
}
}
} // item
} // dialog

View file

@ -5,52 +5,32 @@ import QtWebChannel 1.0
import QtWebSockets 1.0
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
import "Global.js" as Global
import "windows" as Windows
import "controls"
import "styles"
VrDialog {
Windows.Window {
id: root
HifiConstants { id: hifi }
title: "QmlWindow"
resizable: true
enabled: false
visible: false
focus: true
property var channel;
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false
contentImplicitWidth: clientArea.implicitWidth
contentImplicitHeight: clientArea.implicitHeight
property alias source: pageLoader.source
function raiseWindow() {
Global.raiseWindow(root)
}
function raiseWindow() { Desktop.raise(root) }
Item {
id: clientArea
implicitHeight: 600
implicitWidth: 800
x: root.clientX
y: root.clientY
width: root.clientWidth
height: root.clientHeight
Loader {
id: pageLoader
objectName: "Loader"
focus: true
clip: true
Loader {
id: pageLoader
objectName: "Loader"
anchors.fill: parent
focus: true
property var dialog: root
property var dialog: root
Keys.onPressed: {
console.log("QmlWindow pageLoader keypress")
}
Keys.onPressed: {
console.log("QmlWindow pageLoader keypress")
}
} // item
}
} // dialog

View file

@ -7,22 +7,25 @@ import "Global.js" as Global
// windows will be childed.
Item {
id: desktop
objectName: Global.OFFSCREEN_ROOT_OBJECT_NAME
anchors.fill: parent;
onParentChanged: forceActiveFocus();
// Allows QML/JS to find the desktop through the parent chain
property bool desktopRoot: true
// The VR version of the primary menu
property var rootMenu: Menu { objectName: "rootMenu" }
// List of all top level windows
property var windows: [];
property var rootMenu: Menu {
objectName: "rootMenu"
}
onChildrenChanged: windows = Global.getTopLevelWindows(desktop);
onChildrenChanged: {
windows = Global.getTopLevelWindows(desktop);
}
onParentChanged: {
forceActiveFocus();
}
// The tool window, one instance
property alias toolWindow: toolWindow
ToolWindow { id: toolWindow }
function raise(item) {
Global.raiseWindow(item);
}
}

View file

@ -0,0 +1,126 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtWebEngine 1.1
import Qt.labs.settings 1.0
import "windows" as Windows
import "controls" as Controls
Windows.Window {
id: toolWindow
resizable: true
objectName: "ToolWindow"
destroyOnCloseButton: false
destroyOnInvisible: false
visible: false
property string newTabSource
property alias tabView: tabView
onParentChanged: {
x = desktop.width / 2 - width / 2;
y = desktop.height / 2 - height / 2;
}
Settings {
category: "ToolWindow.Position"
property alias x: toolWindow.x
property alias y: toolWindow.y
}
property var webTabCreator: Component {
Controls.WebView {
id: webView
property string originalUrl;
// Both toolWindow.newTabSource and url can change, so we need
// to store the original url here, without creating any bindings
Component.onCompleted: {
originalUrl = toolWindow.newTabSource;
url = originalUrl;
}
}
}
TabView {
id: tabView; width: 384; height: 640;
onCountChanged: {
if (0 == count) {
toolWindow.visible = false
}
}
}
function updateVisiblity() {
var newVisible = false
console.log("Checking " + tabView.count + " children")
for (var i = 0; i < tabView.count; ++i) {
if (tabView.getTab(i).enabled) {
console.log("Tab " + i + " enabled");
newVisible = true;
break;
}
}
console.log("Setting toolWindow visible: " + newVisible);
visible = newVisible
}
function findIndexForUrl(source) {
for (var i = 0; i < tabView.count; ++i) {
var tab = tabView.getTab(i);
if (tab && tab.item && tab.item.originalUrl &&
tab.item.originalUrl === source) {
return i;
}
}
return -1;
}
function removeTabForUrl(source) {
var index = findIndexForUrl(source);
if (index < 0) {
console.warn("Could not find tab for " + source);
return;
}
tabView.removeTab(index);
console.log("Updating visibility based on child tab removed");
updateVisiblity();
}
function addWebTab(properties) {
if (!properties.source) {
console.warn("Attempted to open Web Tool Pane without URl")
return;
}
var existingTabIndex = findIndexForUrl(properties.source);
if (existingTabIndex >= 0) {
console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source)
return tabView.getTab(existingTabIndex);
}
var title = properties.title || "Unknown";
newTabSource = properties.source;
console.log(typeof(properties.source))
var newTab = tabView.addTab(title, webTabCreator);
newTab.active = true;
newTab.enabled = false;
if (properties.width) {
tabView.width = Math.min(Math.max(tabView.width, properties.width),
toolWindow.maxSize.x);
}
if (properties.height) {
tabView.height = Math.min(Math.max(tabView.height, properties.height),
toolWindow.maxSize.y);
}
console.log("Updating visibility based on child tab added");
newTab.enabledChanged.connect(function() {
console.log("Updating visibility based on child tab enabled change");
updateVisiblity();
})
updateVisiblity();
return newTab
}
}

View file

@ -3,8 +3,6 @@ import QtQuick.Controls 1.2
import "."
import "../styles"
import "../Global.js" as Global
/*
* FIXME Need to create a client property here so that objects can be
* placed in it without having to think about positioning within the outer
@ -48,7 +46,7 @@ DialogBase {
if (enabled) {
visible = true;
if (root.parent) {
Global.raiseWindow(root);
Desktop.raise(root);
}
}
}

View file

@ -0,0 +1,50 @@
import QtQuick 2.5
import QtWebEngine 1.1
WebEngineView {
id: root
property var originalUrl
property int lastFixupTime: 0
Component.onCompleted: {
console.log("Connecting JS messaging to Hifi Logging")
// Ensure the JS from the web-engine makes it to our logging
root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message);
});
}
onUrlChanged: {
var currentUrl = url.toString();
var newUrl = urlHandler.fixupUrl(currentUrl).toString();
if (newUrl != currentUrl) {
var now = new Date().valueOf();
if (url === originalUrl && (now - lastFixupTime < 100)) {
console.warn("URL fixup loop detected")
return;
}
originalUrl = url
lastFixupTime = now
url = newUrl;
}
}
onLoadingChanged: {
// Required to support clicking on "hifi://" links
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
var url = loadRequest.url.toString();
if (urlHandler.canHandleUrl(url)) {
if (urlHandler.handleUrl(url)) {
root.stop();
}
}
}
}
profile: WebEngineProfile {
id: webviewProfile
httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)"
storageName: "qmlWebEngine"
}
}

View file

@ -0,0 +1,232 @@
import QtQuick 2.0
import QtQuick.Controls 1.4
import Qt.labs.folderlistmodel 2.1
import Qt.labs.settings 1.0
import ".."
import "."
// Work in progress....
DialogBase {
id: root
Constants { id: vr }
property string settingsName: ""
signal selectedFile(var file);
signal canceled();
function selectCurrentFile() {
var row = tableView.currentRow
console.log("Selecting row " + row)
var fileName = folderModel.get(row, "fileName");
if (fileName === "..") {
folderModel.folder = folderModel.parentFolder
} else if (folderModel.isFolder(row)) {
folderModel.folder = folderModel.get(row, "fileURL");
} else {
selectedFile(folderModel.get(row, "fileURL"));
enabled = false
}
}
property var folderModel: FolderListModel {
id: folderModel
showDotAndDotDot: true
showDirsFirst: true
folder: "file:///C:/";
onFolderChanged: tableView.currentRow = 0
}
property var filterModel: ListModel {
ListElement {
text: "All Files (*.*)"
filter: "*"
}
ListElement {
text: "Javascript Files (*.js)"
filter: "*.js"
}
ListElement {
text: "QML Files (*.qml)"
filter: "*.qml"
}
}
Settings {
// fixme, combine with a property to allow different saved locations
category: "FileOpenLastFolder." + settingsName
property alias folder: folderModel.folder
}
Rectangle {
id: currentDirectoryWrapper
anchors.left: parent.left
anchors.leftMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
anchors.top: parent.top
anchors.topMargin: 8
height: currentDirectory.implicitHeight + 8
radius: vr.styles.radius
color: vr.controls.colors.inputBackground
TextEdit {
enabled: false
id: currentDirectory
text: folderModel.folder
anchors {
fill: parent
leftMargin: 8
rightMargin: 8
topMargin: 4
bottomMargin: 4
}
}
}
TableView {
id: tableView
focus: true
model: folderModel
anchors.top: currentDirectoryWrapper.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: selectionType.top
anchors.margins: 8
itemDelegate: Item {
Text {
anchors.verticalCenter: parent.verticalCenter
font.family: vr.fonts.lightFontName
font.pointSize: 12
color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor
elide: styleData.elideMode
text: getText();
function getText() {
switch (styleData.column) {
//case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss");
case 2: return folderModel.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value);
default: return styleData.value;
}
}
function formatSize(size) {
var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ];
var suffixIndex = 0
while ((size / 1024.0) > 1.1) {
size /= 1024.0;
++suffixIndex;
}
size = Math.round(size*1000)/1000;
size = size.toLocaleString()
return size + " " + suffixes[suffixIndex];
}
}
}
TableViewColumn {
role: "fileName"
title: "Name"
width: 400
}
TableViewColumn {
role: "fileModified"
title: "Date Modified"
width: 200
}
TableViewColumn {
role: "fileSize"
title: "Size"
width: 200
}
function selectItem(row) {
selectCurrentFile()
}
Keys.onReturnPressed: selectCurrentFile();
onDoubleClicked: { currentRow = row; selectCurrentFile(); }
onCurrentRowChanged: currentSelection.text = model.get(currentRow, "fileName");
KeyNavigation.left: cancelButton
KeyNavigation.right: selectionType
}
Rectangle {
anchors.right: selectionType.left
anchors.rightMargin: 8
anchors.top: selectionType.top
anchors.bottom: selectionType.bottom
anchors.left: parent.left
anchors.leftMargin: 8
radius: 8
color: "#eee"
Text {
id: currentSelection
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
anchors.verticalCenter: parent.verticalCenter
font.family: vr.fonts.mainFontName
font.pointSize: 18
}
}
ComboBox {
id: selectionType
anchors.bottom: buttonRow.top
anchors.bottomMargin: 8
anchors.right: parent.right
anchors.rightMargin: 8
anchors.left: buttonRow.left
model: filterModel
onCurrentIndexChanged: folderModel.nameFilters = [
filterModel.get(currentIndex).filter
]
KeyNavigation.left: tableView
KeyNavigation.right: openButton
}
Row {
id: buttonRow
anchors.right: parent.right
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
layoutDirection: Qt.RightToLeft
spacing: 8
Button {
id: cancelButton
text: "Cancel"
KeyNavigation.up: selectionType
KeyNavigation.left: openButton
KeyNavigation.right: tableView.contentItem
Keys.onReturnPressed: { canceled(); root.enabled = false }
onClicked: { canceled(); root.enabled = false }
}
Button {
id: openButton
text: "Open"
enabled: tableView.currentRow != -1 && !folderModel.get(tableView.currentRow, "fileIsDir")
KeyNavigation.up: selectionType
KeyNavigation.left: selectionType
KeyNavigation.right: cancelButton
onClicked: selectCurrentFile();
Keys.onReturnPressed: selectedFile(folderModel.get(tableView.currentRow, "fileURL"))
}
}
Keys.onPressed: {
if (event.key === Qt.Key_Backspace && folderModel.parentFolder && folderModel.parentFolder != "") {
console.log("Navigating to " + folderModel.parentFolder)
folderModel.folder = folderModel.parentFolder
event.accepted = true
}
}
}

View file

@ -0,0 +1,311 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
// Stubs for the global service objects set by Interface.cpp when creating the UI
// This is useful for testing inside Qt creator where these services don't actually exist.
Item {
Item {
objectName: "urlHandler"
function fixupUrl(url) { return url; }
function canHandleUrl(url) { return false; }
function handleUrl(url) { return true; }
}
Item {
objectName: "Account"
function isLoggedIn() { return true; }
function getUsername() { return "Jherico"; }
}
Item {
objectName: "ScriptDiscoveryService"
property var scriptsModelFilter: scriptsModel
signal scriptCountChanged()
property var _runningScripts:[
{ name: "wireFrameTest.js", url: "foo/wireframetest.js", path: "foo/wireframetest.js", local: true },
{ name: "edit.js", url: "foo/edit.js", path: "foo/edit.js", local: false },
{ name: "listAllScripts.js", url: "foo/listAllScripts.js", path: "foo/listAllScripts.js", local: false },
{ name: "users.js", url: "foo/users.js", path: "foo/users.js", local: false },
]
function getRunning() {
return _runningScripts;
}
}
Item {
id: menuHelper
objectName: "MenuHelper"
Component {
id: modelMaker
ListModel { }
}
function toModel(menu, parent) {
if (!parent) { parent = menuHelper }
var result = modelMaker.createObject(parent);
if (menu.type !== MenuItemType.Menu) {
console.warn("Not a menu: " + menu);
return result;
}
var items = menu.items;
for (var i = 0; i < items.length; ++i) {
var item = items[i];
switch (item.type) {
case 2:
result.append({"name": item.title, "item": item})
break;
case 1:
result.append({"name": item.text, "item": item})
break;
case 0:
result.append({"name": "", "item": item})
break;
}
}
return result;
}
}
Item {
objectName: "Desktop"
property string _OFFSCREEN_ROOT_OBJECT_NAME: "desktopRoot";
property string _OFFSCREEN_DIALOG_OBJECT_NAME: "topLevelWindow";
function findChild(item, name) {
for (var i = 0; i < item.children.length; ++i) {
if (item.children[i].objectName === name) {
return item.children[i];
}
}
return null;
}
function findParent(item, name) {
while (item) {
if (item.objectName === name) {
return item;
}
item = item.parent;
}
return null;
}
function findDialog(item) {
item = findParent(item, _OFFSCREEN_DIALOG_OBJECT_NAME);
return item;
}
function closeDialog(item) {
item = findDialog(item);
if (item) {
item.enabled = false
} else {
console.warn("Could not find top level dialog")
}
}
function getDesktop(item) {
while (item) {
if (item.desktopRoot) {
break;
}
item = item.parent;
}
return item
}
function raise(item) {
var desktop = getDesktop(item);
if (desktop) {
desktop.raise(item);
}
}
}
Menu {
id: root
objectName: "rootMenu"
Menu {
title: "Audio"
}
Menu {
title: "Avatar"
}
Menu {
title: "Display"
ExclusiveGroup { id: displayMode }
Menu {
title: "More Stuff"
Menu { title: "Empty" }
MenuItem {
text: "Do Nothing"
onTriggered: console.log("Nothing")
}
}
MenuItem {
text: "Oculus"
exclusiveGroup: displayMode
checkable: true
}
MenuItem {
text: "OpenVR"
exclusiveGroup: displayMode
checkable: true
}
MenuItem {
text: "OSVR"
exclusiveGroup: displayMode
checkable: true
}
MenuItem {
text: "2D Screen"
exclusiveGroup: displayMode
checkable: true
checked: true
}
MenuItem {
text: "3D Screen (Active)"
exclusiveGroup: displayMode
checkable: true
}
MenuItem {
text: "3D Screen (Passive)"
exclusiveGroup: displayMode
checkable: true
}
}
Menu {
title: "View"
Menu {
title: "Camera Mode"
ExclusiveGroup { id: cameraMode }
MenuItem {
exclusiveGroup: cameraMode
text: "First Person";
onTriggered: console.log(text + " checked " + checked)
checkable: true
checked: true
}
MenuItem {
exclusiveGroup: cameraMode
text: "Third Person";
onTriggered: console.log(text)
checkable: true
}
MenuItem {
exclusiveGroup: cameraMode
text: "Independent Mode";
onTriggered: console.log(text)
checkable: true
}
MenuItem {
exclusiveGroup: cameraMode
text: "Entity Mode";
onTriggered: console.log(text)
enabled: false
checkable: true
}
MenuItem {
exclusiveGroup: cameraMode
text: "Fullscreen Mirror";
onTriggered: console.log(text)
checkable: true
}
}
}
Menu {
title: "Edit"
MenuItem {
text: "Undo"
shortcut: "Ctrl+Z"
enabled: false
onTriggered: console.log(text)
}
MenuItem {
text: "Redo"
shortcut: "Ctrl+Shift+Z"
enabled: false
onTriggered: console.log(text)
}
MenuSeparator { }
MenuItem {
text: "Cut"
shortcut: "Ctrl+X"
onTriggered: console.log(text)
}
MenuItem {
text: "Copy"
shortcut: "Ctrl+C"
onTriggered: console.log(text)
}
MenuItem {
text: "Paste"
shortcut: "Ctrl+V"
visible: false
onTriggered: console.log("Paste")
}
}
Menu {
title: "Navigate"
}
Menu {
title: "Market"
}
Menu {
title: "Settings"
}
Menu {
title: "Developer"
}
Menu {
title: "Quit"
}
Menu {
title: "File"
Action {
id: login
text: "Login"
}
Action {
id: quit
text: "Quit"
shortcut: "Ctrl+Q"
onTriggered: Qt.quit();
}
MenuItem { action: quit }
MenuItem { action: login }
}
}
}

View file

@ -4,90 +4,133 @@ import "."
import "../controls"
Frame {
id: root
anchors { margins: -16; topMargin: parent.closable ? -32 : -16; }
id: frame
// The frame fills the parent, which should be the size of the content.
// The frame decorations use negative anchor margins to extend beyond
anchors.fill: parent
// Size of the controls
readonly property real iconSize: 24;
// Convenience accessor for the window
property alias window: frame.parent
// FIXME needed?
property alias decoration: decoration
MouseArea {
id: controlsArea
anchors.fill: desktopControls
hoverEnabled: true
drag.target: root.parent
propagateComposedEvents: true
onClicked: {
root.raise()
mouse.accepted = false;
}
MouseArea {
id: sizeDrag
enabled: root.parent.resizable
property int startX
property int startY
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 16
height: 16
z: 1000
hoverEnabled: true
onPressed: {
startX = mouseX
startY = mouseY
}
onPositionChanged: {
if (pressed) {
root.deltaSize((mouseX - startX), (mouseY - startY))
startX = mouseX
startY = mouseY
}
}
}
Rectangle {
anchors { margins: -4 }
visible: !decoration.visible
anchors.fill: parent;
color: "#7f7f7f7f";
radius: 3;
}
Rectangle {
id: desktopControls
// FIXME this doesn't work as expected
visible: root.parent.showFrame
id: decoration
anchors { margins: -iconSize; topMargin: -iconSize * (window.closable ? 2 : 1); }
visible: window.activator.containsMouse
anchors.fill: parent;
color: "#7f7f7f7f";
radius: 3;
// Allow dragging of the window
MouseArea {
id: dragMouseArea
anchors.fill: parent
drag {
target: window
// minimumX: (decoration.width - window.width) * -1
// minimumY: 0
// maximumX: (window.parent.width - window.width) - 2 * (decoration.width - window.width)
// maximumY: (window.parent.height - window.height) - 2 * (decoration.height - window.height)
}
}
Row {
id: controlsRow
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 4
anchors.topMargin: 4
spacing: 4
anchors.rightMargin: iconSize
anchors.topMargin: iconSize / 2
spacing: iconSize / 4
FontAwesome {
visible: false
text: "\uf08d"
style: Text.Outline; styleColor: "white"
size: 16
rotation: !root.parent ? 90 : root.parent.pinned ? 0 : 90
color: root.pinned ? "red" : "black"
size: frame.iconSize
rotation: !frame.parent ? 90 : frame.parent.pinned ? 0 : 90
color: frame.pinned ? "red" : "black"
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: { root.pin(); mouse.accepted = false; }
onClicked: { frame.pin(); mouse.accepted = false; }
}
}
FontAwesome {
visible: root.parent.closable
visible: window.closable
text: closeClickArea.containsMouse ? "\uf057" : "\uf05c"
style: Text.Outline;
styleColor: "white"
color: closeClickArea.containsMouse ? "red" : "black"
size: 16
size: frame.iconSize
MouseArea {
id: closeClickArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.close();
onClicked: frame.close();
}
}
}
// Allow sizing of the window
// FIXME works in native QML, doesn't work in Interface
MouseArea {
id: sizeDrag
width: iconSize
height: iconSize
anchors {
right: decoration.right;
bottom: decoration.bottom
bottomMargin: iconSize * 2
}
property vector2d pressOrigin
property vector2d sizeOrigin
property bool hid: false
onPressed: {
console.log("Pressed on size")
pressOrigin = Qt.vector2d(mouseX, mouseY)
sizeOrigin = Qt.vector2d(window.content.width, window.content.height)
hid = false;
}
onReleased: {
if (hid) {
window.content.visible = true
hid = false;
}
}
onPositionChanged: {
if (pressed) {
if (window.content.visible) {
window.content.visible = false;
hid = true;
}
var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin);
frame.deltaSize(delta.x, delta.y)
}
}
}
FontAwesome {
visible: window.resizable
rotation: -45
anchors { centerIn: sizeDrag }
horizontalAlignment: Text.AlignHCenter
text: "\uf07d"
size: iconSize / 3 * 2
style: Text.Outline; styleColor: "white"
}
}
}

View file

@ -6,78 +6,75 @@ import "../styles"
FocusScope {
id: window
objectName: "topLevelWindow"
HifiConstants { id: hifi }
implicitHeight: frame.height
implicitWidth: frame.width
// The Window size is the size of the content, while the frame
// decorations can extend outside it. Windows should generally not be
// given explicit height / width, but rather be allowed to conform to
// their content
implicitHeight: content.height
implicitWidth: content.width
property bool topLevelWindow: true
property string title
property bool showFrame: true
// Should the window include a close control?
property bool closable: true
property bool destroyOnInvisible: false
// Should hitting the close button hide or destroy the window?
property bool destroyOnCloseButton: true
property bool pinned: false
// Should hiding the window destroy it or just hide it?
property bool destroyOnInvisible: false
// FIXME support for pinned / unpinned pending full design
// property bool pinnable: false
// property bool pinned: false
property bool resizable: false
property real minX: 320
property real minY: 240;
property vector2d minSize: Qt.vector2d(100, 100)
property vector2d maxSize: Qt.vector2d(1280, 720)
// The content to place inside the window, determined by the client
default property var content
property var frame: DefaultFrame { anchors.fill: content }
property var blur: FastBlur { anchors.fill: content; source: content; visible: false; radius: 0}
//property var hoverDetector: MouseArea { anchors.fill: frame; hoverEnabled: true; propagateComposedEvents: true; }
//property bool mouseInWindow: hoverDetector.containsMouse
children: [ frame, content, blur ]
signal windowDestroyed();
QtObject {
id: d
property vector2d minPosition: Qt.vector2d(0, 0);
property vector2d maxPosition: Qt.vector2d(100, 100);
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
onContentChanged: {
if (content) {
content.anchors.fill = window
}
function updateParentRect() {
// if (!frame) { return; }
// console.log(window.parent.width);
// console.log(frame.width);
// minPosition = Qt.vector2d(-frame.anchors.leftMargin, -frame.anchors.topMargin);
// maxPosition = Qt.vector2d(
// Math.max(minPosition.x, Desktop.width - frame.width + minPosition.x),
// Math.max(minPosition.y, Desktop.height - frame.height + minPosition.y))
// console.log(maxPosition);
}
function keepOnScreen() {
//window.x = clamp(x, minPosition.x, maxPosition.x);
//window.y = clamp(y, minPosition.y, maxPosition.y);
}
onMinPositionChanged: keepOnScreen();
onMaxPositionChanged: keepOnScreen();
}
// Default to a standard frame. Can be overriden to provide custom
// frame styles, like a full desktop frame to simulate a modal window
property var frame: DefaultFrame {
z: -1
anchors.fill: parent
}
// This mouse area serves to raise the window. To function, it must live
// in the window and have a higher Z-order than the content, but follow
// the position and size of frame decoration
property var activator: MouseArea {
width: frame.decoration.width
height: frame.decoration.height
x: frame.decoration.anchors.margins
y: frame.decoration.anchors.topMargin
propagateComposedEvents: true
hoverEnabled: true
onPressed: { window.raise(); mouse.accepted = false; }
// Debugging visualization
// Rectangle { anchors.fill:parent; color: "#7f00ff00" }
}
children: [ frame, content, activator ]
signal windowDestroyed();
Component.onCompleted: {
d.updateParentRect();
fadeTargetProperty = visible ? 1.0 : 0.0
raise();
}
Component.onDestruction: {
content.destroy();
console.log("Destroyed " + window);
windowDestroyed();
}
onParentChanged: {
d.updateParentRect();
raise();
}
onFrameChanged: d.updateParentRect();
onWidthChanged: d.updateParentRect();
onHeightChanged: d.updateParentRect();
onXChanged: d.keepOnScreen();
onYChanged: d.keepOnScreen();
onParentChanged: raise();
Connections {
target: frame
@ -85,15 +82,15 @@ FocusScope {
onClose: window.close();
onPin: window.pin();
onDeltaSize: {
console.log("deltaSize")
content.width = Math.max(content.width + dx, minX)
content.height = Math.max(content.height + dy, minY)
var newSize = Qt.vector2d(content.width + dx, content.height + dy);
newSize = clampVector(newSize, minSize, maxSize);
window.width = newSize.x
window.height = newSize.y
}
}
function raise() {
if (enabled && parent) {
if (visible && parent) {
Desktop.raise(window)
if (!focus) {
focus = true;
@ -102,48 +99,58 @@ FocusScope {
}
function pin() {
pinned = ! pinned
// pinned = ! pinned
}
// our close function performs the same way as the OffscreenUI class:
// don't do anything but manipulate the enabled flag and let the other
// don't do anything but manipulate the targetVisible flag and let the other
// mechanisms decide if the window should be destroyed after the close
// animation completes
function close() {
console.log("Closing " + window)
if (destroyOnCloseButton) {
destroyOnInvisible = true
}
enabled = false;
visible = false;
}
onEnabledChanged: {
if (!enabled) {
if (blur) {
blur.visible = true;
}
if (content) {
content.visible = false;
}
}
//
// Enable window visibility transitions
//
opacity = enabled ? 1.0 : 0.0
// If the dialog is initially invisible, setting opacity doesn't
// trigger making it visible.
if (enabled) {
visible = true;
raise();
// The target property to animate, usually scale or opacity
property alias fadeTargetProperty: window.opacity
// always start the property at 0 to enable fade in on creation
opacity: 0
// Some dialogs should be destroyed when they become
// invisible, so handle that
onVisibleChanged: {
// If someone directly set the visibility to false
// toggle it back on and use the targetVisible flag to transition
// via fading.
if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) {
var target = visible;
visible = !visible;
fadeTargetProperty = target ? 1.0 : 0.0;
return;
}
if (!visible && destroyOnInvisible) {
console.log("Destroying " + window);
destroy();
return;
}
}
// The offscreen UI will enable an object, rather than manipulating it's
// visibility, so that we can do animations in both directions. Because
// visibility and enabled are boolean flags, they cannot be animated. So when
// enabled is change, we modify a property that can be animated, like scale or
// opacity, and then when the target animation value is reached, we can
// modify the visibility
// visibility is a boolean flags, it cannot be animated. So when
// targetVisible is changed, we modify a property that can be animated,
// like scale or opacity, and then when the target animation value is reached,
// we can modify the visibility
// The actual animator
Behavior on opacity {
Behavior on fadeTargetProperty {
NumberAnimation {
duration: hifi.effects.fadeInDuration
easing.type: Easing.OutCubic
@ -151,31 +158,31 @@ FocusScope {
}
// Once we're transparent, disable the dialog's visibility
onOpacityChanged: {
visible = (opacity != 0.0);
if (opacity == 1.0) {
content.visible = true;
blur.visible = false;
}
onFadeTargetPropertyChanged: {
visible = (fadeTargetProperty != 0.0);
}
// Some dialogs should be destroyed when they become
// invisible, so handle that
onVisibleChanged: {
if (!visible && destroyOnInvisible) {
console.log("Destroying " + window);
destroy();
}
}
Keys.onPressed: {
switch(event.key) {
case Qt.Key_W:
if (event.modifiers == Qt.ControlModifier) {
if (event.modifiers === Qt.ControlModifier) {
event.accepted = true
enabled = false
visible = false
}
break;
}
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function clampVector(value, min, max) {
return Qt.vector2d(
clamp(value.x, min.x, max.x),
clamp(value.y, min.y, max.y))
}
}

View file

@ -395,7 +395,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
QApplication(argc, argv),
_dependencyManagerIsSetup(setupEssentials(argc, argv)),
_window(new MainWindow(desktop())),
_toolWindow(NULL),
_undoStackScriptingInterface(&_undoStack),
_frameCount(0),
_fps(60.0f),
@ -678,10 +677,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
_renderEngine->addTask(make_shared<RenderDeferredTask>());
_renderEngine->registerScene(_main3DScene);
_toolWindow = new ToolWindow();
_toolWindow->setWindowFlags((_toolWindow->windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowMinimizeButtonHint);
_toolWindow->setWindowTitle("Tools");
_offscreenContext->makeCurrent();
// Tell our entity edit sender about our known jurisdictions
@ -4751,7 +4746,7 @@ void Application::showFriendsWindow() {
auto webWindowClass = _window->findChildren<WebWindowClass>(FRIENDS_WINDOW_OBJECT_NAME);
if (webWindowClass.empty()) {
auto friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH,
FRIENDS_WINDOW_HEIGHT, false);
FRIENDS_WINDOW_HEIGHT);
friendsWindow->setParent(_window);
friendsWindow->setObjectName(FRIENDS_WINDOW_OBJECT_NAME);
connect(friendsWindow, &WebWindowClass::closed, &WebWindowClass::deleteLater);

View file

@ -62,7 +62,6 @@
#include "ui/OverlayConductor.h"
#include "ui/overlays/Overlays.h"
#include "ui/SnapshotShareDialog.h"
#include "ui/ToolWindow.h"
#include "UndoStackScriptingInterface.h"
class OffscreenGLCanvas;
@ -164,8 +163,6 @@ public:
NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; }
ToolWindow* getToolWindow() { return _toolWindow ; }
virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; }
virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine);
@ -400,8 +397,6 @@ private:
MainWindow* _window;
ToolWindow* _toolWindow;
QUndoStack _undoStack;
UndoStackScriptingInterface _undoStackScriptingInterface;

View file

@ -9,6 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QVBoxLayout>
#include <QApplication>
#include <QMainWindow>
@ -35,48 +36,25 @@ void ScriptEventBridge::emitScriptEvent(const QString& data) {
emit scriptEventReceived(data);
}
WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow)
: QObject(NULL),
_eventBridge(new ScriptEventBridge(this)),
_isToolWindow(isToolWindow) {
WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height)
: QObject(NULL), _eventBridge(new ScriptEventBridge(this)) {
auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window);
dialogWidget->setWindowTitle(title);
dialogWidget->resize(width, height);
dialogWidget->installEventFilter(this);
connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed);
if (_isToolWindow) {
ToolWindow* toolWindow = qApp->getToolWindow();
auto layout = new QVBoxLayout(dialogWidget);
layout->setContentsMargins(0, 0, 0, 0);
dialogWidget->setLayout(layout);
auto dockWidget = new QDockWidget(title, toolWindow);
dockWidget->setFeatures(QDockWidget::DockWidgetMovable);
connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged);
_webView = new QWebView(dialogWidget);
_webView = new QWebView(dockWidget);
addEventBridgeToWindowObject();
layout->addWidget(_webView);
dockWidget->setWidget(_webView);
addEventBridgeToWindowObject();
auto titleWidget = new QWidget(dockWidget);
dockWidget->setTitleBarWidget(titleWidget);
toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal);
_windowWidget = dockWidget;
} else {
auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window);
dialogWidget->setWindowTitle(title);
dialogWidget->resize(width, height);
dialogWidget->installEventFilter(this);
connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed);
auto layout = new QVBoxLayout(dialogWidget);
layout->setContentsMargins(0, 0, 0, 0);
dialogWidget->setLayout(layout);
_webView = new QWebView(dialogWidget);
layout->addWidget(_webView);
addEventBridgeToWindowObject();
_windowWidget = dialogWidget;
}
_windowWidget = dialogWidget;
auto style = QStyleFactory::create("fusion");
if (style) {
@ -121,13 +99,8 @@ void WebWindowClass::addEventBridgeToWindowObject() {
void WebWindowClass::setVisible(bool visible) {
if (visible) {
if (_isToolWindow) {
QMetaObject::invokeMethod(
qApp->getToolWindow(), "setVisible", Qt::AutoConnection, Q_ARG(bool, visible));
} else {
QMetaObject::invokeMethod(_windowWidget, "showNormal", Qt::AutoConnection);
QMetaObject::invokeMethod(_windowWidget, "raise", Qt::AutoConnection);
}
QMetaObject::invokeMethod(_windowWidget, "showNormal", Qt::AutoConnection);
QMetaObject::invokeMethod(_windowWidget, "raise", Qt::AutoConnection);
}
QMetaObject::invokeMethod(_windowWidget, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible));
}
@ -182,13 +155,18 @@ void WebWindowClass::raise() {
QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
WebWindowClass* retVal;
QString file = context->argument(0).toString();
if (context->argument(4).toBool()) {
qWarning() << "ToolWindow views with WebWindow are no longer supported. Use OverlayWebWindow instead";
return QScriptValue();
} else {
qWarning() << "WebWindow views are deprecated. Use OverlayWebWindow instead";
}
QMetaObject::invokeMethod(DependencyManager::get<WindowScriptingInterface>().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(WebWindowClass*, retVal),
Q_ARG(const QString&, file),
Q_ARG(QString, context->argument(1).toString()),
Q_ARG(int, context->argument(2).toInteger()),
Q_ARG(int, context->argument(3).toInteger()),
Q_ARG(bool, context->argument(4).toBool()));
Q_ARG(int, context->argument(3).toInteger()));
connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater);

View file

@ -39,7 +39,7 @@ class WebWindowClass : public QObject {
Q_PROPERTY(QSizeF size READ getSize WRITE setSize);
public:
WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow = false);
WebWindowClass(const QString& title, const QString& url, int width, int height);
~WebWindowClass();
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
@ -75,7 +75,6 @@ private:
QWidget* _windowWidget;
QWebView* _webView;
ScriptEventBridge* _eventBridge;
bool _isToolWindow;
};
#endif

View file

@ -23,6 +23,7 @@
#include "Menu.h"
#include "OffscreenUi.h"
#include "ui/ModelsBrowser.h"
#include "WebWindowClass.h"
#include "WindowScriptingInterface.h"
@ -37,8 +38,8 @@ WindowScriptingInterface::WindowScriptingInterface() :
connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
}
WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow) {
return new WebWindowClass(title, url, width, height, isToolWindow);
WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height) {
return new WebWindowClass(title, url, width, height);
}
QScriptValue WindowScriptingInterface::hasFocus() {

View file

@ -19,7 +19,7 @@
#include <QComboBox>
#include <QLineEdit>
#include "WebWindowClass.h"
class WebWindowClass;
class WindowScriptingInterface : public QObject, public Dependency {
Q_OBJECT
@ -82,8 +82,8 @@ private slots:
void nonBlockingFormAccepted() { _nonBlockingFormActive = false; _formResult = QDialog::Accepted; emit nonBlockingFormClosed(); }
void nonBlockingFormRejected() { _nonBlockingFormActive = false; _formResult = QDialog::Rejected; emit nonBlockingFormClosed(); }
WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow);
WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height);
private:
QString jsRegExp2QtRegExp(QString string);
QDialog* createForm(const QString& title, QScriptValue form);

View file

@ -148,8 +148,7 @@ void DialogsManager::lodTools() {
}
void DialogsManager::toggleToolWindow() {
QMainWindow* toolWindow = qApp->getToolWindow();
toolWindow->setVisible(!toolWindow->isVisible());
DependencyManager::get<OffscreenUi>()->toggleToolWindow();
}
void DialogsManager::hmdTools(bool showTools) {

View file

@ -79,9 +79,6 @@ HMDToolsDialog::HMDToolsDialog(QWidget* parent) :
// what screens we're allowed on
watchWindow(windowHandle());
auto dialogsManager = DependencyManager::get<DialogsManager>();
if (qApp->getToolWindow()) {
watchWindow(qApp->getToolWindow()->windowHandle());
}
if (dialogsManager->getBandwidthDialog()) {
watchWindow(dialogsManager->getBandwidthDialog()->windowHandle());
}

View file

@ -28,6 +28,7 @@
#include "Snapshot.h"
#include "UserActivityLogger.h"
#include "UIUtil.h"
#include "scripting/WebWindowClass.h"
const int PREFERENCES_HEIGHT_PADDING = 20;
@ -135,7 +136,7 @@ void PreferencesDialog::openFullAvatarModelBrowser() {
const auto WIDTH = 900;
const auto HEIGHT = 700;
if (!_marketplaceWindow) {
_marketplaceWindow = new WebWindowClass("Marketplace", MARKETPLACE_URL, WIDTH, HEIGHT, false);
_marketplaceWindow = new WebWindowClass("Marketplace", MARKETPLACE_URL, WIDTH, HEIGHT);
}
_marketplaceWindow->setVisible(true);

View file

@ -17,7 +17,7 @@
#include <QDialog>
#include <QString>
#include "scripting/WebWindowClass.h"
class WebWindowClass;
class PreferencesDialog : public QDialog {
Q_OBJECT
@ -41,7 +41,7 @@ private:
QString _displayNameString;
WebWindowClass* _marketplaceWindow = NULL;
WebWindowClass* _marketplaceWindow { nullptr };
private slots:
void accept();

View file

@ -1,145 +0,0 @@
//
// ToolWindow.cpp
// interface/src/ui
//
// Created by Ryan Huffman on 11/13/14.
// Copyright 2014 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
//
#include "Application.h"
#include "MainWindow.h"
#include "ToolWindow.h"
#include "UIUtil.h"
const int DEFAULT_WIDTH = 300;
ToolWindow::ToolWindow(QWidget* parent) :
QMainWindow(parent),
_selfHidden(false),
_hasShown(false),
_lastGeometry() {
setTabPosition(Qt::TopDockWidgetArea, QTabWidget::TabPosition::North);
# ifndef Q_OS_LINUX
setDockOptions(QMainWindow::ForceTabbedDocks);
# endif
qApp->installEventFilter(this);
}
bool ToolWindow::event(QEvent* event) {
QEvent::Type type = event->type();
if (type == QEvent::Show) {
if (!_hasShown) {
_hasShown = true;
QMainWindow* mainWindow = qApp->getWindow();
QRect mainGeometry = mainWindow->geometry();
int titleBarHeight = UIUtil::getWindowTitleBarHeight(this);
int topMargin = titleBarHeight;
_lastGeometry = QRect(mainGeometry.topLeft().x(), mainGeometry.topLeft().y() + topMargin,
DEFAULT_WIDTH, mainGeometry.height() - topMargin);
}
setGeometry(_lastGeometry);
return true;
} else if (type == QEvent::Hide) {
_lastGeometry = geometry();
return true;
}
return QMainWindow::event(event);
}
bool ToolWindow::eventFilter(QObject* sender, QEvent* event) {
# ifndef Q_OS_LINUX
switch (event->type()) {
case QEvent::WindowStateChange:
if (qApp->getWindow()->isMinimized()) {
// If we are already visible, we are self-hiding
_selfHidden = isVisible();
setVisible(false);
} else {
if (_selfHidden) {
setVisible(true);
}
}
break;
case QEvent::ApplicationDeactivate:
_selfHidden = isVisible();
setVisible(false);
break;
case QEvent::ApplicationActivate:
if (_selfHidden) {
setVisible(true);
}
break;
default:
break;
}
# endif
return false;
}
void ToolWindow::onChildVisibilityUpdated(bool visible) {
if (!_selfHidden && visible) {
setVisible(true);
} else {
bool hasVisible = false;
QList<QDockWidget*> dockWidgets = findChildren<QDockWidget*>();
for (int i = 0; i < dockWidgets.count(); i++) {
if (dockWidgets[i]->isVisible()) {
hasVisible = true;
break;
}
}
// If a child was hidden and we don't have any children still visible, hide ourself.
if (!hasVisible) {
setVisible(false);
}
}
}
void ToolWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget) {
QList<QDockWidget*> dockWidgets = findChildren<QDockWidget*>();
QMainWindow::addDockWidget(area, dockWidget);
// We want to force tabbing, so retabify all of our widgets.
QDockWidget* lastDockWidget = dockWidget;
foreach (QDockWidget* nextDockWidget, dockWidgets) {
tabifyDockWidget(lastDockWidget, nextDockWidget);
lastDockWidget = nextDockWidget;
}
connect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated);
}
void ToolWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget, Qt::Orientation orientation) {
QList<QDockWidget*> dockWidgets = findChildren<QDockWidget*>();
QMainWindow::addDockWidget(area, dockWidget, orientation);
QDockWidget* lastDockWidget = dockWidget;
foreach(QDockWidget* nextDockWidget, dockWidgets) {
tabifyDockWidget(lastDockWidget, nextDockWidget);
lastDockWidget = nextDockWidget;
}
connect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated);
}
void ToolWindow::removeDockWidget(QDockWidget* dockWidget) {
QMainWindow::removeDockWidget(dockWidget);
disconnect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated);
}

View file

@ -1,44 +0,0 @@
//
// ToolWindow.h
// interface/src/ui
//
// Created by Ryan Huffman on 11/13/14.
// Copyright 2014 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
//
#ifndef hifi_ToolWindow_h
#define hifi_ToolWindow_h
#include <QDockWidget>
#include <QEvent>
#include <QMainWindow>
#include <QRect>
#include <QWidget>
class ToolWindow : public QMainWindow {
Q_OBJECT
public:
ToolWindow(QWidget* parent = NULL);
virtual bool event(QEvent* event);
virtual void addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget);
virtual void addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget, Qt::Orientation orientation);
virtual void removeDockWidget(QDockWidget* dockWidget);
virtual bool eventFilter(QObject* sender, QEvent* event);
public slots:
void onChildVisibilityUpdated(bool visible);
private:
// Indicates whether this window was hidden by itself (because the main window lost focus).
bool _selfHidden;
bool _hasShown;
QRect _lastGeometry;
};
#endif // hifi_ToolWindow_h

View file

@ -340,12 +340,25 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
_qmlComponent = new QQmlComponent(_qmlEngine);
}
void OffscreenQmlSurface::resize(const QSize& newSize) {
void OffscreenQmlSurface::resize(const QSize& newSize_) {
if (!_renderer || !_renderer->_quickWindow) {
return;
}
const float MAX_OFFSCREEN_DIMENSION = 4096;
QSize newSize = newSize_;
if (newSize.width() > MAX_OFFSCREEN_DIMENSION || newSize.height() > MAX_OFFSCREEN_DIMENSION) {
float scale = std::min(
((float)newSize.width() / MAX_OFFSCREEN_DIMENSION),
((float)newSize.height() / MAX_OFFSCREEN_DIMENSION));
newSize = QSize(
std::max(static_cast<int>(scale * newSize.width()), 10),
std::max(static_cast<int>(scale * newSize.height()), 10));
}
QSize currentSize = _renderer->_quickWindow->geometry().size();
if (newSize == currentSize) {

View file

@ -21,10 +21,6 @@
#include "MessageDialog.h"
// Needs to match the constants in resources/qml/Global.js
static const QString OFFSCREEN_ROOT_OBJECT_NAME = "desktopRoot";
static const QString OFFSCREEN_WINDOW_OBJECT_NAME = "topLevelWindow";
static QQuickItem* _desktop { nullptr };
class OffscreenFlags : public QObject {
Q_OBJECT
Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged)
@ -252,6 +248,48 @@ void OffscreenUi::createDesktop() {
_desktop = dynamic_cast<QQuickItem*>(load("Root.qml"));
Q_ASSERT(_desktop);
getRootContext()->setContextProperty("Desktop", _desktop);
_toolWindow = _desktop->findChild<QQuickItem*>("ToolWindow");
}
void OffscreenUi::toggleToolWindow() {
_toolWindow->setEnabled(!_toolWindow->isEnabled());
}
QQuickItem* OffscreenUi::getDesktop() {
return _desktop;
}
QQuickItem* OffscreenUi::getToolWindow() {
return _toolWindow;
}
Q_DECLARE_METATYPE(std::function<void()>);
static auto VoidLambdaType = qRegisterMetaType<std::function<void()>>();
Q_DECLARE_METATYPE(std::function<QVariant()>);
static auto VariantLambdaType = qRegisterMetaType<std::function<QVariant()>>();
void OffscreenUi::executeOnUiThread(std::function<void()> function) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "executeOnUiThread", Qt::QueuedConnection,
Q_ARG(std::function<void()>, function));
return;
}
function();
}
QVariant OffscreenUi::returnFromUiThread(std::function<QVariant()> function) {
if (QThread::currentThread() != thread()) {
QVariant result;
QMetaObject::invokeMethod(this, "returnFromUiThread", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QVariant, result),
Q_ARG(std::function<QVariant()>, function));
return result;
}
return function();
}
#include "OffscreenUi.moc"

View file

@ -12,6 +12,7 @@
#ifndef hifi_OffscreenUi_h
#define hifi_OffscreenUi_h
#include <QtCore/QVariant>
#include <gl/OffscreenQmlSurface.h>
#include <QMessageBox>
@ -33,6 +34,12 @@ public:
bool navigationFocused();
void setNavigationFocused(bool focused);
QQuickItem* getDesktop();
QQuickItem* getToolWindow();
Q_INVOKABLE void executeOnUiThread(std::function<void()> function);
Q_INVOKABLE QVariant returnFromUiThread(std::function<QVariant()> function);
// Messagebox replacement functions
using ButtonCallback = std::function<void(QMessageBox::StandardButton)>;
static ButtonCallback NO_OP_CALLBACK;
@ -70,6 +77,11 @@ public:
QMessageBox::StandardButtons buttons = QMessageBox::Ok);
static void error(const QString& text); // Interim dialog in new style
void toggleToolWindow();
private:
QQuickItem* _desktop { nullptr };
QQuickItem* _toolWindow { nullptr };
};
#endif

View file

@ -31,25 +31,15 @@ static const char* const URL_PROPERTY = "source";
// Method called by Qt scripts to create a new web window in the overlay
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine,
[&](QQmlContext* context, QObject* object) { return new QmlWebWindowClass(object); });
[&](QObject* object) { return new QmlWebWindowClass(object); });
}
QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) {
QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString)));
}
void QmlWebWindowClass::handleNavigation(const QString& url) {
bool handled = false;
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
if (handler) {
if (handler->canAcceptURL(url)) {
handled = handler->acceptURL(url);
}
}
if (handled) {
QMetaObject::invokeMethod(_qmlWindow, "stop", Qt::AutoConnection);
}
// FIXME remove.
void QmlWebWindowClass::handleNavigation(const QString& url) {
}
QString QmlWebWindowClass::getURL() const {

View file

@ -35,6 +35,7 @@ static const char* const TITLE_PROPERTY = "title";
static const char* const WIDTH_PROPERTY = "width";
static const char* const HEIGHT_PROPERTY = "height";
static const char* const VISIBILE_PROPERTY = "visible";
static const char* const TOOLWINDOW_PROPERTY = "toolWindow";
void QmlScriptEventBridge::emitWebEvent(const QString& data) {
QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data));
@ -86,13 +87,14 @@ void QmlWindowClass::setupServer() {
QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
QScriptContext* context, QScriptEngine* engine,
std::function<QmlWindowClass*(QQmlContext*, QObject*)> function)
std::function<QmlWindowClass*(QObject*)> builder)
{
const auto argumentCount = context->argumentCount();
QString url;
QString title;
int width = -1, height = -1;
bool visible = true;
bool toolWindow = false;
if (argumentCount > 1) {
if (!context->argument(0).isUndefined()) {
@ -107,6 +109,9 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
if (context->argument(3).isNumber()) {
height = context->argument(3).toInt32();
}
if (context->argument(4).isBool()) {
toolWindow = context->argument(4).toBool();
}
} else {
auto argumentObject = context->argument(0);
qDebug() << argumentObject.toString();
@ -125,6 +130,9 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
if (argumentObject.property(VISIBILE_PROPERTY).isBool()) {
visible = argumentObject.property(VISIBILE_PROPERTY).toBool();
}
if (argumentObject.property(TOOLWINDOW_PROPERTY).isBool()) {
toolWindow = argumentObject.property(TOOLWINDOW_PROPERTY).toBool();
}
}
if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) {
@ -137,17 +145,44 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
}
QmlWindowClass* retVal{ nullptr };
auto offscreenUi = DependencyManager::get<OffscreenUi>();
qDebug() << "Clearing component cache";
offscreenUi->getRootContext()->engine()->clearComponentCache();
// Build the event bridge and wrapper on the main thread
QMetaObject::invokeMethod(offscreenUi.data(), "load", Qt::BlockingQueuedConnection,
Q_ARG(const QString&, qmlSource),
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](QQmlContext* context, QObject* object) {
if (toolWindow) {
auto toolWindow = offscreenUi->getToolWindow();
QVariantMap properties;
properties.insert(TITLE_PROPERTY, title);
properties.insert(SOURCE_PROPERTY, url);
if (width != -1 && height != -1) {
properties.insert(WIDTH_PROPERTY, width);
properties.insert(HEIGHT_PROPERTY, height);
}
// Build the event bridge and wrapper on the main thread
QVariant newTabVar;
bool invokeResult = QMetaObject::invokeMethod(toolWindow, "addWebTab", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QVariant, newTabVar),
Q_ARG(QVariant, QVariant::fromValue(properties)));
QQuickItem* newTab = qvariant_cast<QQuickItem*>(newTabVar);
if (!invokeResult || !newTab) {
return QScriptValue();
}
offscreenUi->returnFromUiThread([&] {
setupServer();
retVal = function(context, object);
retVal = builder(newTab);
retVal->_toolWindow = true;
offscreenUi->getRootContext()->engine()->setObjectOwnership(retVal->_qmlWindow, QQmlEngine::CppOwnership);
registerObject(url.toLower(), retVal);
return QVariant();
});
} else {
// Build the event bridge and wrapper on the main thread
QMetaObject::invokeMethod(offscreenUi.data(), "load", Qt::BlockingQueuedConnection,
Q_ARG(const QString&, qmlSource),
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](QQmlContext* context, QObject* object) {
setupServer();
retVal = builder(object);
context->engine()->setObjectOwnership(retVal->_qmlWindow, QQmlEngine::CppOwnership);
registerObject(url.toLower(), retVal);
if (!title.isEmpty()) {
@ -158,9 +193,12 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
}
object->setProperty(SOURCE_PROPERTY, url);
if (visible) {
object->setProperty("enabled", true);
object->setProperty("visible", true);
}
}));
}
retVal->_source = url;
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater);
return engine->newQObject(retVal);
}
@ -168,7 +206,7 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
// Method called by Qt scripts to create a new web window in the overlay
QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
return internalConstructor("QmlWindow.qml", context, engine, [&](QQmlContext* context, QObject* object){
return internalConstructor("QmlWindow.qml", context, engine, [&](QObject* object){
return new QmlWindowClass(object);
});
}
@ -181,6 +219,21 @@ QmlWindowClass::QmlWindowClass(QObject* qmlWindow)
Q_ASSERT(dynamic_cast<const QQuickItem*>(_qmlWindow));
}
QmlWindowClass::~QmlWindowClass() {
if (_qmlWindow) {
if (_toolWindow) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
auto toolWindow = offscreenUi->getToolWindow();
auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::QueuedConnection,
Q_ARG(QVariant, _source));
Q_ASSERT(invokeResult);
} else {
_qmlWindow->deleteLater();
}
_qmlWindow = nullptr;
}
}
void QmlWindowClass::registerObject(const QString& name, QObject* object) {
webChannel.registerObject(name, object);
}
@ -189,74 +242,85 @@ void QmlWindowClass::deregisterObject(QObject* object) {
webChannel.deregisterObject(object);
}
void QmlWindowClass::setVisible(bool visible) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible));
return;
}
auto qmlWindow = asQuickItem();
if (qmlWindow->isEnabled() != visible) {
qmlWindow->setEnabled(visible);
emit visibilityChanged(visible);
}
}
QQuickItem* QmlWindowClass::asQuickItem() const {
if (_toolWindow) {
return DependencyManager::get<OffscreenUi>()->getToolWindow();
}
return dynamic_cast<QQuickItem*>(_qmlWindow);
}
bool QmlWindowClass::isVisible() const {
if (QThread::currentThread() != thread()) {
bool result;
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result));
return result;
void QmlWindowClass::setVisible(bool visible) {
// For tool window tabs we special case visiblility as enable / disable of the tab, not the window
// The tool window itself has special logic based on whether any tabs are enabled
if (_toolWindow) {
auto targetTab = dynamic_cast<QQuickItem*>(_qmlWindow);
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
targetTab->setEnabled(visible);
//emit visibilityChanged(visible);
});
} else {
QQuickItem* targetWindow = asQuickItem();
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
targetWindow->setVisible(visible);
//emit visibilityChanged(visible);
});
}
}
return asQuickItem()->isEnabled();
bool QmlWindowClass::isVisible() const {
// The tool window itself has special logic based on whether any tabs are enabled
if (_toolWindow) {
auto targetTab = dynamic_cast<QQuickItem*>(_qmlWindow);
return DependencyManager::get<OffscreenUi>()->returnFromUiThread([&] {
return QVariant::fromValue(targetTab->isEnabled());
}).toBool();
} else {
QQuickItem* targetWindow = asQuickItem();
return DependencyManager::get<OffscreenUi>()->returnFromUiThread([&] {
return QVariant::fromValue(targetWindow->isVisible());
}).toBool();
}
}
glm::vec2 QmlWindowClass::getPosition() const {
if (QThread::currentThread() != thread()) {
glm::vec2 result;
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result));
return result;
}
return glm::vec2(asQuickItem()->x(), asQuickItem()->y());
QQuickItem* targetWindow = asQuickItem();
QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant {
return targetWindow->position();
});
return toGlm(result.toPointF());
}
void QmlWindowClass::setPosition(const glm::vec2& position) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position));
return;
}
asQuickItem()->setPosition(QPointF(position.x, position.y));
QQuickItem* targetWindow = asQuickItem();
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
targetWindow->setPosition(QPointF(position.x, position.y));
});
}
void QmlWindowClass::setPosition(int x, int y) {
setPosition(glm::vec2(x, y));
}
// FIXME move to GLM helpers
glm::vec2 toGlm(const QSizeF& size) {
return glm::vec2(size.width(), size.height());
}
glm::vec2 QmlWindowClass::getSize() const {
if (QThread::currentThread() != thread()) {
glm::vec2 result;
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result));
return result;
}
return glm::vec2(asQuickItem()->width(), asQuickItem()->height());
QQuickItem* targetWindow = asQuickItem();
QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant {
return QSizeF(targetWindow->width(), targetWindow->height());
});
return toGlm(result.toSizeF());
}
void QmlWindowClass::setSize(const glm::vec2& size) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size));
}
asQuickItem()->setSize(QSizeF(size.x, size.y));
QQuickItem* targetWindow = asQuickItem();
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
targetWindow->setSize(QSizeF(size.x, size.y));
});
}
void QmlWindowClass::setSize(int width, int height) {
@ -264,27 +328,28 @@ void QmlWindowClass::setSize(int width, int height) {
}
void QmlWindowClass::setTitle(const QString& title) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title));
}
_qmlWindow->setProperty(TITLE_PROPERTY, title);
QQuickItem* targetWindow = asQuickItem();
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
targetWindow->setProperty(TITLE_PROPERTY, title);
});
}
void QmlWindowClass::close() {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection);
}
_qmlWindow->setProperty("destroyOnInvisible", true);
_qmlWindow->setProperty("visible", false);
_qmlWindow->deleteLater();
DependencyManager::get<OffscreenUi>()->executeOnUiThread([this] {
if (_qmlWindow) {
_qmlWindow->setProperty("destroyOnInvisible", true);
_qmlWindow->setProperty("visible", false);
_qmlWindow->deleteLater();
_qmlWindow = nullptr;
}
});
}
void QmlWindowClass::hasClosed() {
}
void QmlWindowClass::raise() {
QMetaObject::invokeMethod(_qmlWindow, "raiseWindow", Qt::QueuedConnection);
QMetaObject::invokeMethod(asQuickItem(), "raiseWindow", Qt::QueuedConnection);
}
#include "QmlWindowClass.moc"

View file

@ -51,6 +51,7 @@ class QmlWindowClass : public QObject {
public:
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
QmlWindowClass(QObject* qmlWindow);
~QmlWindowClass();
public slots:
bool isVisible() const;
@ -84,7 +85,7 @@ protected slots:
protected:
static QScriptValue internalConstructor(const QString& qmlSource,
QScriptContext* context, QScriptEngine* engine,
std::function<QmlWindowClass*(QQmlContext*, QObject*)> function);
std::function<QmlWindowClass*(QObject*)> function);
static void setupServer();
static void registerObject(const QString& name, QObject* object);
static void deregisterObject(QObject* object);
@ -95,9 +96,10 @@ protected:
// FIXME needs to be initialized in the ctor once we have support
// for tool window panes in QML
const bool _isToolWindow { false };
bool _toolWindow { false };
const int _windowId;
QObject* const _qmlWindow;
QObject* _qmlWindow;
QString _source;
};
#endif