diff --git a/.gitsubprojects b/.gitsubprojects index fa65acc1..f1e93754 100644 --- a/.gitsubprojects +++ b/.gitsubprojects @@ -1,5 +1,5 @@ # -*- mode: cmake -*- git_subproject(ZeroEQ https://github.com/HBPVIS/ZeroEQ.git 1019277) -git_subproject(Deflect https://github.com/BlueBrain/Deflect.git 7d2918e) +git_subproject(Deflect https://github.com/BlueBrain/Deflect.git d937377) git_subproject(TUIO https://github.com/BlueBrain/TUIO.git fdf5cf7) git_subproject(VirtualKeyboard https://github.com/rdumusc/QtFreeVirtualKeyboard.git e3ee94d) diff --git a/apps/Launcher/CMakeLists.txt b/apps/Launcher/CMakeLists.txt index 0eae1644..97255a81 100644 --- a/apps/Launcher/CMakeLists.txt +++ b/apps/Launcher/CMakeLists.txt @@ -4,7 +4,6 @@ # Raphael Dumusc set(TIDELAUNCHER_HEADERS - FileInfoHelper.h Launcher.h ) diff --git a/apps/Launcher/Launcher.h b/apps/Launcher/Launcher.h index bfb9ddb7..7b8f9c01 100644 --- a/apps/Launcher/Launcher.h +++ b/apps/Launcher/Launcher.h @@ -41,7 +41,7 @@ #ifndef LAUNCHER_H #define LAUNCHER_H -#include "FileInfoHelper.h" +#include "tide/master/FileInfoHelper.h" #include diff --git a/apps/Whiteboard/Whiteboard.cpp b/apps/Whiteboard/Whiteboard.cpp index 7301b7c1..30d1281b 100644 --- a/apps/Whiteboard/Whiteboard.cpp +++ b/apps/Whiteboard/Whiteboard.cpp @@ -43,6 +43,8 @@ #include "tide/master/localstreamer/QmlKeyInjector.h" #include "tide/master/MasterConfiguration.h" +#include + namespace { const std::string deflectHost( "localhost" ); @@ -67,6 +69,8 @@ Whiteboard::Whiteboard( int& argc, char* argv[] ) auto item = _qmlStreamer->getRootItem(); item->setProperty( "saveURL", config.getWhiteboardSaveFolder() ); + QQmlEngine* engine = _qmlStreamer->getQmlEngine(); + engine->rootContext()->setContextProperty( "fileInfo", &_fileInfoHelper ); } bool Whiteboard::event( QEvent* event_ ) diff --git a/apps/Whiteboard/Whiteboard.h b/apps/Whiteboard/Whiteboard.h index b08e89f4..c40888cc 100644 --- a/apps/Whiteboard/Whiteboard.h +++ b/apps/Whiteboard/Whiteboard.h @@ -40,7 +40,7 @@ #ifndef WHITEBOARD_H #define WHITEBOARD_H -#include +#include "tide/master/FileInfoHelper.h" #include #include @@ -59,6 +59,7 @@ class Whiteboard : public QGuiApplication std::unique_ptr _qmlStreamer; bool event( QEvent* event ) final; + FileInfoHelper _fileInfoHelper; }; #endif diff --git a/apps/Whiteboard/main.cpp b/apps/Whiteboard/main.cpp index 4d4b03a2..2fde7db0 100644 --- a/apps/Whiteboard/main.cpp +++ b/apps/Whiteboard/main.cpp @@ -46,6 +46,9 @@ int main( int argc, char** argv ) logger_id = "whiteboard"; qInstallMessageHandler( qtMessageLogger ); + // Load virtualkeyboard input context plugin + qputenv( "QT_IM_MODULE", QByteArray( "virtualkeyboard" )); + std::unique_ptr whiteboard; try { diff --git a/apps/Whiteboard/qml/whiteboard.qml b/apps/Whiteboard/qml/whiteboard.qml index 5db1e5e2..b0840c47 100755 --- a/apps/Whiteboard/qml/whiteboard.qml +++ b/apps/Whiteboard/qml/whiteboard.qml @@ -9,7 +9,6 @@ import QtQuick.Controls.Styles 1.2 Item { id: root - objectName: "Whiteboard" width: 1920 @@ -27,10 +26,48 @@ Item { property int lastY property int offsetY: 0 property int offsetX: 0 - + property var path property var singleTouchPoint: [] property var singleLine: [] property var allCurves: [] + property var fileList: [] + property bool pathAvail: false + + function checkFileExists() { + if (textInput.text != "") { + path = saveURL + textInput.text + ".png" + if (fileInfo.fileExists(path)) { + infoBox.z = 2 + infoText.text = "Are you sure? File already exists" + textInput.focus = false + buttonOK.visible = true + } else { + saveCanvas() + } + } + } + + function saveCanvas() { + buttonOK.visible = false + if (canvas.save(path)) + infoText.text = "Saved as: \n" + path + else + infoText.text = "Error saving as: \n" + path + infoBox.z = 2 + } + + function cancelSave() { + buttonOK.visible = false + infoText.text = "Save canceled" + infoBox.z = 2 + } + + function toggleSavePanel() { + if (savePanel.state == "on") + savePanel.state = "off" + else + savePanel.state = "on" + } onWidthChanged: { offsetX = (width - oldWidth) / 2 @@ -119,6 +156,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter color: "black" + radius: width * 0.5 } MouseArea { anchors.fill: parent @@ -136,7 +174,6 @@ Item { height: headerHeight color: "lightsteelblue" anchors.top: parent.top - Row { id: colorTools spacing: 5 @@ -157,9 +194,7 @@ Item { anchors.leftMargin: 75 MouseArea { anchors.fill: parent - onClicked: { - saveMenu.toggle() - } + onClicked: toggleSavePanel() } } @@ -167,13 +202,13 @@ Item { width: viewColor.model.count * (viewColor.buttonWidth + colorTools.spacing) height: 75 anchors.rightMargin: 75 - ListView { anchors.fill: parent id: viewColor model: colorModel delegate: colorDelegate spacing: 5 + boundsBehavior: Flickable.StopAtBounds property int buttonWidth: 75 orientation: ListView.Horizontal highlight: Rectangle { @@ -196,6 +231,7 @@ Item { model: brushModel delegate: brushDelegate spacing: 5 + boundsBehavior: Flickable.StopAtBounds property int buttonWidth: 75 orientation: ListView.Horizontal highlight: Rectangle { @@ -236,18 +272,6 @@ Item { anchors.bottom: parent.bottom width: root.width height: root.height - headerHeight - Text { - id: infoBox - text: "" - font.family: "Helvetica" - font.pointSize: 60 - color: "red" - - anchors.top: saveMenu.bottom - anchors.horizontalCenter: parent.horizontalCenter - z: 100 - visible: false - } Canvas { id: canvas @@ -264,14 +288,11 @@ Item { ctx.beginPath() ctx.strokeStyle = singleCurve[1] ctx.lineWidth = singleCurve[2] - ctx.lineCap = "round" - ctx.beginPath() - ctx.strokeStyle = singleCurve[1] - ctx.lineWidth = singleCurve[2] if (singleCurve[0].length < 4) { - ctx.arc(singleCurve[0][0][0], singleCurve[0][0][1], 1, - 0, Math.PI * 2, false) + ctx.arc(singleCurve[0][0][0] + singleCurve[3][0], + singleCurve[0][0][1] + singleCurve[3][1], 1, 0, + Math.PI * 2, false) } else { for (var i = 0; i < singleCurve[0].length - 1; i++) { ctx.moveTo(singleCurve[0][i][0] + singleCurve[3][0], @@ -285,38 +306,58 @@ Item { ctx.save() } } - - + MultiPointTouchArea { + id: savePanelBackground + anchors.fill: parent + enabled: false + onTouchUpdated: { + if (savePanel.visible) + toggleSavePanel() + } + Rectangle { + anchors.fill: parent + color: "lightsteelblue" + opacity: 0.3 + visible: parent.enabled + } + } MultiPointTouchArea { id: area + enabled: !savePanelBackground.enabled anchors.fill: parent property var paths: [] - touchPoints: TouchPoint { - id: point1 - } + + + touchPoints: [ + TouchPoint { + id: point0 + } + ] onPressed: { - singleLine = new Array(0) - lastX = point1.x - lastY = point1.y - singleLine.push([point1.x, point1.y]) + if (touchPoints[0] === point0) { + singleLine = new Array(0) + lastX = point0.x + lastY = point0.y + singleLine.push([point0.x, point0.y]) + } } - onTouchUpdated: { - singleLine.push([point1.x, point1.y]) + onUpdated: { + singleLine.push([point0.x, point0.y]) canvasTmp.requestPaint() } onReleased: { - var singleCurve = new Array(0) - singleCurve.push(singleLine) - singleCurve.push(brushColor) - singleCurve.push(brushSize) - singleCurve.push([0, 0]) - - allCurves.push(singleCurve) - canvas.requestPaint() - - paths = [] - canvasTmp.getContext("2d").reset() + if (touchPoints[0] === point0) { + var singleCurve = new Array(0) + singleCurve.push(singleLine) + singleCurve.push(brushColor) + singleCurve.push(brushSize) + singleCurve.push([0, 0]) + allCurves.push(singleCurve) + canvas.requestPaint() + paths = [] + canvasTmp.getContext("2d").reset() + } } } @@ -330,8 +371,8 @@ Item { ctx2.strokeStyle = brushColor ctx2.beginPath() ctx2.moveTo(lastX, lastY) - lastX = point1.x - lastY = point1.y + lastX = point0.x + lastY = point0.y ctx2.lineTo(lastX, lastY) ctx2.stroke() ctx2.closePath() @@ -340,113 +381,167 @@ Item { } Rectangle { - color: "#e7edf5" - function toggle() { - if (saveMenu.state == "on") - saveMenu.state = "off" - else - saveMenu.state = "on" - } - id: saveMenu - width: root.width - height: 50 - anchors.top: parent.top + id: savePanel + width: 800 + height: 250 + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + visible: false states: [ State { name: "on" PropertyChanges { - target: saveMenu + target: savePanel visible: true } PropertyChanges { target: imgSave opacity: 0.1 } + PropertyChanges { + target: savePanelBackground + enabled: true + } + PropertyChanges { + target: textInput + focus: true + } + PropertyChanges { + target: infoBox + z: 0 + } }, State { name: "off" PropertyChanges { - target: saveMenu + target: savePanel visible: false } + PropertyChanges { + target: savePanelBackground + enabled: false + } + PropertyChanges { + target: textInput + focus: false + } } ] - Rectangle { - color: "#e7edf5" - anchors.centerIn: parent - width: 600 - height: parent.height - - Rectangle { - color: "#7b899b" - anchors.centerIn: parent - id: rect5 - anchors.fill: parent - anchors.leftMargin: 50 - anchors.rightMargin: 50 - height: parent.height - TextInput { - wrapMode: TextInput.Wrap - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignHCenter - id: inputFileName - color: "black" - font.pixelSize: 24 - text: "" - anchors.fill: parent - focus: true + TextField { + id: textInput + placeholderText: "File name" + focus: false + width: 700 + height: 50 + z: 1 + anchors.left: parent.left + style: TextFieldStyle { + font.pointSize: control.height * 0.5 + } + onFocusChanged: { + if (focus) + Qt.inputMethod.show() + else + Qt.inputMethod.hide() + } + selectByMouse: true + validator: RegExpValidator { + regExp: /[\w.]*/ + } + onAccepted: { + checkFileExists() + if (textInput.text == "") + Qt.inputMethod.show() + } + Button { + anchors.left: textInput.right + text: "Save" + implicitWidth: 100 + implicitHeight: 50 + onClicked: { + checkFileExists() } - - Button { - anchors.left: inputFileName.right - text: "Save!" - onClicked: { - if (inputFileName.text != "") { - var path = saveURL + inputFileName.text + ".png" - if (canvas.save(path)) { - infoBox.text = "SAVED as " + path - infoBox.visible = true - timer1.start() - } else { - infoBox.text = "Error saving!" - infoBox.visible = true - timer1.start() - } - } - } - - style: ButtonStyle { - background: Rectangle { - implicitWidth: 100 - implicitHeight: 50 - border.width: 0 - border.color: "#7b899b" - radius: 0 - gradient: Gradient { - GradientStop { - position: 0 - color: control.pressed ? "#7b899b" : "#b0c4de" - } - GradientStop { - position: 1 - color: control.pressed ? "#b0c4de" : "#7b899b" - } - } - } - } - - Timer { - id: timer1 - interval: 750 - running: false - repeat: false - onTriggered: infoBox.visible = false + enabled: (textInput.text == "") ? false : true + style: ButtonStyle { + label: Text { + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pointSize: control.height * 0.4 + text: control.text + color: control.enabled ? "black" : "gray" } } } } + Rectangle { + id: infoBox + anchors.fill: parent + width: 800 + height: 250 + color: "black" + Text { + id: infoText + anchors.fill: parent + anchors.margins: 10 + text: "" + font.family: "Verdana" + font.pointSize: 35 + color: "white" + wrapMode: Text.Wrap + } + Row { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + spacing: 10 + height: 50 + + Button { + id: buttonOK + implicitWidth: 100 + implicitHeight: 50 + style: ButtonStyle { + label: Text { + renderType: Text.NativeRendering + horizontalAlignment: Text.AlignHCenter + font.pointSize: control.height * 0.3 + verticalAlignment: Text.AlignVCenter + text: "OK" + color: control.enabled ? "black" : "gray" + } + } + onClicked: saveCanvas() + } + Button { + id: buttonCancel + visible: buttonOK.visible + implicitWidth: 100 + implicitHeight: 50 + style: ButtonStyle { + label: Text { + renderType: Text.NativeRendering + font.pointSize: control.height * 0.3 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "Cancel" + color: control.enabled ? "black" : "gray" + } + } + onClicked: cancelSave() + } + } + } + } + Loader { + id: virtualKeyboard + source: "qrc:/virtualkeyboard/InputPanel.qml" + anchors.top: textInput.bottom + anchors.right: savePanel.right + anchors.left: savePanel.left + anchors.bottom: savePanel.bottom + visible: Qt.inputMethod.visible ? true : false } } } diff --git a/doc/Changelog.md b/doc/Changelog.md index 3f78f6e3..072d6c5f 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -8,6 +8,8 @@ Changelog {#changelog} * [97](https://github.com/BlueBrain/Tide/pull/97): Webbrowsers can be saved and restored from sessions and display the page title in their title bar. +* [96](https://github.com/BlueBrain/Tide/pull/96): + Bug fixes in Whiteboard application. * [95](https://github.com/BlueBrain/Tide/pull/95): More consistent and intuitive user experience: - Double-tap a window to make it fullscreen diff --git a/tests/cpp/core/ContentControllerTests.cpp b/tests/cpp/core/ContentControllerTests.cpp index eaba2c37..0f4dfc17 100644 --- a/tests/cpp/core/ContentControllerTests.cpp +++ b/tests/cpp/core/ContentControllerTests.cpp @@ -97,7 +97,8 @@ BOOST_AUTO_TEST_CASE( testFactoryMethod ) dummyContent.type = CONTENT_TYPE_WEBBROWSER; BOOST_CHECK_THROW( ContentController::create( window ), std::bad_cast ); - ContentWindow webWindow( ContentFactory::getWebbrowserContent( "abc" )); + ContentWindow webWindow( + ContentFactory::getPixelStreamContent( "abc", StreamType::WEBBROWSER )); BOOST_CHECK_NO_THROW( controller = ContentController::create( webWindow )); #endif #if TIDE_USE_QT5WEBKITWIDGETS diff --git a/tide/core/scene/ContentFactory.cpp b/tide/core/scene/ContentFactory.cpp index 8cef76c8..551bc58e 100644 --- a/tide/core/scene/ContentFactory.cpp +++ b/tide/core/scene/ContentFactory.cpp @@ -158,19 +158,23 @@ ContentPtr ContentFactory::getContent( const QString& uri ) return ContentPtr(); } -ContentPtr ContentFactory::getPixelStreamContent( const QString& uri ) -{ - return ContentPtr( new PixelStreamContent( uri )); -} - -ContentPtr ContentFactory::getWebbrowserContent( const QString& uri ) +ContentPtr ContentFactory::getPixelStreamContent( const QString& uri, + StreamType stream ) { + if ( stream == StreamType::WEBBROWSER ) + { #if TIDE_USE_QT5WEBKITWIDGETS || TIDE_USE_QT5WEBENGINE - return ContentPtr( new WebbrowserContent( uri )); + return ContentPtr( new WebbrowserContent( uri )); #else - Q_UNUSED( uri ); - throw std::runtime_error( "Tide was compiled without WebbrowserContent!" ); + Q_UNUSED( uri ); + throw std::runtime_error( "Tide compiled without WebbrowserContent!" ); #endif + } + else + { + const auto keyboard = stream == StreamType::EXTERNAL; + return ContentPtr( new PixelStreamContent( uri, keyboard )); + } } ContentPtr ContentFactory::getErrorContent( const QSize& size ) diff --git a/tide/core/scene/ContentFactory.h b/tide/core/scene/ContentFactory.h index 3e97ed0d..7cb14a16 100644 --- a/tide/core/scene/ContentFactory.h +++ b/tide/core/scene/ContentFactory.h @@ -41,12 +41,10 @@ #define CONTENTFACTORY_H #include "types.h" -#include "ContentType.h" +#include "scene/ContentType.h" #include -class Content; - class ContentFactory { public: @@ -54,10 +52,8 @@ class ContentFactory static ContentPtr getContent( const QString& uri ); /** Special case: PixelStreamContent type cannot be derived from its uri. */ - static ContentPtr getPixelStreamContent( const QString& uri ); - - /** Create a Webbrowser Content (special type of PixelStream). */ - static ContentPtr getWebbrowserContent( const QString& uri ); + static ContentPtr getPixelStreamContent( const QString& uri, + StreamType stream = StreamType::EXTERNAL ); /** Get a Content object representing a loading error. */ static ContentPtr getErrorContent( const QSize& size = QSize( )); diff --git a/tide/core/scene/ContentType.h b/tide/core/scene/ContentType.h index f3633f1a..141ebcce 100644 --- a/tide/core/scene/ContentType.h +++ b/tide/core/scene/ContentType.h @@ -56,6 +56,16 @@ enum CONTENT_TYPE CONTENT_TYPE_IMAGE_PYRAMID }; +/** + * The different types of pixel streams that can be created. + */ +enum class StreamType +{ + EXTERNAL, + WEBBROWSER, + WHITEBOARD +}; + QString getContentTypeString( const CONTENT_TYPE type ); CONTENT_TYPE getContentType( const QString& typeString ); diff --git a/tide/master/CMakeLists.txt b/tide/master/CMakeLists.txt index ef9cb241..8ddb47fb 100644 --- a/tide/master/CMakeLists.txt +++ b/tide/master/CMakeLists.txt @@ -21,6 +21,7 @@ list(APPEND TIDEMASTER_PUBLIC_HEADERS control/LayoutEngine.h control/PixelStreamController.h control/ZoomController.h + FileInfoHelper.h localstreamer/HtmlSelectReplacer.h localstreamer/CommandLineOptions.h localstreamer/PixelStreamerLauncher.h diff --git a/apps/Launcher/FileInfoHelper.h b/tide/master/FileInfoHelper.h similarity index 96% rename from apps/Launcher/FileInfoHelper.h rename to tide/master/FileInfoHelper.h index ba26ed4a..a0b96549 100644 --- a/apps/Launcher/FileInfoHelper.h +++ b/tide/master/FileInfoHelper.h @@ -55,6 +55,11 @@ class FileInfoHelper : public QObject { return QFileInfo( filePath ).completeBaseName(); } + + Q_INVOKABLE bool fileExists( const QString& filePath ) const + { + return QFileInfo( filePath ).exists(); + } }; #endif diff --git a/tide/master/PixelStreamWindowManager.cpp b/tide/master/PixelStreamWindowManager.cpp index c99ab187..f0f51a93 100644 --- a/tide/master/PixelStreamWindowManager.cpp +++ b/tide/master/PixelStreamWindowManager.cpp @@ -46,6 +46,7 @@ #include "localstreamer/PixelStreamerLauncher.h" #include "log.h" #include "scene/ContentFactory.h" +#include "scene/ContentType.h" #include "scene/ContentWindow.h" #include "scene/DisplayGroup.h" #include "scene/PixelStreamContent.h" @@ -97,10 +98,10 @@ bool _isPanel( const QString& uri ) } ContentWindowPtr _makeStreamWindow( const QString& uri, const QSize& size, - const bool webbrowser ) + const StreamType stream ) { - auto content = webbrowser ? ContentFactory::getWebbrowserContent( uri ) : - ContentFactory::getPixelStreamContent( uri ); + auto content = ContentFactory::getPixelStreamContent( uri, stream ); + if( size.isValid( )) content->setDimensions( size ); @@ -120,8 +121,7 @@ void PixelStreamWindowManager::openWindow( const QString& uri, put_flog( LOG_INFO, "opening pixel stream window: '%s'", uri.toLocal8Bit().constData( )); - const auto webbrowser = (stream == StreamType::WEBBROWSER); - auto window = _makeStreamWindow( uri, size, webbrowser ); + auto window = _makeStreamWindow( uri, size, stream ); ContentWindowController controller{ *window, _displayGroup }; controller.resize( size.isValid() ? size : EMPTY_STREAM_SIZE ); diff --git a/tide/master/PixelStreamWindowManager.h b/tide/master/PixelStreamWindowManager.h index b993296b..480f2de0 100644 --- a/tide/master/PixelStreamWindowManager.h +++ b/tide/master/PixelStreamWindowManager.h @@ -42,6 +42,7 @@ #define PIXEL_STREAM_WINDOW_MANAGER_H #include "types.h" +#include "scene/ContentType.h" #include @@ -51,16 +52,6 @@ #include #include -/** - * The different types of stream windows that can be created. - */ -enum class StreamType -{ - EXTERNAL, - WEBBROWSER, - WHITEBOARD -}; - /** * Handles window creation, association and updates for pixel streamers, both * local and external. The association is one streamer to one window. diff --git a/tide/master/localstreamer/PixelStreamerLauncher.cpp b/tide/master/localstreamer/PixelStreamerLauncher.cpp index 6de43bce..18045a13 100644 --- a/tide/master/localstreamer/PixelStreamerLauncher.cpp +++ b/tide/master/localstreamer/PixelStreamerLauncher.cpp @@ -41,8 +41,9 @@ #include "log.h" #include "CommandLineOptions.h" -#include "PixelStreamWindowManager.h" #include "MasterConfiguration.h" +#include "PixelStreamWindowManager.h" +#include "scene/ContentType.h" #include "scene/ContentWindow.h" #include "scene/WebbrowserContent.h"