diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index e5ff849df8..ea2f048c1f 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -35,7 +35,7 @@ FocusScope { } onHeightChanged: d.handleSizeChanged(); - + onWidthChanged: d.handleSizeChanged(); // Controls and windows can trigger this signal to ensure the desktop becomes visible @@ -70,8 +70,8 @@ FocusScope { } var oldRecommendedRect = recommendedRect; var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedOverlayRect(); - var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, - newRecommendedRectJS.width, + var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, + newRecommendedRectJS.width, newRecommendedRectJS.height); var oldChildren = expectedChildren; @@ -366,8 +366,8 @@ FocusScope { } var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedOverlayRect(); - var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, - newRecommendedRectJS.width, + var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, + newRecommendedRectJS.width, newRecommendedRectJS.height); var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; var newX = newRecommendedRect.x + ((newRecommendedRect.width - targetWindow.width) / 2); @@ -402,7 +402,7 @@ FocusScope { repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); } - function repositionWindow(targetWindow, forceReposition, + function repositionWindow(targetWindow, forceReposition, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions) { if (desktop.width === 0 || desktop.height === 0) { @@ -456,6 +456,11 @@ FocusScope { return inputDialogBuilder.createObject(desktop, properties); } + Component { id: customInputDialogBuilder; CustomQueryDialog { } } + function customInputDialog(properties) { + return customInputDialogBuilder.createObject(desktop, properties); + } + Component { id: fileDialogBuilder; FileDialog { } } function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); @@ -487,7 +492,7 @@ FocusScope { } function unfocusWindows() { - // First find the active focus item, and unfocus it, all the way + // First find the active focus item, and unfocus it, all the way // up the parent chain to the window var currentFocus = offscreenWindow.activeFocusItem; var targetWindow = d.getDesktopWindow(currentFocus); diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml new file mode 100644 index 0000000000..3f2bab010f --- /dev/null +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -0,0 +1,237 @@ +// +// CustomQueryDialog.qml +// +// Created by Zander Otavka on 7/14/16 +// Copyright 2016 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 +// + +import QtQuick 2.5; +import QtQuick.Controls 1.4; +import QtQuick.Dialogs 1.2 as OriginalDialogs; + +import "../controls-uit"; +import "../styles-uit"; +import "../windows"; + +ModalWindow { + id: root; + HifiConstants { id: hifi; } + implicitWidth: 640; + implicitHeight: 320; + visible: true; + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none; + property string iconText: ""; + property int iconSize: 35; + onIconChanged: updateIcon(); + + property var textInput; + property var comboBox; + property var checkBox; + + property var result; + property alias current: textField.text; + + // For text boxes + property alias placeholderText: textField.placeholderText; + + // For combo boxes + property bool editable: true; + + property int titleWidth: 0; + onTitleWidthChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + Item { + clip: true; + width: pane.width; + height: pane.height; + anchors.margins: 0; + + QtObject { + id: d; + readonly property int minWidth: 480; + readonly property int maxWdith: 1280; + readonly property int minHeight: 120; + readonly property int maxHeight: 720; + + function resize() { + var targetWidth = Math.max(titleWidth, pane.width); + var targetHeight = (textInput ? textField.controlHeight : 0) + extraInputs.height + + (6 * hifi.dimensions.contentSpacing.y) + buttons.height; + // var targetHeight = 720; + print("CQD extraInputs.height = " + extraInputs.height); + print("CQD textField.controlHeight = " + textField.controlHeight); + print("CQD buttons.height = " + buttons.height); + print("CQD targetHeight = " + targetHeight); + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + root.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? + d.maxHeight : targetHeight); + print("CQD comboBoxField.visible = " + comboBoxField.visible); + if (comboBoxField.visible) { + print("CQD parent = " + parent); + comboBoxField.width = extraInputs.width / 2; + } + } + } + + Item { + anchors { + top: parent.top; + bottom: extraInputs.top; + left: parent.left; + right: parent.right; + margins: 0; + bottomMargin: 2 * hifi.dimensions.contentSpacing.y; + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textField; + label: root.textInput.label; + focus: root.textInput ? true : false; + visible: root.textInput ? true : false; + anchors { + left: parent.left; + right: parent.right; + bottom: parent.bottom; + } + } + } + + Row { + id: extraInputs; + spacing: hifi.dimensions.contentSpacing.x; + anchors { + left: parent.left; + right: parent.right; + bottom: buttons.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + height: comboBoxField.controlHeight; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + + CheckBox { + id: checkBoxField; + text: root.checkBox.label; + focus: root.checkBox ? true : false; + visible: root.checkBox ? true : false; + anchors { + left: parent.left; + bottom: parent.bottom; + } + } + + ComboBox { + id: comboBoxField; + label: root.comboBox.label; + focus: root.comboBox ? true : false; + visible: root.comboBox ? true : false; + anchors { + right: parent.right; + bottom: parent.bottom; + } + model: root.comboBox ? root.comboBox.items : []; + } + } + + Row { + id: buttons; + focus: true; + spacing: hifi.dimensions.contentSpacing.x; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + + Button { + id: acceptButton; + action: acceptAction; + // anchors { + // bottom: parent.bottom; + // right: cancelButton.left; + // leftMargin: hifi.dimensions.contentSpacing.x; + // } + } + + Button { + id: cancelButton; + action: cancelAction; + // anchors { + // bottom: parent.bottom; + // right: parent.right; + // leftMargin: hifi.dimensions.contentSpacing.x; + // } + } + } + + Action { + id: cancelAction; + text: qsTr("Cancel"); + shortcut: Qt.Key_Escape; + onTriggered: { + root.result = null; + root.canceled(); + root.destroy(); + } + } + + Action { + id: acceptAction; + text: qsTr("Add"); + shortcut: Qt.Key_Return; + onTriggered: { + root.result = { + textInput: textField.text, + comboBox: comboBoxField.currentText, + checkBox: checkBoxField.checked + }; + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return; + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger(); + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger(); + event.accepted = true; + break; + } + } + + Component.onCompleted: { + updateIcon(); + d.resize(); + textField.forceActiveFocus(); + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fb48472b14..49b9d875cb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4710,6 +4710,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); + qScriptRegisterMetaType(scriptEngine, CustomPromptResultToScriptValue, CustomPromptResultFromScriptValue); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter, "Window"); // register `location` on the global object. diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index f0ae221566..b165cda135 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -29,6 +29,20 @@ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStanda static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; +QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) { + if (!result.value.isValid()) { + return QScriptValue::UndefinedValue; + } + + Q_ASSERT(result.value.userType() == qMetaTypeId()); + return engine->toScriptValue(result.value.toMap()); +} + +void CustomPromptResultFromScriptValue(const QScriptValue& object, CustomPromptResult& result) { + result.value = object.toVariant(); +} + + WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); @@ -95,6 +109,14 @@ QScriptValue WindowScriptingInterface::prompt(const QString& message, const QStr return ok ? QScriptValue(result) : QScriptValue::NullValue; } +CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { + CustomPromptResult result; + bool ok = false; + auto configMap = config.toMap(); + result.value = OffscreenUi::getCustomInfo(OffscreenUi::ICON_NONE, "", configMap, &ok); + return ok ? result : CustomPromptResult(); +} + QString fixupPathForMac(const QString& directory) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 4f26ccd057..9d73111333 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -18,6 +18,18 @@ class WebWindowClass; + +class CustomPromptResult { +public: + QVariant value; +}; + +Q_DECLARE_METATYPE(CustomPromptResult); + +QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result); +void CustomPromptResultFromScriptValue(const QScriptValue& object, CustomPromptResult& result); + + class WindowScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int innerWidth READ getInnerWidth) @@ -38,6 +50,7 @@ public slots: void alert(const QString& message = ""); QScriptValue confirm(const QString& message = ""); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); + CustomPromptResult customPrompt(const QVariant& config); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void copyToClipboard(const QString& text); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 28aba4a365..47ca210d01 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -343,6 +343,23 @@ QString OffscreenUi::getItem(const Icon icon, const QString& title, const QStrin return result.toString(); } +QVariant OffscreenUi::getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok) { + if (ok) { + *ok = false; + } + + QVariant result = DependencyManager::get()->customInputDialog(icon, title, config); + if (ok && result.isValid()) { + *ok = true; + } + + // Casts from QJSValue to QVariantMap (not sure how, just copied from http://lists.qt-project.org/pipermail/development/2014-September/018513.html) + Q_ASSERT(result.userType() == qMetaTypeId()); + result = qvariant_cast(result).toVariant(); + + return result; +} + QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current) { if (QThread::currentThread() != thread()) { QVariant result; @@ -358,6 +375,20 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } +QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, const QVariantMap& config) { + if (QThread::currentThread() != thread()) { + QVariant result; + QMetaObject::invokeMethod(this, "customInputDialog", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVariant, result), + Q_ARG(Icon, icon), + Q_ARG(QString, title), + Q_ARG(QVariantMap, config)); + return result; + } + + return waitForInputDialogResult(createCustomInputDialog(icon, title, config)); +} + void OffscreenUi::togglePinned() { bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); if (!invokeResult) { @@ -401,6 +432,23 @@ QQuickItem* OffscreenUi::createInputDialog(const Icon icon, const QString& title return qvariant_cast(result); } +QQuickItem* OffscreenUi::createCustomInputDialog(const Icon icon, const QString& title, const QVariantMap& config) { + QVariantMap map = config; + map.insert("title", title); + map.insert("icon", icon); + QVariant result; + bool invokeResult = QMetaObject::invokeMethod(_desktop, "customInputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + + if (!invokeResult) { + qWarning() << "Failed to create custom message box"; + return nullptr; + } + + return qvariant_cast(result); +} + QVariant OffscreenUi::waitForInputDialogResult(QQuickItem* inputDialog) { if (!inputDialog) { return QVariant(); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 2bd00bf612..36297d76c7 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -122,7 +122,9 @@ public: static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant()); + Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config); QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current); + QQuickItem* createCustomInputDialog(const Icon icon, const QString& title, const QVariantMap& config); QVariant waitForInputDialogResult(QQuickItem* inputDialog); // Compatibility with QInputDialog::getText @@ -140,6 +142,7 @@ public: static QString getText(const Icon icon, const QString & title, const QString & label, const QString & text = QString(), bool * ok = 0); static QString getItem(const Icon icon, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0); + static QVariant getCustomInfo(const Icon icon, const QString& title, const QVariantMap& config, bool* ok = 0); unsigned int getMenuUserDataId() const;