Merge pull request #11303 from vladest/webbrowser_updates

Webbrowser updates
This commit is contained in:
Brad Hefta-Gaub 2017-11-19 11:11:09 -08:00 committed by GitHub
commit ff8ca6da4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 469 additions and 142 deletions

View file

@ -1,7 +1,7 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 1.2 import QtQuick.Controls 1.2
import QtWebChannel 1.0 import QtWebChannel 1.0
import QtWebEngine 1.2 import QtWebEngine 1.5
import "controls-uit" import "controls-uit"
import "styles" as HifiStyles import "styles" as HifiStyles

View file

@ -8,7 +8,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
import QtQuick 2.5 import QtQuick 2.7
import QtWebEngine 1.5 import QtWebEngine 1.5
WebEngineView { WebEngineView {

View file

@ -1,6 +1,6 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtWebEngine 1.1; import QtWebEngine 1.5;
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import "../desktop" as OriginalDesktop import "../desktop" as OriginalDesktop

View file

@ -1,3 +1,4 @@
// //
// WebBrowser.qml // WebBrowser.qml
// //
@ -9,12 +10,12 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
import QtQuick 2.5 import QtQuick 2.7
import QtQuick.Controls 1.5 as QQControls import QtQuick.Controls 2.2 as QQControls
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0
import QtWebEngine 1.2 import QtWebEngine 1.5
import QtWebChannel 1.0 import QtWebChannel 1.0
import "../styles-uit" import "../styles-uit"
@ -22,6 +23,8 @@ import "../controls-uit" as HifiControls
import "../windows" import "../windows"
import "../controls" import "../controls"
import HifiWeb 1.0
Rectangle { Rectangle {
id: root; id: root;
@ -32,214 +35,401 @@ Rectangle {
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
property bool keyboardRaised: false property bool keyboardRaised: false
property bool punctuationMode: false property bool punctuationMode: false
property var suggestionsList: []
readonly property string searchUrlTemplate: "https://www.google.com/search?client=hifibrowser&q=";
WebBrowserSuggestionsEngine {
id: searchEngine
onSuggestions: {
if (suggestions.length > 0) {
suggestionsList = []
suggestionsList.push(addressBarInput.text); //do not overwrite edit text
for(var i = 0; i < suggestions.length; i++) {
suggestionsList.push(suggestions[i]);
}
addressBar.model = suggestionsList
if (!addressBar.popup.visible) {
addressBar.popup.open();
}
}
}
}
Timer {
id: suggestionRequestTimer
interval: 200
repeat: false
onTriggered: {
if (addressBar.editText !== "") {
searchEngine.querySuggestions(addressBarInput.text);
}
}
}
color: hifi.colors.baseGray; color: hifi.colors.baseGray;
// only show the title if loaded through a "loader" function goTo(url) {
//must be valid attempt to open an site with dot
var urlNew = url
if (url.indexOf(".") > 0) {
if (url.indexOf("http") < 0) {
urlNew = "http://" + url;
}
} else {
urlNew = searchUrlTemplate + url
}
addressBar.model = []
//need to rebind if binfing was broken by selecting from suggestions
addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; });
webStack.currentItem.webEngineView.url = urlNew
suggestionRequestTimer.stop();
addressBar.popup.close();
}
Column { Column {
spacing: 2 spacing: 2
width: parent.width; width: parent.width;
RowLayout { RowLayout {
id: addressBarRow
width: parent.width; width: parent.width;
height: 48 height: 48
HifiControls.WebGlyphButton { HifiControls.WebGlyphButton {
enabled: webEngineView.canGoBack enabled: webStack.currentItem.webEngineView.canGoBack || webStack.depth > 1
glyph: hifi.glyphs.backward; glyph: hifi.glyphs.backward;
anchors.verticalCenter: parent.verticalCenter; anchors.verticalCenter: parent.verticalCenter;
size: 38; size: 38;
onClicked: { onClicked: {
webEngineView.goBack() if (webStack.currentItem.webEngineView.canGoBack) {
webStack.currentItem.webEngineView.goBack();
} else if (webStack.depth > 1) {
webStack.pop();
}
} }
} }
HifiControls.WebGlyphButton { HifiControls.WebGlyphButton {
enabled: webEngineView.canGoForward enabled: webStack.currentItem.webEngineView.canGoForward
glyph: hifi.glyphs.forward; glyph: hifi.glyphs.forward;
anchors.verticalCenter: parent.verticalCenter; anchors.verticalCenter: parent.verticalCenter;
size: 38; size: 38;
onClicked: { onClicked: {
webEngineView.goForward() webStack.currentItem.webEngineView.goForward();
} }
} }
QQControls.TextField { QQControls.ComboBox {
id: addressBar id: addressBar
Image { //selectByMouse: true
anchors.verticalCenter: addressBar.verticalCenter; focus: true
x: 5
z: 2 editable: true
id: faviconImage //flat: true
width: 16; height: 16 indicator: Item {}
sourceSize: Qt.size(width, height) background: Item {}
source: webEngineView.icon onActivated: {
goTo(textAt(index));
} }
HifiControls.WebGlyphButton { onHighlightedIndexChanged: {
glyph: webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall; if (highlightedIndex >= 0) {
anchors.verticalCenter: parent.verticalCenter; addressBar.editText = textAt(highlightedIndex)
width: hifi.dimensions.controlLineHeight }
z: 2 }
x: addressBar.width - 28
onClicked: { popup.height: webStack.height
if (webEngineView.loading) {
webEngineView.stop() onFocusChanged: {
} else { if (focus) {
reloadTimer.start() addressBarInput.selectAll();
}
}
contentItem: QQControls.TextField {
id: addressBarInput
leftPadding: 26
rightPadding: hifi.dimensions.controlLineHeight + 5
text: addressBar.editText
placeholderText: qsTr("Enter URL")
font: addressBar.font
selectByMouse: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
onFocusChanged: {
if (focus) {
selectAll();
}
}
Keys.onDeletePressed: {
addressBarInput.text = ""
}
Keys.onPressed: {
if (event.key === Qt.Key_Return) {
goTo(addressBarInput.text);
event.accepted = true;
}
}
Image {
anchors.verticalCenter: parent.verticalCenter;
x: 5
z: 2
id: faviconImage
width: 16; height: 16
sourceSize: Qt.size(width, height)
source: webStack.currentItem.webEngineView.icon
}
HifiControls.WebGlyphButton {
glyph: webStack.currentItem.webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall;
anchors.verticalCenter: parent.verticalCenter;
width: hifi.dimensions.controlLineHeight
z: 2
x: addressBarInput.width - implicitWidth
onClicked: {
if (webStack.currentItem.webEngineView.loading) {
webStack.currentItem.webEngineView.stop();
} else {
webStack.currentItem.reloadTimer.start();
}
} }
} }
} }
style: TextFieldStyle { Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i");
padding {
left: 26; Keys.onPressed: {
right: 26 if (event.key === Qt.Key_Return) {
goTo(addressBarInput.text);
event.accepted = true;
} }
} }
focus: true
onEditTextChanged: {
if (addressBar.editText !== "" && addressBar.editText !== webStack.currentItem.webEngineView.url.toString()) {
suggestionRequestTimer.restart();
} else {
addressBar.model = []
addressBar.popup.close();
}
}
Layout.fillWidth: true Layout.fillWidth: true
text: webEngineView.url editText: webStack.currentItem.webEngineView.url
onAccepted: webEngineView.url = text onAccepted: goTo(addressBarInput.text);
} }
HifiControls.WebGlyphButton { HifiControls.WebGlyphButton {
checkable: true checkable: true
//only QtWebEngine 1.3 checked: webStack.currentItem.webEngineView.audioMuted
//checked: webEngineView.audioMuted
glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted
anchors.verticalCenter: parent.verticalCenter; anchors.verticalCenter: parent.verticalCenter;
width: hifi.dimensions.controlLineHeight width: hifi.dimensions.controlLineHeight
onClicked: { onClicked: {
webEngineView.triggerWebAction(WebEngineView.ToggleMediaMute) webStack.currentItem.webEngineView.audioMuted = !webStack.currentItem.webEngineView.audioMuted
} }
} }
} }
QQControls.ProgressBar { QQControls.ProgressBar {
id: loadProgressBar id: loadProgressBar
style: ProgressBarStyle { background: Rectangle {
background: Rectangle { implicitHeight: 2
color: "#6A6A6A" color: "#6A6A6A"
} }
progress: Rectangle{
contentItem: Item {
implicitHeight: 2
Rectangle {
width: loadProgressBar.visualPosition * parent.width
height: parent.height
color: "#00B4EF" color: "#00B4EF"
} }
} }
width: parent.width; width: parent.width;
minimumValue: 0 from: 0
maximumValue: 100 to: 100
value: webEngineView.loadProgress value: webStack.currentItem.webEngineView.loadProgress
height: 2 height: 2
} }
HifiControls.BaseWebView { Component {
id: webEngineView id: webViewComponent
focus: true Rectangle {
objectName: "tabletWebEngineView" property alias webEngineView: webEngineView
property alias reloadTimer: reloadTimer
url: "http://www.highfidelity.com" property WebEngineNewViewRequest request: null
property real webViewHeight: root.height - loadProgressBar.height - 48 - 4
width: parent.width; property bool isDialog: QQControls.StackView.index > 0
height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight property real margins: isDialog ? 10 : 0
profile: HFWebEngineProfile; color: "#d1d1d1"
property string userScriptUrl: "" QQControls.StackView.onActivated: {
addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; });
// creates a global EventBridge object.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.DocumentCreation
worldId: WebEngineScript.MainWorld
}
// detects when to raise and lower virtual keyboard
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
// User script.
WebEngineScript {
id: userScript
sourceUrl: webEngineView.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
settings.autoLoadImages: true
settings.javascriptEnabled: true
settings.errorPageEnabled: true
settings.pluginsEnabled: true
settings.fullScreenSupportEnabled: false
//from WebEngine 1.3
// settings.autoLoadIconsForPage: false
// settings.touchIconsEnabled: false
onCertificateError: {
error.defer();
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
webEngineView.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)";
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, true);
}
onNewViewRequested: {
if (!request.userInitiated) {
print("Warning: Blocked a popup window.");
}
}
onRenderProcessTerminated: {
var status = "";
switch (terminationStatus) {
case WebEngineView.NormalTerminationStatus:
status = "(normal exit)";
break;
case WebEngineView.AbnormalTerminationStatus:
status = "(abnormal exit)";
break;
case WebEngineView.CrashedTerminationStatus:
status = "(crashed)";
break;
case WebEngineView.KilledTerminationStatus:
status = "(killed)";
break;
} }
print("Render process exited with code " + exitCode + " " + status); onRequestChanged: {
reloadTimer.running = true; if (isDialog && request !== null && request !== undefined) {//is Dialog ?
} request.openIn(webEngineView);
}
}
onWindowCloseRequested: { HifiControls.BaseWebView {
} id: webEngineView
anchors.fill: parent
anchors.margins: parent.margins
Timer { layer.enabled: parent.isDialog
id: reloadTimer layer.effect: DropShadow {
interval: 0 verticalOffset: 8
running: false horizontalOffset: 8
repeat: false color: "#330066ff"
onTriggered: webEngineView.reload() samples: 10
spread: 0.5
}
focus: true
objectName: "tabletWebEngineView"
//profile: HFWebEngineProfile;
profile.httpUserAgent: "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"
property string userScriptUrl: ""
onLoadingChanged: {
if (!loading) {
addressBarInput.cursorPosition = 0 //set input field cursot to beginning
suggestionRequestTimer.stop();
addressBar.popup.close();
}
}
onLinkHovered: {
//TODO: change cursor shape?
}
// creates a global EventBridge object.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.Deferred
worldId: WebEngineScript.MainWorld
}
// detects when to raise and lower virtual keyboard
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
// User script.
WebEngineScript {
id: userScript
sourceUrl: webEngineView.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
settings.autoLoadImages: true
settings.javascriptEnabled: true
settings.errorPageEnabled: true
settings.pluginsEnabled: true
settings.fullScreenSupportEnabled: true
settings.autoLoadIconsForPage: true
settings.touchIconsEnabled: true
onCertificateError: {
error.defer();
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, true);
}
onNewViewRequested: {
if (request.destination == WebEngineView.NewViewInDialog) {
webStack.push(webViewComponent, {"request": request});
} else {
request.openIn(webEngineView);
}
}
onRenderProcessTerminated: {
var status = "";
switch (terminationStatus) {
case WebEngineView.NormalTerminationStatus:
status = "(normal exit)";
break;
case WebEngineView.AbnormalTerminationStatus:
status = "(abnormal exit)";
break;
case WebEngineView.CrashedTerminationStatus:
status = "(crashed)";
break;
case WebEngineView.KilledTerminationStatus:
status = "(killed)";
break;
}
console.error("Render process exited with code " + exitCode + " " + status);
reloadTimer.running = true;
}
onFullScreenRequested: {
if (request.toggleOn) {
webEngineView.state = "FullScreen";
} else {
webEngineView.state = "";
}
request.accept();
}
onWindowCloseRequested: {
webStack.pop();
}
}
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: webEngineView.reload()
}
} }
} }
QQControls.StackView {
id: webStack
width: parent.width;
property real webViewHeight: root.height - loadProgressBar.height - 48 - 4
height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight
Component.onCompleted: webStack.push(webViewComponent, {"webEngineView.url": "https://www.highfidelity.com"});
}
} }
HifiControls.Keyboard { HifiControls.Keyboard {
id: keyboard id: keyboard
raised: parent.keyboardEnabled && parent.keyboardRaised raised: parent.keyboardEnabled && parent.keyboardRaised

View file

@ -203,6 +203,8 @@
#include "commerce/Wallet.h" #include "commerce/Wallet.h"
#include "commerce/QmlCommerce.h" #include "commerce/QmlCommerce.h"
#include "webbrowser/WebBrowserSuggestionsEngine.h"
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
// FIXME seems to be broken. // FIXME seems to be broken.
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
@ -2223,6 +2225,7 @@ void Application::initializeUi() {
QmlCommerce::registerType(); QmlCommerce::registerType();
qmlRegisterType<ResourceImageItem>("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType<ResourceImageItem>("Hifi", 1, 0, "ResourceImageItem");
qmlRegisterType<Preference>("Hifi", 1, 0, "Preference"); qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
qmlRegisterType<WebBrowserSuggestionsEngine>("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine");
auto offscreenUi = DependencyManager::get<OffscreenUi>(); auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->create(); offscreenUi->create();

View file

@ -0,0 +1,88 @@
//
// WebBrowserSuggestionsEngine.cpp
// interface/src/webbrowser
//
// Created by Vlad Stelmahovsky on 30/10/17.
// Copyright 2017 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 "WebBrowserSuggestionsEngine.h"
#include "qregexp.h"
#include <qbuffer.h>
#include <qcoreapplication.h>
#include <qlocale.h>
#include <qnetworkrequest.h>
#include <qnetworkreply.h>
#include <qregexp.h>
#include <qstringlist.h>
#include <QUrlQuery>
#include <QJsonDocument>
#include <NetworkAccessManager.h>
static const QString GoogleSuggestionsUrl = "https://suggestqueries.google.com/complete/search?output=firefox&q=%1";
static const int SUGGESTIONS_LIST_INDEX = 1;
WebBrowserSuggestionsEngine::WebBrowserSuggestionsEngine(QObject* parent)
: QObject(parent)
, _suggestionsReply(0) {
_currentNAM = &NetworkAccessManager::getInstance();
connect(_currentNAM, &QNetworkAccessManager::finished, this, &WebBrowserSuggestionsEngine::suggestionsFinished);
}
WebBrowserSuggestionsEngine::~WebBrowserSuggestionsEngine() {
disconnect(_currentNAM, &QNetworkAccessManager::finished, this, &WebBrowserSuggestionsEngine::suggestionsFinished);
}
void WebBrowserSuggestionsEngine::querySuggestions(const QString &searchString) {
if (_suggestionsReply) {
_suggestionsReply->disconnect(this);
_suggestionsReply->abort();
_suggestionsReply->deleteLater();
_suggestionsReply = 0;
}
QString url = QString(GoogleSuggestionsUrl).arg(searchString);
_suggestionsReply = _currentNAM->get(QNetworkRequest(url));
}
void WebBrowserSuggestionsEngine::suggestionsFinished(QNetworkReply *reply) {
if (reply != _suggestionsReply) {
return; //invalid reply. ignore
}
const QByteArray response = _suggestionsReply->readAll();
_suggestionsReply->close();
_suggestionsReply->deleteLater();
_suggestionsReply = 0;
QJsonParseError err;
QJsonDocument json = QJsonDocument::fromJson(response, &err);
const QVariant res = json.toVariant();
if (err.error != QJsonParseError::NoError || res.type() != QVariant::List) {
return;
}
const QVariantList list = res.toList();
if (list.size() <= SUGGESTIONS_LIST_INDEX) {
return;
}
QStringList out;
const QVariantList& suggList = list.at(SUGGESTIONS_LIST_INDEX).toList();
foreach (const QVariant &v, suggList) {
out.append(v.toString());
}
emit suggestions(out);
}

View file

@ -0,0 +1,46 @@
//
// WebBrowserSuggestionsEngine.h
// interface/src/webbrowser
//
// Created by Vlad Stelmahovsky on 30/10/17.
// Copyright 2017 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 WEBBROWSERSUGGESTIONSENGINE_H
#define WEBBROWSERSUGGESTIONSENGINE_H
#include <qpair.h>
#include <qimage.h>
#include <qmap.h>
#include <qnetworkaccessmanager.h>
#include <qstring.h>
#include <qurl.h>
class QNetworkReply;
class WebBrowserSuggestionsEngine : public QObject {
Q_OBJECT
public:
WebBrowserSuggestionsEngine(QObject* parent = 0);
virtual ~WebBrowserSuggestionsEngine();
public slots:
void querySuggestions(const QString& searchString);
signals:
void suggestions(const QStringList& suggestions);
private slots:
void suggestionsFinished(QNetworkReply *reply);
private:
QNetworkReply* _suggestionsReply;
QNetworkAccessManager* _currentNAM;
};
#endif // WEBBROWSERSUGGESTIONSENGINE_H