Repository: penk/terrarium-app Branch: master Commit: 564aec92ad5b Files: 38 Total size: 61.3 KB Directory structure: gitextract_mb_qe0pv/ ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── patches/ │ └── expose_loader_errorstring_invokable.patch ├── platform/ │ ├── android/ │ │ └── AndroidManifest.xml │ ├── arch/ │ │ └── terrarium.desktop │ ├── ios/ │ │ ├── Images.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.launchimage/ │ │ │ └── Contents.json │ │ └── Info.plist │ ├── mac/ │ │ ├── Info.plist │ │ └── icon.icns │ └── ubuntu/ │ ├── debian/ │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ ├── copyright │ │ ├── install │ │ ├── rules │ │ └── source/ │ │ └── format │ ├── manifest.json │ ├── terrarium.desktop │ └── terrarium.json ├── qml/ │ ├── BottomBar.qml │ ├── CustomButton.qml │ ├── HttpServer.qml │ ├── NaviBar.qml │ ├── QtWebKit.qml │ ├── QtWebView.qml │ ├── assets.qrc │ └── main.qml ├── src/ │ ├── documenthandler.cpp │ ├── documenthandler.h │ ├── main.cpp │ ├── qmlhighlighter.cpp │ ├── qmlhighlighter.h │ ├── quickitemgrabber.cpp │ └── quickitemgrabber.h └── terrarium-app.pro ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # C++ objects and libs *.slo *.lo *.o *.a *.la *.lai *.so *.dll *.dylib # Qt-es /.qmake.cache /.qmake.stash *.pro.user *.pro.user.* *.moc moc_*.cpp qrc_*.cpp ui_*.h Makefile* *-build-* # QtCreator *.autosave *.swp *.app *.dmg *deployment-settings.json # Project files Terrarium Terrarium.build Terrarium.xcodeproj .tmp terrarium_plugin_import.cpp terrarium_qml_plugin_import.cpp qt.conf ================================================ FILE: .gitmodules ================================================ [submodule "qhttpserver"] path = qhttpserver url = https://github.com/rschroll/qhttpserver.git ignore = dirty ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014-2015 Chen, Ping-Hsun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Terrarium - UI Prototyping Tool for Coders ========= ![Doge](http://i.imgur.com/Z0KMIaf.png) Terrarium is a cross platform QML Playground: the view [renders lively](http://i.imgur.com/MCA641U.gif) as you type in the editor, makes prototyping and experimenting with [QtQuick](http://qt.digia.com/qtquick/) a lot more fun! It monitors changes in its `TextEdit`, and triggers the view to reload source from the local http server. If you're looking for a file system watcher implementation, please refer to [QML LiveReload](https://github.com/penk/qml-livereload). ### More details on http://www.terrariumapp.com ## Download * [iOS](https://itunes.apple.com/us/app/terrarium/id891232736?ls=1&mt=8) * [Android](https://play.google.com/store/apps/details?id=com.terrariumapp.penk.Terrarium) or [download apk](https://github.com/penk/terrarium-app/releases/download/V1.5/TerrariumApp_1.5.apk) * [Mac OSX](https://github.com/penk/terrarium-app/releases/download/V1.5/Terrarium-1.5.dmg) * [Ubuntu Linux](https://github.com/penk/terrarium-app/releases/download/V1.5/terrarium_1.5_amd64.deb) * [Ubuntu Touch](https://github.com/penk/terrarium-app/releases/download/V1.5/com.ubuntu.developer.penk.terrarium_1.5_armhf.click) ## Build Instructions git clone https://github.com/penk/terrarium-app.git cd terrarium-app && git submodule init && git submodule update qmake && make ## Platform Specific Instructions ### For Arch-Linux Just go to AUR: `yaourt -S terrarium-git` ### For Mac OSX/iOS To add icons to iOS build, first generate and open `Terrarium.xcodeproj`, switch AppIcon to use [Assets Catalog](https://developer.apple.com/library/ios/recipes/xcode_help-image_catalog-1.0/Recipe.html), then replace `Terrarium/Images.xcassets/` directory with `platform/ios/Images.xcassets`. As for Mac OSX, refer to `macdeployqt` command in `terrarium-app.pro` file. ### For Ubuntu Desktop/Phone If you're using Qt packages from apt archive instead of [qt-project.org](http://download.qt-project.org/) releases, here's the dependencies: sudo apt-get install qt5-qmake qt5-default qtbase5-dev qtdeclarative5-dev build-essential All `debian/` package information can be found under `platform/ubuntu/` directory, copy it to current path and build the package by: cp -r platform/ubuntu/debian . cp platform/ubuntu/terrarium.desktop . dpkg-buildpackage -b If you're building click package, execute following command on device (for native compile): cp platform/ubuntu/* . click build . And install it pkcon --allow-untrusted install-local com.ubuntu.developer.penk.terrarium_1.5_armhf.click ### For Android First generate your keystore by `keytool` keytool -genkey -v -keystore ../TerrariumApp.keystore -alias TerrariumApp -keyalg RSA -keysize 2048 -validity 10000 then ~/Qt5.4.1/5.4/android_armv7/bin/qmake make make install INSTALL_ROOT=../android-terrarium Build and sign apk by: ~/Qt5.4.1/5.4/android_armv7/bin/androiddeployqt --input \ android-libTerrarium.so-deployment-settings.json \ --output ../android-terrarium --release --sign ../TerrariumApp.keystore TerrariumApp ## Screenshots * [Android 5.0.0](http://i.imgur.com/0X6e6wK.png) * [iOS 8.2](http://i.imgur.com/n2EPoha.png) * [Mac OSX 10.10.2](http://i.imgur.com/Z0KMIaf.png) * [Ubuntu Touch](http://i.imgur.com/KShLea0.png) * [Ubuntu 14.10](http://i.imgur.com/TI2rLIX.png) ## LICENSE Copyright © 2014-2015 Ping-Hsun (penk) Chen The source code is, unless otherwise specified, distributed under the terms of the MIT License. ## CREDITS * [DocumentHandler](https://github.com/khertan/ownNotes) by Benoît HERVIER * [QMLHighligher](https://gitorious.org/aalperts-automatons/bragi) by Alan Alpert * [QHttpServer](https://github.com/rschroll/qhttpserver) by Robert Schroll * [Font Awesome](http://fontawesome.io) by Dave Gandy ================================================ FILE: patches/expose_loader_errorstring_invokable.patch ================================================ --- qtdeclarative-everywhere-src-5.11.1.orig/src/quick/items/qquickloader.cpp +++ qtdeclarative-everywhere-src-5.11.1/src/quick/items/qquickloader.cpp @@ -629,6 +629,25 @@ } } +QString QQuickLoader::errorString() const +{ + Q_D(const QQuickLoader); + QString ret; + if(d->component->errors().isEmpty()) + return ret; + + if (d->component && d->component->isError()) { + const QList errorList = d->component->errors(); + for (const QQmlError &e : errorList) { + ret += e.url().toString() + QLatin1Char(':') + + QString::number(e.line()) + QLatin1Char(' ') + + e.description() + QLatin1Char('\n'); + } + } + + return ret; +} + void QQuickLoaderIncubator::setInitialState(QObject *o) { loader->setInitialState(o); --- qtdeclarative-everywhere-src-5.11.1.orig/src/quick/items/qquickloader_p.h +++ qtdeclarative-everywhere-src-5.11.1/src/quick/items/qquickloader_p.h @@ -76,6 +76,7 @@ void setActive(bool newVal); Q_INVOKABLE void setSource(QQmlV4Function *); + Q_INVOKABLE QString errorString() const; QUrl source() const; void setSource(const QUrl &); ================================================ FILE: platform/android/AndroidManifest.xml ================================================ ================================================ FILE: platform/arch/terrarium.desktop ================================================ [Desktop Entry] Version=1.5 Name=Terrarium GenericName=Terrarium Comment=UI Prototyping Tool for Coders Type=Application Icon=terrarium-app.png Exec=Terrarium Terminal=false MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; X-Ubuntu-Touch=true ================================================ FILE: platform/ios/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-40@2x.png", "scale" : "2x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon-1.png", "scale" : "1x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon@2x-1.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-60@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-Small-1.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-Small@2x-1.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-40.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-40@2x-1.png", "scale" : "2x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50.png", "scale" : "1x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50@2x.png", "scale" : "2x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-72.png", "scale" : "1x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-72@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-76.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-76@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: platform/ios/Images.xcassets/LaunchImage.launchimage/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "1x", "orientation" : "portrait" }, { "idiom" : "iphone", "scale" : "2x", "orientation" : "portrait" }, { "orientation" : "portrait", "idiom" : "iphone", "filename" : "Default-568h@2x.png", "subtype" : "retina4", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "iphone", "minimum-system-version" : "7.0", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "iphone", "filename" : "Default-568h@2x.png", "minimum-system-version" : "7.0", "subtype" : "retina4", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "ipad", "extent" : "to-status-bar", "scale" : "1x" }, { "orientation" : "portrait", "idiom" : "ipad", "extent" : "to-status-bar", "scale" : "2x" }, { "orientation" : "landscape", "idiom" : "ipad", "extent" : "to-status-bar", "scale" : "1x" }, { "orientation" : "landscape", "idiom" : "ipad", "extent" : "to-status-bar", "scale" : "2x" }, { "orientation" : "portrait", "idiom" : "ipad", "minimum-system-version" : "7.0", "extent" : "full-screen", "scale" : "1x" }, { "orientation" : "portrait", "idiom" : "ipad", "minimum-system-version" : "7.0", "extent" : "full-screen", "scale" : "2x" }, { "orientation" : "landscape", "idiom" : "ipad", "minimum-system-version" : "7.0", "extent" : "full-screen", "scale" : "1x" }, { "orientation" : "landscape", "idiom" : "ipad", "minimum-system-version" : "7.0", "extent" : "full-screen", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: platform/ios/Info.plist ================================================ CFBundleDisplayName ${PRODUCT_NAME} CFBundleExecutable Terrarium CFBundleGetInfoString Created by Qt/QMake CFBundleIcons CFBundleIcons~ipad CFBundleIdentifier com.terrariumapp.${PRODUCT_NAME:rfc1034identifier} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString 1.5 CFBundleSignature ???? CFBundleVersion 1.5.1 LSRequiresIPhoneOS NOTE This file was generated by Qt/QMake. UIStatusBarHidden UIStatusBarHidden~ipad UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: platform/mac/Info.plist ================================================ CFBundleExecutable Terrarium CFBundleIconFile icon.icns CFBundleIdentifier com.terrariumapp.terrarium CFBundleInfoDictionaryVersion 1.0 CFBundleName Terrarium CFBundlePackageType APPL CFBundleShortVersionString 1.0 NSPrincipalClass NSApplication NSHighResolutionCapable True ================================================ FILE: platform/ubuntu/debian/changelog ================================================ terrarium (1.5) trusty; urgency=medium * Bump version -- Ping-Hsun Chen (penk) Mon, 16 Mar 2015 11:10:32 +0800 terrarium (0.1) trusty; urgency=medium * Initial commit -- Ping-Hsun Chen (penk) Tue, 17 Jun 2014 10:19:26 +0000 ================================================ FILE: platform/ubuntu/debian/compat ================================================ 8 ================================================ FILE: platform/ubuntu/debian/control ================================================ Source: terrarium Priority: extra Maintainer: Ping-Hsun Chen (penk) Build-Depends: debhelper (>= 8.0.0), qt5-qmake, qt5-default, qtbase5-dev, qtdeclarative5-dev Standards-Version: 3.9.4 Section: misc Homepage: http://www.terrariumapp.com Package: terrarium Section: misc Architecture: i386 amd64 armhf Depends: ${misc:Depends}, ${shlibs:Depends}, qmlscene, libqt5sql5-sqlite, qtdeclarative5-qtquick2-plugin, qtdeclarative5-window-plugin, Description: Live QML Editor and Viewer Terrarium is an open source QML Playgrounds ================================================ FILE: platform/ubuntu/debian/copyright ================================================ Files: * Copyright: 2014 Ping-Hsun Chen (penk) License: MIT License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: platform/ubuntu/debian/install ================================================ terrarium.desktop /usr/share/applications terrarium-app.png /usr/share/terrarium/ Terrarium /usr/bin/ ================================================ FILE: platform/ubuntu/debian/rules ================================================ #!/usr/bin/make -f # Uncomment this to turn on verbose mode. # #export DH_VERBOSE=1 %: dh $@ ================================================ FILE: platform/ubuntu/debian/source/format ================================================ 3.0 (native) ================================================ FILE: platform/ubuntu/manifest.json ================================================ { "description": "UI Prototyping Tool for Coders", "framework": "ubuntu-sdk-14.04", "architecture": "armhf", "hooks": { "terrarium": { "apparmor": "terrarium.json", "desktop": "terrarium.desktop" } }, "maintainer": "Penk Chen ", "name": "com.ubuntu.developer.penk.terrarium", "title": "Terrarium", "version": "1.5" } ================================================ FILE: platform/ubuntu/terrarium.desktop ================================================ [Desktop Entry] Version=1.5 Name=Terrarium GenericName=Terrarium Comment=UI Prototyping Tool for Coders Type=Application Icon=./terrarium-app.png Exec=./Terrarium Terminal=false MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; X-Ubuntu-Touch=true ================================================ FILE: platform/ubuntu/terrarium.json ================================================ { "policy_groups": [ "networking", "sensors", "webview" ], "policy_version": 1.1 } ================================================ FILE: qml/BottomBar.qml ================================================ import QtQuick 2.0 Rectangle { anchors { bottom: parent.bottom left: background.left margins: 5 * scaleRatio } radius: 0.5 * height height: 50 * scaleRatio width: height gradient: Gradient { GradientStop { position: 0.0; color: "#70787F" } GradientStop { position: 1.0; color: "#383C40" } } CustomButton { id: viewSwitchButton anchors.fill: parent //anchors { top: parent.top; left: parent.left } anchors.margins: -20 //os_type[platform] == 'ios' ? 12 : 0 // "\uf121" : fa-code, to editor // "\uf0db" : fa-columns, to splitted view // "\uf144" : fa-play-circle, to viewer icon.text: if (splitState=='viewer' && root.width * scaleRatio > 600) { "\uf0db" } else if (splitState=='viewer') { "\uf121" } else if (splitState=='editor') { "\uf144" } else if (splitState=='splitted') { "\uf121" } else "\uf144" defaultColor: "#CAD8E5" onClicked: { // splitted -> editor -> viewer if (splitState == 'editor') splitState = 'viewer'; else if (splitState == 'viewer' && root.width * scaleRatio > 600) splitState = 'splitted'; else splitState = 'editor'; view.state = splitState; } } } ================================================ FILE: qml/CustomButton.qml ================================================ import QtQuick 2.0 Item { id: button width: 30 * scaleRatio; height: 30 * scaleRatio; property alias icon: buttonIcon property variant defaultColor: "#CAD8E5" signal clicked() anchors { margins: 10 } Text { id: buttonIcon anchors.centerIn: parent font { family: fontAwesome.name; pointSize: os_type[platform] == 'ios' ? 32 : 26 } color: defaultColor MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: button.clicked() } } } ================================================ FILE: qml/HttpServer.qml ================================================ import QtQuick 2.0 import HttpServer 1.0 HttpServer { id: server Component.onCompleted: listen(platformIP, 5000) onNewRequest: { var route = /^\/\?/; if (request.url.toString().match(/\/update\?/)) { editor.text = decodeURI(request.url.toString().replace(/\/update\?/, "")).replace(/%23/g, '#'); console.log(editor.text) response.writeHead(200) response.end() reloadView(); } else if ( route.test(request.url) ) { response.writeHead(200) response.write(editor.text) response.end() } else { response.writeHead(404) response.end() } } } ================================================ FILE: qml/NaviBar.qml ================================================ import QtQuick 2.0 import QtGraphicalEffects 1.0 Rectangle { id: navigationBar width: parent.width height: 44 * scaleRatio anchors { bottom: parent.bottom left: parent.left } color: "white" Rectangle { id: repeater border.color: "#007edf" width: 240 * scaleRatio height: 29 * scaleRatio anchors.centerIn: parent radius: 5 smooth: true visible: false Row { Rectangle { width: 80 *scaleRatio ; height: 29 * scaleRatio color: splitState == 'editor' ? "#007edf" : "transparent" Text { text: "Editor" color: splitState == 'editor' ? "white" : '#007edf' anchors.centerIn: parent font.pointSize: 15 } } Rectangle { width: 80 * scaleRatio; height: 29 * scaleRatio border.width: 1 border.color: "#007edf" color: splitState == 'splitted' ? "#007edf" : "transparent" Text { text: "Split" color: splitState == 'splitted' ? "white" : "#007edf" anchors.centerIn: parent font.pointSize: 15 } } Rectangle { width: 80 * scaleRatio; height: 29 * scaleRatio color: splitState == 'viewer' ? "#007edf" : "transparent" Text { text: "Viewer" color: splitState == 'viewer' ? "white" : "#007edf" anchors.centerIn: parent font.pointSize: 15 } } } } Rectangle { id: mask width: repeater.width height: repeater.height anchors.fill: repeater radius: 5 } OpacityMask { visible: (parent.state === 'view') anchors.fill: repeater source: repeater maskSource: mask } Row { anchors.centerIn: parent visible: (parent.state === 'view') MouseArea { width: 80 * scaleRatio height: 29 * scaleRatio onPressed: splitState = 'editor' } MouseArea { width: 80 * scaleRatio height: 29 * scaleRatio onPressed: splitState = 'splitted' } MouseArea { width: 80 * scaleRatio height: 29 * scaleRatio onPressed: splitState = 'viewer' } } Text { visible: (parent.state == 'selection') anchors { left: parent.left verticalCenter: parent.verticalCenter margins: 10 * scaleRatio } font { family: fontAwesome.name; pointSize: 26 } text: "\uf057" color: 'grey' MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: { navigationBar.state = 'view'; editor.deselect(); } } } Row { anchors.centerIn: parent visible: (parent.state === 'selection') spacing: 20 * scaleRatio Text { visible: (editor.selectionStart !== editor.selectionEnd) color: "#007edf" font.pointSize: 17 text: "Cut" MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: editor.cut() } } Text { visible: (editor.selectionStart !== editor.selectionEnd) color: "#007edf" font.pointSize: 17 text: "Copy" MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: { editor.copy() editor.deselect() } } } Text { visible: (editor.selectionStart === editor.selectionEnd) color: "#007edf" font.pointSize: 17 text: "Select" MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: editor.selectWord() } } Text { visible: (editor.selectionStart === editor.selectionEnd) color: "#007edf" font.pointSize: 17 text: "Select All" MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: editor.selectAll() } } Text { visible: (editor.canPaste === true) color: "#007edf" font.pointSize: 17 text: "Paste" MouseArea { anchors.fill: parent anchors.margins: -5 * scaleRatio onPressed: editor.paste() } } } states: [ State { name: "view" }, State { name: "selection" } ] } ================================================ FILE: qml/QtWebKit.qml ================================================ import QtWebKit 3.0 import QtWebKit.experimental 1.0 WebView {} ================================================ FILE: qml/QtWebView.qml ================================================ import QtWebView 1.0 WebView {} ================================================ FILE: qml/assets.qrc ================================================ main.qml HttpServer.qml BottomBar.qml NaviBar.qml CustomButton.qml fontawesome-webfont.ttf shadow.png QtWebKit.qml QtWebView.qml ================================================ FILE: qml/main.qml ================================================ import QtQuick 2.0 import QtQuick.Window 2.0 import HttpServer 1.0 import QtQuick.LocalStorage 2.0 import DocumentHandler 1.0 Window { id: root width: Screen.width height: Screen.height visible: true title: "Terrarium - UI Prototyping Tool for Coders" property variant httpServer: {} property variant httpd: {} property string splitState: (root.width * scaleRatio > 600) ? 'splitted' : 'editor' property variant os_type: { '0': 'macx', '1': 'ios', '2': 'android', '3': 'linux', '4': 'default' } property variant platformSetting: { 'ios': { 'lineNumberSpacing': -1, 'lineNumberPadding' : 20, 'defaultFont': 'Courier New' }, 'macx': { 'lineNumberSpacing': -1, 'lineNumberPadding' : 20, 'defaultFont': 'Menlo' }, 'android': { 'lineNumberSpacing': 0, 'lineNumberPadding' : 20, 'defaultFont': 'Droid Sans Mono' }, 'linux': { 'lineNumberSpacing': 0, 'lineNumberPadding' : 20, 'defaultFont': 'Droid Sans Mono' }, 'default': { 'lineNumberSpacing': 0, 'lineNumberPadding' : 20, 'defaultFont': 'Droid Sans Mono' }, } property variant lineNumberPadding: platformSetting[os_type[platform]]['lineNumberPadding'] property variant lineNumberSpacing: platformSetting[os_type[platform]]['lineNumberSpacing'] property variant scaleRatio: Screen.pixelDensity.toFixed(0) / 5 FontLoader { id: fontAwesome; source: "fontawesome-webfont.ttf" } Component.onCompleted: { // FIXME: workaround for Ubuntu Phone if ((scaleRatio < 1) && (os_type[platform]==='linux')) scaleRatio = 2; httpServer = Qt.createComponent("HttpServer.qml"); if (httpServer.status == Component.Ready) { httpd = httpServer.createObject(root, {'id': 'httpd'}); timer.running = true; } else { console.log('error loading http server') } var db = getDatabase(); db.transaction( function(tx) { var result = tx.executeSql("SELECT * FROM previous"); for (var i=0; i < result.rows.length; i++) { editor.text = result.rows.item(i).editor } tx.executeSql("DROP TABLE IF EXISTS previous"); } ); } Component.onDestruction: { saveContent(); } function saveContent() { var db = getDatabase(); db.transaction( function(tx) { tx.executeSql('insert into previous values (?);', editor.text); } ); } function getDatabase() { var db = LocalStorage.openDatabaseSync("terrarium", "1.0", "file saving db", 100000); db.transaction(function(tx) {tx.executeSql('CREATE TABLE IF NOT EXISTS previous (editor TEXT)'); }); return db; } Timer { id: timer interval: 500; running: false; repeat: false onTriggered: reloadView() } function reloadView() { viewLoader.setSource('http://'+platformIP+':5000/?'+Math.random()) // workaround for cache } NaviBar { state: "view" id: navibar z: 2 } Item { id: view state: root.splitState width: root.width/2 height: root.height anchors { top: parent.top; right: parent.right; bottom: navibar.top; } visible: opacity > 0 ? true : false Rectangle { color: 'grey' visible: errorMessage.text != "" anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: Screen.top anchors.bottomMargin: errorMessage.text == "" ? 0 : -height width: parent.width height: errorMessage.height Behavior on anchors.bottomMargin { NumberAnimation { duration: 150; easing.type: Easing.OutQuad } } Text { id: errorMessage anchors { left: parent.left; right: parent.right; top: parent.top; margins: 20; topMargin: 10 } font.pointSize: 20 wrapMode: Text.WordWrap text: "" } } Loader { id: viewLoader anchors.fill: parent property variant errorLineNumber: 0 onStatusChanged: { if (viewLoader.status == Loader.Error) { errorMessage.text = viewLoader.errorString().replace(/http:\/\/.*:5000\/\?.*?:/g, "Line: "); // restart http server when connection refused var connectionRefused = /Connection refused/; if (connectionRefused.test(errorMessage.text)) { httpd.destroy(); httpd = httpServer.createObject(root, {'id': 'httpd'}); saveContent(); reloadView(); } errorLineNumber = errorMessage.text.match(/^Line: (.*?) /)[1]; lineNumberRepeater.itemAt(errorLineNumber - 1).bgcolor = 'red' } else { errorMessage.text = ""; if (errorLineNumber > 0) lineNumberRepeater.itemAt(errorLineNumber - 1).bgcolor = 'transparent' } } } states: [ State { name: "splitted" PropertyChanges { target: view; width: root.width/2 } PropertyChanges { target: view; opacity: 1 } PropertyChanges { target: background; width: root.width/2 } PropertyChanges { target: background; opacity: 1 } }, State { name: "editor" PropertyChanges { target: view; width: 0 } PropertyChanges { target: view; opacity: 0 } PropertyChanges { target: background; width: root.width } PropertyChanges { target: background; opacity: 1 } }, State { name: "viewer" PropertyChanges { target: view; width: root.width } PropertyChanges { target: view; opacity: 1 } PropertyChanges { target: background; width: 0 } PropertyChanges { target: background; opacity: 0 } } ] transitions: [ Transition { to: "*" NumberAnimation { target: view; properties: "width"; duration: 300; easing.type: Easing.InOutQuad; } NumberAnimation { target: background; properties: "width"; duration: 300; easing.type: Easing.InOutQuad; } } ] } Rectangle { id: background width: root.width/2 height: root.height anchors { top: parent.top; left: parent.left; bottom: navibar.top} color: '#1d1f21' visible: opacity > 0 ? true : false Flickable { id: flickable anchors { fill: parent; } flickableDirection: Flickable.VerticalFlick boundsBehavior: Flickable.DragOverBounds contentWidth: parent.width contentHeight: editor.height clip: true Column { id: lineNumber anchors { margins: 20; left: parent.left; top: parent.top } spacing: lineNumberSpacing Repeater { id: lineNumberRepeater model: editor.lineCount Text { property alias bgcolor: rect.color width: 20 text: index + 1 color: 'lightgray' font.pointSize: editor.font.pointSize horizontalAlignment: TextEdit.AlignHCenter Rectangle { id: rect color: 'transparent' anchors.fill: parent opacity: 0.5 } } } } Rectangle { id: editorCurrentLineHighlight anchors { left: lineNumber.right margins: lineNumberPadding } visible: editor.focus width: editor.width height: editor.cursorRectangle.height y: editor.cursorRectangle.y + lineNumberPadding color: '#454545' } TextEdit { id: editor anchors { margins: lineNumberPadding left: lineNumber.right; right: parent.right; top: parent.top } wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere; renderType: Text.NativeRendering onTextChanged: timer.restart(); onSelectedTextChanged: { if (editor.selectedText === "") { navibar.state = 'view' } } // FIXME: stupid workaround for indent Keys.onPressed: { if (event.key == Qt.Key_BraceRight) { editor.select(0, cursorPosition) var previousContent = editor.selectedText.split(/\r\n|\r|\n/) editor.deselect() var currentLine = previousContent[previousContent.length - 1] var leftBrace = /{/, rightBrace = /}/; if (!leftBrace.test(currentLine)) { editor.remove(cursorPosition, cursorPosition - currentLine.length); currentLine = currentLine.toString().replace(/ {1,4}$/, ""); editor.insert(cursorPosition, currentLine); } } } Keys.onReturnPressed: { editor.select(0, cursorPosition) var previousContent = editor.selectedText.split(/\r\n|\r|\n/) editor.deselect() var currentLine = previousContent[previousContent.length - 1] var leftBrace = /{/, rightBrace = /}/; editor.insert(cursorPosition, "\n") var whitespaceAppend = currentLine.match(new RegExp(/^[ \t]*/)) // whitespace if (leftBrace.test(currentLine)) // indent whitespaceAppend += " "; editor.insert(cursorPosition, whitespaceAppend) } // style from Atom dark theme: // https://github.com/atom/atom-dark-syntax/blob/master/stylesheets/syntax-variables.less color: '#c5c8c6' selectionColor: '#0C75BC' selectByMouse: true font { pointSize: 18; family: platformSetting[os_type[platform]]['defaultFont'] } text: documentHandler.text inputMethodHints: Qt.ImhNoPredictiveText DocumentHandler { id: documentHandler target: editor Component.onCompleted: { documentHandler.text = "import QtQuick 2.0\n\nRectangle { \n color: '#FEEB75'" + "\n Text { \n anchors.centerIn: parent" + "\n text: 'Hello, World!' \n } \n}" } } // FIXME: add selection / copy / paste popup MouseArea { id: handler // FIXME: disable on desktop enabled: os_type[platform] != 'macx' anchors.fill: parent propagateComposedEvents: true onPressed: { editor.cursorPosition = parent.positionAt(mouse.x, mouse.y); editor.focus = true navibar.state = 'view' Qt.inputMethod.show(); } onPressAndHold: { navibar.state = 'selection' Qt.inputMethod.hide(); } onDoubleClicked: { editor.selectWord() navibar.state = 'selection' } } } // end of editor } } Image { fillMode: Image.TileHorizontally source: "shadow.png" width: navibar.width anchors.bottom: navibar.top height: 6 } } ================================================ FILE: src/documenthandler.cpp ================================================ /**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Quick Controls module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "documenthandler.h" #include #include #include #include DocumentHandler::DocumentHandler() : m_target(0) , m_doc(0) , m_cursorPosition(-1) , m_selectionStart(0) , m_selectionEnd(0) , m_highlighter(0) { } void DocumentHandler::setTarget(QQuickItem *target) { m_doc = 0; m_highlighter = 0; m_target = target; if (!m_target) return; QVariant doc = m_target->property("textDocument"); if (doc.canConvert()) { QQuickTextDocument *qqdoc = doc.value(); if (qqdoc) { m_doc = qqdoc->textDocument(); m_highlighter = new QMLHighlighter(m_doc); //m_highlighter = new Highlighter(m_doc); } } emit targetChanged(); } void DocumentHandler::setText(const QString &arg) { if (m_text != arg) { m_text = arg; emit textChanged(); } } QString DocumentHandler::text() const { return m_text; } void DocumentHandler::setCursorPosition(int position) { if (position == m_cursorPosition) return; m_cursorPosition = position; reset(); } void DocumentHandler::reset() { } QTextCursor DocumentHandler::textCursor() const { QTextCursor cursor = QTextCursor(m_doc); if (m_selectionStart != m_selectionEnd) { cursor.setPosition(m_selectionStart); cursor.setPosition(m_selectionEnd, QTextCursor::KeepAnchor); } else { cursor.setPosition(m_cursorPosition); } return cursor; } void DocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format) { QTextCursor cursor = textCursor(); if (!cursor.hasSelection()) cursor.select(QTextCursor::WordUnderCursor); cursor.mergeCharFormat(format); } void DocumentHandler::setSelectionStart(int position) { m_selectionStart = position; } void DocumentHandler::setSelectionEnd(int position) { m_selectionEnd = position; } ================================================ FILE: src/documenthandler.h ================================================ /**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Quick Controls module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef DOCUMENTHANDLER_H #define DOCUMENTHANDLER_H #include #include "qmlhighlighter.h" #include #include #include QT_BEGIN_NAMESPACE class QTextDocument; QT_END_NAMESPACE class DocumentHandler : public QObject { Q_OBJECT Q_ENUMS(HAlignment) Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged) Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged) Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged) Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) public: DocumentHandler(); QQuickItem *target() { return m_target; } void setTarget(QQuickItem *target); void setCursorPosition(int position); void setSelectionStart(int position); void setSelectionEnd(int position); int cursorPosition() const { return m_cursorPosition; } int selectionStart() const { return m_selectionStart; } int selectionEnd() const { return m_selectionEnd; } QString text() const; public Q_SLOTS: void setText(const QString &arg); Q_SIGNALS: void targetChanged(); void cursorPositionChanged(); void selectionStartChanged(); void selectionEndChanged(); void textChanged(); private: void reset(); QTextCursor textCursor() const; void mergeFormatOnWordOrSelection(const QTextCharFormat &format); QQuickItem *m_target; QTextDocument *m_doc; int m_cursorPosition; int m_selectionStart; int m_selectionEnd; QMLHighlighter *m_highlighter; QString m_text; }; #endif ================================================ FILE: src/main.cpp ================================================ #include #include #include #if QT_VERSION > QT_VERSION_CHECK(5, 1, 0) #include #endif #include "qhttpserver/src/qhttpserver.h" #include "qhttpserver/src/qhttprequest.h" #include "qhttpserver/src/qhttpresponse.h" #include "qhttpserver/src/qhttpconnection.h" #include "documenthandler.h" #include "quickitemgrabber.h" #if USE_WEBENGINE #include #endif int main(int argc, char *argv[]) { QStringList imports, plugins; QGuiApplication app(argc, argv); app.setApplicationName("Terrarium"); app.setOrganizationName("terrariumapp"); app.setOrganizationDomain("terrariumapp.com"); #if defined(Q_OS_MACX) int platformId = 0; #elif defined(Q_OS_IOS) int platformId = 1; #elif defined(Q_OS_ANDROID) int platformId = 2; #elif defined(Q_OS_LINUX) int platformId = 3; #else int platformId = 4; #endif qmlRegisterType("HttpServer", 1, 0, "HttpServer"); qmlRegisterType("DocumentHandler", 1, 0, "DocumentHandler"); qmlRegisterUncreatableType("HttpServer", 1, 0, "HttpRequest", "Do not create HttpRequest directly"); qmlRegisterUncreatableType("HttpServer", 1, 0, "HttpResponse", "Do not create HttpResponse directly"); QString platformIP; foreach (const QHostAddress &address, QNetworkInterface::allAddresses()) { if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress(QHostAddress::LocalHost)) platformIP = address.toString(); } // Handle command line arguments const QStringList arguments = QCoreApplication::arguments(); for (int i = 1, size = arguments.size(); i < size; ++i) { const QString lowerArgument = arguments.at(i).toLower(); if (lowerArgument == QLatin1String("-i") && i + 1 < size) { imports.append(arguments.at(++i)); } else if (lowerArgument == QLatin1String("-p") && i + 1 < size) { plugins.append(arguments.at(++i)); } } #if USE_WEBENGINE QtWebEngine::initialize(); #endif #if QT_VERSION > QT_VERSION_CHECK(5, 1, 0) QQmlApplicationEngine engine; for(int i = 0; i < imports.size(); ++i) { engine.addImportPath(imports[i]); } for(int i = 0; i < plugins.size(); ++i) { engine.addPluginPath(plugins[i]); } engine.rootContext()->setContextProperty("platform", QVariant::fromValue(platformId)); engine.rootContext()->setContextProperty("platformIP", QVariant::fromValue(platformIP)); engine.rootContext()->setContextProperty("Grabber",new QuickItemGrabber(&app)); engine.load(QUrl("qrc:///main.qml")); #else QQuickView view; view.engine()->rootContext()->setContextProperty("platform", QVariant::fromValue(platformId)); view.engine()->rootContext()->setContextProperty("platformIP", QVariant::fromValue(platformIP)); view.setSource(QUrl("qrc:///main.qml")); view.show(); #endif return app.exec(); } ================================================ FILE: src/qmlhighlighter.cpp ================================================ #include "qmlhighlighter.h" #include void QMLHighlighter::highlightBlock(const QString &text) { QTextCharFormat keywordFormat; keywordFormat.setForeground(QColor("#d7ffaf")); // Identifier QTextCharFormat typeFormat; typeFormat.setForeground(QColor("#afffff")); // Type QTextCharFormat commentFormat; commentFormat.setForeground(QColor("#8a8a8a")); // Comment QTextCharFormat numericConstantFormat; numericConstantFormat.setForeground(QColor("#ffffd7")); // Constant QTextCharFormat stringConstantFormat; stringConstantFormat.setForeground(QColor("#ffffd7")); QRegExp type("\\b[A-Z][A-Za-z]+\\b"); QRegExp numericConstant("[0-9]+\\.?[0-9]*"); QRegExp stringConstant("['\"].*['\"]");//Not multiline strings, but they're rare QRegExp lineComment("//[^\n]*"); QRegExp startComment("/\\*"); QRegExp endComment("\\*/"); applyBasicHighlight(text, type, typeFormat); applyBasicHighlight(text, numericConstant, numericConstantFormat); applyBasicHighlight(text, stringConstant, stringConstantFormat); applyBasicHighlight(text, lineComment, commentFormat); setCurrentBlockState(0); int startIndex = 0; if (previousBlockState() != 1) startIndex = text.indexOf(startComment); while (startIndex >= 0) { int endIndex = text.indexOf(endComment, startIndex); int commentLength; if (endIndex == -1) { setCurrentBlockState(1); commentLength = text.length() - startIndex; } else { commentLength = endIndex - startIndex + endComment.matchedLength(); } setFormat(startIndex, commentLength, commentFormat); startIndex = text.indexOf(startComment, startIndex + commentLength); } } void QMLHighlighter::applyBasicHighlight(const QString &text, QRegExp &re, QTextCharFormat &format) { int index = text.indexOf(re); while (index >= 0) { int length = re.matchedLength(); setFormat(index, length, format); index = text.indexOf(re, index + length); } } ================================================ FILE: src/qmlhighlighter.h ================================================ #include class QMLHighlighter : public QSyntaxHighlighter { Q_OBJECT public: //BUG: QSyntaxHighlighter(0) crashes! QMLHighlighter(QObject* parent=new QObject()) : QSyntaxHighlighter(parent) { } QMLHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { } protected: virtual void highlightBlock(const QString &text); private: void applyBasicHighlight(const QString &text, QRegExp &re, QTextCharFormat &format); }; ================================================ FILE: src/quickitemgrabber.cpp ================================================ /** Author: Ben Lau (https://github.com/benlau) */ #include #include #include #include #include "quickitemgrabber.h" #if (QT_VERSION < QT_VERSION_CHECK(5, 4, 0)) #include #include #else // Added since Qt 5.4 #include #endif QuickItemGrabber::QuickItemGrabber(QObject *parent) : QObject(parent) { m_busy = false; m_ready = false; } bool QuickItemGrabber::busy() const { return m_busy; } bool QuickItemGrabber::grab(QQuickItem *target,QSize targetSize) { if (m_busy || target == 0 || !target->window() || !target->window()->isVisible() ) { return false; } m_ready = false; m_targetSize = targetSize; m_target = target; m_window = target->window(); if (m_targetSize.isEmpty()) { m_targetSize = QSize(m_target->width(),m_target->height()); } #if (QT_VERSION < QT_VERSION_CHECK(5, 4, 0)) QQuickItemPrivate::get(m_target)->refFromEffectItem(false); m_target->window()->update(); connect(m_window.data(),SIGNAL(beforeSynchronizing()), this,SLOT(ready()),Qt::DirectConnection); connect(m_window.data(),SIGNAL(afterRendering()), this,SLOT(capture()),Qt::DirectConnection); #else result = target->grabToImage(m_targetSize); if (result.isNull()) { qDebug() << "Can't grab target item"; return false; } connect(result.data(),SIGNAL(ready()), this,SLOT(onGrabResultReady())); #endif setBusy(true); return true; } bool QuickItemGrabber::save(QString filename) { if (m_image.isNull()) { qWarning() << "QuickItemGrabber::save() - The image is null"; return false; } return m_image.save(filename); } void QuickItemGrabber::ready() { m_ready = true; } #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) void QuickItemGrabber::onGrabResultReady() { QImage image = result->image(); setImage(image); result.clear(); setBusy(false); emit grabbed(); } #endif QImage QuickItemGrabber::image() const { return m_image; } #if (QT_VERSION < QT_VERSION_CHECK(5, 4, 0)) void QuickItemGrabber::capture() { if (!m_ready) { // It is not ready yet return; } if (m_target) { // Just in case the item is destroyed before rendering completed QOpenGLContext* context = QOpenGLContext::currentContext(); QQuickShaderEffectTexture *m_texture = new QQuickShaderEffectTexture(m_target); m_texture->setItem(QQuickItemPrivate::get(m_target)->itemNode()); // Set the source rectangle QSize sourceSize; sourceSize = QSize(m_target->width(),m_target->height()); m_texture->setRect(QRectF(0, sourceSize.height(), sourceSize.width(), -sourceSize.height())); QSize maxSize = maxTextureSize(); /* if (!maxSize.isValid()) { GLint param; QOpenGLFunctions glFuncs(context); glFuncs.glGetIntegerv(GL_MAX_TEXTURE_SIZE,¶m); maxSize = QSize(param,param); setMaxTextureSize(maxSize); } QSize textureSize = m_targetSize; if (maxSize.isValid() && (textureSize.width() > maxSize.width() || textureSize.height() > maxSize.height())) { FVRectToRect scaler; scaler.scaleToFit(textureSize,maxSize); // The required size is larger than max texture size. qDebug() << "Downgrade target image from" << textureSize << scaler.transformedRect().toRect().size(); textureSize = scaler.transformedRect().toRect().size(); } */ QSize expectedSize = textureSize(); QSGContext *sg = QSGRenderContext::from(context)->sceneGraphContext(); const QSize minSize = sg->minimumFBOSize(); m_texture->setSize(QSize(qMax(minSize.width(), expectedSize.width()), qMax(minSize.height(), expectedSize.height()))); m_texture->scheduleUpdate(); m_texture->updateTexture(); QImage image = m_texture->toImage(); setImage(image); delete m_texture; m_texture = 0; } disconnect(m_window.data(), SIGNAL(afterRendering()), this, SLOT(capture())); disconnect(m_window.data(), SIGNAL(beforeSynchronizing()), this, SLOT(ready())); setBusy(false); emit grabbed(); } #endif void QuickItemGrabber::setBusy(bool value) { if (m_busy != value) { m_busy = value; emit busyChanged(); } } void QuickItemGrabber::setImage(QImage value) { m_image = value; emit imageChanged(); } void QuickItemGrabber::clear() { setImage(QImage()); } ================================================ FILE: src/quickitemgrabber.h ================================================ /** Author: Ben Lau (https://github.com/benlau) */ #ifndef QUICKITEMGRABBER_H #define QUICKITEMGRABBER_H #include #include #include #include /// QuickItemGrabber grabs QQuickItem into QImage class QuickItemGrabber : public QObject { Q_OBJECT /// "Busy" flag. It is TRUE if the grabber is running. It won't accept another request when busy. Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) /// The grabbed image Q_PROPERTY(QImage image READ image NOTIFY imageChanged) public: explicit QuickItemGrabber(QObject *parent = 0); bool busy() const; QImage image() const; /// Grab the target item and save to "image" property Q_INVOKABLE bool grab(QQuickItem* target,QSize targetSize = QSize()); /// Save the grabbed image into file. It is a blocked call. Q_INVOKABLE bool save(QString filename); /// Clear the captured image. Q_INVOKABLE void clear(); signals: void busyChanged(); void imageChanged(); void grabbed(); void maxTextureSizeChanged(); private: // Ready for capture Q_INVOKABLE void ready(); #if (QT_VERSION < QT_VERSION_CHECK(5, 4, 0)) Q_INVOKABLE void capture(); #else Q_INVOKABLE void onGrabResultReady(); QSharedPointer result; #endif void setBusy(bool value); void setImage(QImage value); bool m_busy; bool m_ready; QSize m_targetSize; QPointer m_target; QPointer m_window; QImage m_image; }; #endif // QUICKITEMGRABBER_H ================================================ FILE: terrarium-app.pro ================================================ TEMPLATE = app TARGET = Terrarium QT += qml quick network sql webengine { QT += webengine DEFINES += USE_WEBENGINE } SOURCES += src/main.cpp \ src/qmlhighlighter.cpp \ src/documenthandler.cpp \ qhttpserver/src/qhttpconnection.cpp \ qhttpserver/src/qhttprequest.cpp \ qhttpserver/src/qhttpresponse.cpp \ qhttpserver/src/qhttpserver.cpp \ qhttpserver/http-parser/http_parser.c \ src/quickitemgrabber.cpp HEADERS += qhttpserver/src/qhttpserver.h \ qhttpserver/src//qhttpresponse.h \ qhttpserver/src//qhttprequest.h \ src/qmlhighlighter.h \ src/documenthandler.h \ qhttpserver/src//qhttpconnection.h \ src/quickitemgrabber.h INCLUDEPATH += ./qhttpserver/http-parser/ RESOURCES += qml/assets.qrc android { ANDROID_PACKAGE_SOURCE_DIR = ./platform/android } macx { QMAKE_MAC_SDK = macosx10.10 QMAKE_INFO_PLIST = platform/mac/Info.plist ICON = platform/mac/icon.icns #QMAKE_POST_LINK += macdeployqt Terrarium.app/ -qmldir=qml/ -verbose=1 -dmg } ios { QMAKE_INFO_PLIST = platform/ios/Info.plist }