From ccfeac7d1e474408e2cae169714326f6198eb17c Mon Sep 17 00:00:00 2001 From: Jocelyn Turcotte <jocelyn.turcotte@digia.com> Date: Mon, 5 Aug 2013 16:37:31 +0200 Subject: [PATCH] Import the demo browser into widget examples. Import the sources as-is, without adding it to the build, to allow performing diffs later on the changes that were needed to port it to use QtWebEngine and manage source compatibility issues. Change-Id: Icf8a284881ce2153e9b5a1ba97dbe77096f1b88d Reviewed-by: Andras Becsi <andras.becsi@digia.com> --- examples/widgets/browser/Info_mac.plist | 43 + examples/widgets/browser/addbookmarkdialog.ui | 98 ++ examples/widgets/browser/autosaver.cpp | 93 ++ examples/widgets/browser/autosaver.h | 75 + examples/widgets/browser/bookmarks.cpp | 986 +++++++++++++ examples/widgets/browser/bookmarks.h | 309 ++++ examples/widgets/browser/bookmarks.ui | 106 ++ examples/widgets/browser/browser.icns | Bin 0 -> 50218 bytes examples/widgets/browser/browser.ico | Bin 0 -> 15374 bytes examples/widgets/browser/browser.pro | 100 ++ examples/widgets/browser/browser.rc | 1 + .../widgets/browser/browserapplication.cpp | 458 ++++++ examples/widgets/browser/browserapplication.h | 118 ++ .../widgets/browser/browsermainwindow.cpp | 948 ++++++++++++ examples/widgets/browser/browsermainwindow.h | 167 +++ examples/widgets/browser/chasewidget.cpp | 141 ++ examples/widgets/browser/chasewidget.h | 85 ++ examples/widgets/browser/cookiejar.cpp | 737 ++++++++++ examples/widgets/browser/cookiejar.h | 203 +++ examples/widgets/browser/cookies.ui | 106 ++ examples/widgets/browser/cookiesexceptions.ui | 184 +++ examples/widgets/browser/data/addtab.png | Bin 0 -> 469 bytes examples/widgets/browser/data/browser.svg | 411 ++++++ examples/widgets/browser/data/closetab.png | Bin 0 -> 516 bytes examples/widgets/browser/data/data.qrc | 11 + .../browser/data/defaultbookmarks.xbel | 43 + examples/widgets/browser/data/defaulticon.png | Bin 0 -> 1473 bytes examples/widgets/browser/data/history.png | Bin 0 -> 1527 bytes examples/widgets/browser/data/loading.gif | Bin 0 -> 847 bytes .../browser/doc/images/browser-demo.png | Bin 0 -> 156342 bytes examples/widgets/browser/doc/src/browser.qdoc | 41 + examples/widgets/browser/downloaditem.ui | 134 ++ examples/widgets/browser/downloadmanager.cpp | 578 ++++++++ examples/widgets/browser/downloadmanager.h | 161 ++ examples/widgets/browser/downloads.ui | 83 ++ examples/widgets/browser/edittableview.cpp | 77 + examples/widgets/browser/edittableview.h | 60 + examples/widgets/browser/edittreeview.cpp | 76 + examples/widgets/browser/edittreeview.h | 60 + examples/widgets/browser/history.cpp | 1291 +++++++++++++++++ examples/widgets/browser/history.h | 349 +++++ examples/widgets/browser/history.ui | 106 ++ examples/widgets/browser/htmls/htmls.qrc | 5 + examples/widgets/browser/htmls/notfound.html | 63 + examples/widgets/browser/main.cpp | 52 + examples/widgets/browser/modelmenu.cpp | 226 +++ examples/widgets/browser/modelmenu.h | 104 ++ .../widgets/browser/networkaccessmanager.cpp | 214 +++ .../widgets/browser/networkaccessmanager.h | 77 + examples/widgets/browser/passworddialog.ui | 111 ++ examples/widgets/browser/proxy.ui | 104 ++ examples/widgets/browser/searchlineedit.cpp | 240 +++ examples/widgets/browser/searchlineedit.h | 102 ++ examples/widgets/browser/settings.cpp | 321 ++++ examples/widgets/browser/settings.h | 73 + examples/widgets/browser/settings.ui | 614 ++++++++ examples/widgets/browser/squeezelabel.cpp | 60 + examples/widgets/browser/squeezelabel.h | 59 + examples/widgets/browser/tabwidget.cpp | 834 +++++++++++ examples/widgets/browser/tabwidget.h | 223 +++ examples/widgets/browser/toolbarsearch.cpp | 163 +++ examples/widgets/browser/toolbarsearch.h | 83 ++ examples/widgets/browser/urllineedit.cpp | 342 +++++ examples/widgets/browser/urllineedit.h | 114 ++ examples/widgets/browser/webview.cpp | 315 ++++ examples/widgets/browser/webview.h | 119 ++ examples/widgets/browser/xbel.cpp | 282 ++++ examples/widgets/browser/xbel.h | 111 ++ 68 files changed, 13450 insertions(+) create mode 100644 examples/widgets/browser/Info_mac.plist create mode 100644 examples/widgets/browser/addbookmarkdialog.ui create mode 100644 examples/widgets/browser/autosaver.cpp create mode 100644 examples/widgets/browser/autosaver.h create mode 100644 examples/widgets/browser/bookmarks.cpp create mode 100644 examples/widgets/browser/bookmarks.h create mode 100644 examples/widgets/browser/bookmarks.ui create mode 100644 examples/widgets/browser/browser.icns create mode 100644 examples/widgets/browser/browser.ico create mode 100644 examples/widgets/browser/browser.pro create mode 100644 examples/widgets/browser/browser.rc create mode 100644 examples/widgets/browser/browserapplication.cpp create mode 100644 examples/widgets/browser/browserapplication.h create mode 100644 examples/widgets/browser/browsermainwindow.cpp create mode 100644 examples/widgets/browser/browsermainwindow.h create mode 100644 examples/widgets/browser/chasewidget.cpp create mode 100644 examples/widgets/browser/chasewidget.h create mode 100644 examples/widgets/browser/cookiejar.cpp create mode 100644 examples/widgets/browser/cookiejar.h create mode 100644 examples/widgets/browser/cookies.ui create mode 100644 examples/widgets/browser/cookiesexceptions.ui create mode 100644 examples/widgets/browser/data/addtab.png create mode 100644 examples/widgets/browser/data/browser.svg create mode 100644 examples/widgets/browser/data/closetab.png create mode 100644 examples/widgets/browser/data/data.qrc create mode 100644 examples/widgets/browser/data/defaultbookmarks.xbel create mode 100644 examples/widgets/browser/data/defaulticon.png create mode 100644 examples/widgets/browser/data/history.png create mode 100644 examples/widgets/browser/data/loading.gif create mode 100644 examples/widgets/browser/doc/images/browser-demo.png create mode 100644 examples/widgets/browser/doc/src/browser.qdoc create mode 100644 examples/widgets/browser/downloaditem.ui create mode 100644 examples/widgets/browser/downloadmanager.cpp create mode 100644 examples/widgets/browser/downloadmanager.h create mode 100644 examples/widgets/browser/downloads.ui create mode 100644 examples/widgets/browser/edittableview.cpp create mode 100644 examples/widgets/browser/edittableview.h create mode 100644 examples/widgets/browser/edittreeview.cpp create mode 100644 examples/widgets/browser/edittreeview.h create mode 100644 examples/widgets/browser/history.cpp create mode 100644 examples/widgets/browser/history.h create mode 100644 examples/widgets/browser/history.ui create mode 100644 examples/widgets/browser/htmls/htmls.qrc create mode 100644 examples/widgets/browser/htmls/notfound.html create mode 100644 examples/widgets/browser/main.cpp create mode 100644 examples/widgets/browser/modelmenu.cpp create mode 100644 examples/widgets/browser/modelmenu.h create mode 100644 examples/widgets/browser/networkaccessmanager.cpp create mode 100644 examples/widgets/browser/networkaccessmanager.h create mode 100644 examples/widgets/browser/passworddialog.ui create mode 100644 examples/widgets/browser/proxy.ui create mode 100644 examples/widgets/browser/searchlineedit.cpp create mode 100644 examples/widgets/browser/searchlineedit.h create mode 100644 examples/widgets/browser/settings.cpp create mode 100644 examples/widgets/browser/settings.h create mode 100644 examples/widgets/browser/settings.ui create mode 100644 examples/widgets/browser/squeezelabel.cpp create mode 100644 examples/widgets/browser/squeezelabel.h create mode 100644 examples/widgets/browser/tabwidget.cpp create mode 100644 examples/widgets/browser/tabwidget.h create mode 100644 examples/widgets/browser/toolbarsearch.cpp create mode 100644 examples/widgets/browser/toolbarsearch.h create mode 100644 examples/widgets/browser/urllineedit.cpp create mode 100644 examples/widgets/browser/urllineedit.h create mode 100644 examples/widgets/browser/webview.cpp create mode 100644 examples/widgets/browser/webview.h create mode 100644 examples/widgets/browser/xbel.cpp create mode 100644 examples/widgets/browser/xbel.h diff --git a/examples/widgets/browser/Info_mac.plist b/examples/widgets/browser/Info_mac.plist new file mode 100644 index 000000000..87435b2b6 --- /dev/null +++ b/examples/widgets/browser/Info_mac.plist @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd"> +<plist version="0.9"> +<dict> + <key>CFBundleIconFile</key> + <string>@ICON@</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleGetInfoString</key> + <string>Created by Qt/QMake</string> + <key>CFBundleIdentifier</key> + <string>com.trolltech.DemoBrowser</string> + <key>CFBundleSignature</key> + <string>ttxt</string> + <key>CFBundleExecutable</key> + <string>@EXECUTABLE@</string> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeExtensions</key> + <array> + <string>html</string> + <string>htm</string> + <string>shtml</string> + <string>xht</string> + <string>xhtml</string> + </array> + <key>CFBundleTypeIconFile</key> + <string>@ICON@</string> + <key>CFBundleTypeName</key> + <string>HTML Document</string> + <key>CFBundleTypeOSTypes</key> + <array> + <string>HTML</string> + </array> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + </dict> + </array> + <key>NOTE</key> + <string>DemoBrowser by Digia Plc and/or its subsidiary(-ies)</string> +</dict> +</plist> diff --git a/examples/widgets/browser/addbookmarkdialog.ui b/examples/widgets/browser/addbookmarkdialog.ui new file mode 100644 index 000000000..3460d7bb8 --- /dev/null +++ b/examples/widgets/browser/addbookmarkdialog.ui @@ -0,0 +1,98 @@ +<ui version="4.0" > + <class>AddBookmarkDialog</class> + <widget class="QDialog" name="AddBookmarkDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>240</width> + <height>168</height> + </rect> + </property> + <property name="windowTitle" > + <string>Add Bookmark</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" > + <item> + <widget class="QLabel" name="label" > + <property name="text" > + <string>Type a name for the bookmark, and choose where to keep it.</string> + </property> + <property name="textFormat" > + <enum>Qt::PlainText</enum> + </property> + <property name="wordWrap" > + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="name" /> + </item> + <item> + <widget class="QComboBox" name="location" /> + </item> + <item> + <spacer name="verticalSpacer" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>20</width> + <height>2</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + <property name="centerButtons" > + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>AddBookmarkDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>AddBookmarkDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/autosaver.cpp b/examples/widgets/browser/autosaver.cpp new file mode 100644 index 000000000..a6efa4ca1 --- /dev/null +++ b/examples/widgets/browser/autosaver.cpp @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "autosaver.h" + +#include <QtCore/QDir> +#include <QtCore/QCoreApplication> +#include <QtCore/QMetaObject> +#include <QtDebug> + +#define AUTOSAVE_IN 1000 * 3 // seconds +#define MAXWAIT 1000 * 15 // seconds + +AutoSaver::AutoSaver(QObject *parent) : QObject(parent) +{ + Q_ASSERT(parent); +} + +AutoSaver::~AutoSaver() +{ + if (m_timer.isActive()) + qWarning() << "AutoSaver: still active when destroyed, changes not saved."; +} + +void AutoSaver::changeOccurred() +{ + if (m_firstChange.isNull()) + m_firstChange.start(); + + if (m_firstChange.elapsed() > MAXWAIT) { + saveIfNeccessary(); + } else { + m_timer.start(AUTOSAVE_IN, this); + } +} + +void AutoSaver::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timer.timerId()) { + saveIfNeccessary(); + } else { + QObject::timerEvent(event); + } +} + +void AutoSaver::saveIfNeccessary() +{ + if (!m_timer.isActive()) + return; + m_timer.stop(); + m_firstChange = QTime(); + if (!QMetaObject::invokeMethod(parent(), "save", Qt::DirectConnection)) { + qWarning() << "AutoSaver: error invoking slot save() on parent"; + } +} diff --git a/examples/widgets/browser/autosaver.h b/examples/widgets/browser/autosaver.h new file mode 100644 index 000000000..cf499b066 --- /dev/null +++ b/examples/widgets/browser/autosaver.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef AUTOSAVER_H +#define AUTOSAVER_H + +#include <QtCore/QObject> +#include <QtCore/QBasicTimer> +#include <QtCore/QTime> + +/* + This class will call the save() slot on the parent object when the parent changes. + It will wait several seconds after changed() to combining multiple changes and + prevent continuous writing to disk. + */ +class AutoSaver : public QObject { + +Q_OBJECT + +public: + AutoSaver(QObject *parent); + ~AutoSaver(); + void saveIfNeccessary(); + +public slots: + void changeOccurred(); + +protected: + void timerEvent(QTimerEvent *event); + +private: + QBasicTimer m_timer; + QTime m_firstChange; + +}; + +#endif // AUTOSAVER_H diff --git a/examples/widgets/browser/bookmarks.cpp b/examples/widgets/browser/bookmarks.cpp new file mode 100644 index 000000000..be4127b03 --- /dev/null +++ b/examples/widgets/browser/bookmarks.cpp @@ -0,0 +1,986 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "bookmarks.h" + +#include "autosaver.h" +#include "browserapplication.h" +#include "history.h" +#include "xbel.h" + +#include <QtCore/QBuffer> +#include <QtCore/QFile> +#include <QtCore/QMimeData> + +#include <QtGui/QDesktopServices> +#include <QtGui/QDragEnterEvent> +#include <QtGui/QIcon> +#include <QtWidgets/QFileDialog> +#include <QtWidgets/QHeaderView> +#include <QtWidgets/QMessageBox> +#include <QtWidgets/QToolButton> + +#include <QWebSettings> + +#include <QtCore/QDebug> + +#define BOOKMARKBAR "Bookmarks Bar" +#define BOOKMARKMENU "Bookmarks Menu" + +BookmarksManager::BookmarksManager(QObject *parent) + : QObject(parent) + , m_loaded(false) + , m_saveTimer(new AutoSaver(this)) + , m_bookmarkRootNode(0) + , m_bookmarkModel(0) +{ + connect(this, SIGNAL(entryAdded(BookmarkNode*)), + m_saveTimer, SLOT(changeOccurred())); + connect(this, SIGNAL(entryRemoved(BookmarkNode*,int,BookmarkNode*)), + m_saveTimer, SLOT(changeOccurred())); + connect(this, SIGNAL(entryChanged(BookmarkNode*)), + m_saveTimer, SLOT(changeOccurred())); +} + +BookmarksManager::~BookmarksManager() +{ + m_saveTimer->saveIfNeccessary(); +} + +void BookmarksManager::changeExpanded() +{ + m_saveTimer->changeOccurred(); +} + +void BookmarksManager::load() +{ + if (m_loaded) + return; + m_loaded = true; + + QString dir = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + QString bookmarkFile = dir + QLatin1String("/bookmarks.xbel"); + if (!QFile::exists(bookmarkFile)) + bookmarkFile = QLatin1String(":defaultbookmarks.xbel"); + + XbelReader reader; + m_bookmarkRootNode = reader.read(bookmarkFile); + if (reader.error() != QXmlStreamReader::NoError) { + QMessageBox::warning(0, QLatin1String("Loading Bookmark"), + tr("Error when loading bookmarks on line %1, column %2:\n" + "%3").arg(reader.lineNumber()).arg(reader.columnNumber()).arg(reader.errorString())); + } + + BookmarkNode *toolbar = 0; + BookmarkNode *menu = 0; + QList<BookmarkNode*> others; + for (int i = m_bookmarkRootNode->children().count() - 1; i >= 0; --i) { + BookmarkNode *node = m_bookmarkRootNode->children().at(i); + if (node->type() == BookmarkNode::Folder) { + // Automatically convert + if (node->title == tr("Toolbar Bookmarks") && !toolbar) { + node->title = tr(BOOKMARKBAR); + } + if (node->title == tr(BOOKMARKBAR) && !toolbar) { + toolbar = node; + } + + // Automatically convert + if (node->title == tr("Menu") && !menu) { + node->title = tr(BOOKMARKMENU); + } + if (node->title == tr(BOOKMARKMENU) && !menu) { + menu = node; + } + } else { + others.append(node); + } + m_bookmarkRootNode->remove(node); + } + Q_ASSERT(m_bookmarkRootNode->children().count() == 0); + if (!toolbar) { + toolbar = new BookmarkNode(BookmarkNode::Folder, m_bookmarkRootNode); + toolbar->title = tr(BOOKMARKBAR); + } else { + m_bookmarkRootNode->add(toolbar); + } + + if (!menu) { + menu = new BookmarkNode(BookmarkNode::Folder, m_bookmarkRootNode); + menu->title = tr(BOOKMARKMENU); + } else { + m_bookmarkRootNode->add(menu); + } + + for (int i = 0; i < others.count(); ++i) + menu->add(others.at(i)); +} + +void BookmarksManager::save() const +{ + if (!m_loaded) + return; + + XbelWriter writer; + QString dir = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + QString bookmarkFile = dir + QLatin1String("/bookmarks.xbel"); + if (!writer.write(bookmarkFile, m_bookmarkRootNode)) + qWarning() << "BookmarkManager: error saving to" << bookmarkFile; +} + +void BookmarksManager::addBookmark(BookmarkNode *parent, BookmarkNode *node, int row) +{ + if (!m_loaded) + return; + Q_ASSERT(parent); + InsertBookmarksCommand *command = new InsertBookmarksCommand(this, parent, node, row); + m_commands.push(command); +} + +void BookmarksManager::removeBookmark(BookmarkNode *node) +{ + if (!m_loaded) + return; + + Q_ASSERT(node); + BookmarkNode *parent = node->parent(); + int row = parent->children().indexOf(node); + RemoveBookmarksCommand *command = new RemoveBookmarksCommand(this, parent, row); + m_commands.push(command); +} + +void BookmarksManager::setTitle(BookmarkNode *node, const QString &newTitle) +{ + if (!m_loaded) + return; + + Q_ASSERT(node); + ChangeBookmarkCommand *command = new ChangeBookmarkCommand(this, node, newTitle, true); + m_commands.push(command); +} + +void BookmarksManager::setUrl(BookmarkNode *node, const QString &newUrl) +{ + if (!m_loaded) + return; + + Q_ASSERT(node); + ChangeBookmarkCommand *command = new ChangeBookmarkCommand(this, node, newUrl, false); + m_commands.push(command); +} + +BookmarkNode *BookmarksManager::bookmarks() +{ + if (!m_loaded) + load(); + return m_bookmarkRootNode; +} + +BookmarkNode *BookmarksManager::menu() +{ + if (!m_loaded) + load(); + + for (int i = m_bookmarkRootNode->children().count() - 1; i >= 0; --i) { + BookmarkNode *node = m_bookmarkRootNode->children().at(i); + if (node->title == tr(BOOKMARKMENU)) + return node; + } + Q_ASSERT(false); + return 0; +} + +BookmarkNode *BookmarksManager::toolbar() +{ + if (!m_loaded) + load(); + + for (int i = m_bookmarkRootNode->children().count() - 1; i >= 0; --i) { + BookmarkNode *node = m_bookmarkRootNode->children().at(i); + if (node->title == tr(BOOKMARKBAR)) + return node; + } + Q_ASSERT(false); + return 0; +} + +BookmarksModel *BookmarksManager::bookmarksModel() +{ + if (!m_bookmarkModel) + m_bookmarkModel = new BookmarksModel(this, this); + return m_bookmarkModel; +} + +void BookmarksManager::importBookmarks() +{ + QString fileName = QFileDialog::getOpenFileName(0, tr("Open File"), + QString(), + tr("XBEL (*.xbel *.xml)")); + if (fileName.isEmpty()) + return; + + XbelReader reader; + BookmarkNode *importRootNode = reader.read(fileName); + if (reader.error() != QXmlStreamReader::NoError) { + QMessageBox::warning(0, QLatin1String("Loading Bookmark"), + tr("Error when loading bookmarks on line %1, column %2:\n" + "%3").arg(reader.lineNumber()).arg(reader.columnNumber()).arg(reader.errorString())); + } + + importRootNode->setType(BookmarkNode::Folder); + importRootNode->title = (tr("Imported %1").arg(QDate::currentDate().toString(Qt::SystemLocaleShortDate))); + addBookmark(menu(), importRootNode); +} + +void BookmarksManager::exportBookmarks() +{ + QString fileName = QFileDialog::getSaveFileName(0, tr("Save File"), + tr("%1 Bookmarks.xbel").arg(QCoreApplication::applicationName()), + tr("XBEL (*.xbel *.xml)")); + if (fileName.isEmpty()) + return; + + XbelWriter writer; + if (!writer.write(fileName, m_bookmarkRootNode)) + QMessageBox::critical(0, tr("Export error"), tr("error saving bookmarks")); +} + +RemoveBookmarksCommand::RemoveBookmarksCommand(BookmarksManager *m_bookmarkManagaer, BookmarkNode *parent, int row) + : QUndoCommand(BookmarksManager::tr("Remove Bookmark")) + , m_row(row) + , m_bookmarkManagaer(m_bookmarkManagaer) + , m_node(parent->children().value(row)) + , m_parent(parent) + , m_done(false) +{ +} + +RemoveBookmarksCommand::~RemoveBookmarksCommand() +{ + if (m_done && !m_node->parent()) { + delete m_node; + } +} + +void RemoveBookmarksCommand::undo() +{ + m_parent->add(m_node, m_row); + emit m_bookmarkManagaer->entryAdded(m_node); + m_done = false; +} + +void RemoveBookmarksCommand::redo() +{ + m_parent->remove(m_node); + emit m_bookmarkManagaer->entryRemoved(m_parent, m_row, m_node); + m_done = true; +} + +InsertBookmarksCommand::InsertBookmarksCommand(BookmarksManager *m_bookmarkManagaer, + BookmarkNode *parent, BookmarkNode *node, int row) + : RemoveBookmarksCommand(m_bookmarkManagaer, parent, row) +{ + setText(BookmarksManager::tr("Insert Bookmark")); + m_node = node; +} + +ChangeBookmarkCommand::ChangeBookmarkCommand(BookmarksManager *m_bookmarkManagaer, BookmarkNode *node, + const QString &newValue, bool title) + : QUndoCommand() + , m_bookmarkManagaer(m_bookmarkManagaer) + , m_title(title) + , m_newValue(newValue) + , m_node(node) +{ + if (m_title) { + m_oldValue = m_node->title; + setText(BookmarksManager::tr("Name Change")); + } else { + m_oldValue = m_node->url; + setText(BookmarksManager::tr("Address Change")); + } +} + +void ChangeBookmarkCommand::undo() +{ + if (m_title) + m_node->title = m_oldValue; + else + m_node->url = m_oldValue; + emit m_bookmarkManagaer->entryChanged(m_node); +} + +void ChangeBookmarkCommand::redo() +{ + if (m_title) + m_node->title = m_newValue; + else + m_node->url = m_newValue; + emit m_bookmarkManagaer->entryChanged(m_node); +} + +BookmarksModel::BookmarksModel(BookmarksManager *bookmarkManager, QObject *parent) + : QAbstractItemModel(parent) + , m_endMacro(false) + , m_bookmarksManager(bookmarkManager) +{ + connect(bookmarkManager, SIGNAL(entryAdded(BookmarkNode*)), + this, SLOT(entryAdded(BookmarkNode*))); + connect(bookmarkManager, SIGNAL(entryRemoved(BookmarkNode*,int,BookmarkNode*)), + this, SLOT(entryRemoved(BookmarkNode*,int,BookmarkNode*))); + connect(bookmarkManager, SIGNAL(entryChanged(BookmarkNode*)), + this, SLOT(entryChanged(BookmarkNode*))); +} + +QModelIndex BookmarksModel::index(BookmarkNode *node) const +{ + BookmarkNode *parent = node->parent(); + if (!parent) + return QModelIndex(); + return createIndex(parent->children().indexOf(node), 0, node); +} + +void BookmarksModel::entryAdded(BookmarkNode *item) +{ + Q_ASSERT(item && item->parent()); + int row = item->parent()->children().indexOf(item); + BookmarkNode *parent = item->parent(); + // item was already added so remove beore beginInsertRows is called + parent->remove(item); + beginInsertRows(index(parent), row, row); + parent->add(item, row); + endInsertRows(); +} + +void BookmarksModel::entryRemoved(BookmarkNode *parent, int row, BookmarkNode *item) +{ + // item was already removed, re-add so beginRemoveRows works + parent->add(item, row); + beginRemoveRows(index(parent), row, row); + parent->remove(item); + endRemoveRows(); +} + +void BookmarksModel::entryChanged(BookmarkNode *item) +{ + QModelIndex idx = index(item); + emit dataChanged(idx, idx); +} + +bool BookmarksModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || count <= 0 || row + count > rowCount(parent)) + return false; + + BookmarkNode *bookmarkNode = node(parent); + for (int i = row + count - 1; i >= row; --i) { + BookmarkNode *node = bookmarkNode->children().at(i); + if (node == m_bookmarksManager->menu() + || node == m_bookmarksManager->toolbar()) + continue; + + m_bookmarksManager->removeBookmark(node); + } + if (m_endMacro) { + m_bookmarksManager->undoRedoStack()->endMacro(); + m_endMacro = false; + } + return true; +} + +QVariant BookmarksModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case 0: return tr("Title"); + case 1: return tr("Address"); + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + +QVariant BookmarksModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.model() != this) + return QVariant(); + + const BookmarkNode *bookmarkNode = node(index); + switch (role) { + case Qt::EditRole: + case Qt::DisplayRole: + if (bookmarkNode->type() == BookmarkNode::Separator) { + switch (index.column()) { + case 0: return QString(50, 0xB7); + case 1: return QString(); + } + } + + switch (index.column()) { + case 0: return bookmarkNode->title; + case 1: return bookmarkNode->url; + } + break; + case BookmarksModel::UrlRole: + return QUrl(bookmarkNode->url); + break; + case BookmarksModel::UrlStringRole: + return bookmarkNode->url; + break; + case BookmarksModel::TypeRole: + return bookmarkNode->type(); + break; + case BookmarksModel::SeparatorRole: + return (bookmarkNode->type() == BookmarkNode::Separator); + break; + case Qt::DecorationRole: + if (index.column() == 0) { + if (bookmarkNode->type() == BookmarkNode::Folder) + return QApplication::style()->standardIcon(QStyle::SP_DirIcon); + return BrowserApplication::instance()->icon(bookmarkNode->url); + } + } + + return QVariant(); +} + +int BookmarksModel::columnCount(const QModelIndex &parent) const +{ + return (parent.column() > 0) ? 0 : 2; +} + +int BookmarksModel::rowCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + return m_bookmarksManager->bookmarks()->children().count(); + + const BookmarkNode *item = static_cast<BookmarkNode*>(parent.internalPointer()); + return item->children().count(); +} + +QModelIndex BookmarksModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || column < 0 || row >= rowCount(parent) || column >= columnCount(parent)) + return QModelIndex(); + + // get the parent node + BookmarkNode *parentNode = node(parent); + return createIndex(row, column, parentNode->children().at(row)); +} + +QModelIndex BookmarksModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + BookmarkNode *itemNode = node(index); + BookmarkNode *parentNode = (itemNode ? itemNode->parent() : 0); + if (!parentNode || parentNode == m_bookmarksManager->bookmarks()) + return QModelIndex(); + + // get the parent's row + BookmarkNode *grandParentNode = parentNode->parent(); + int parentRow = grandParentNode->children().indexOf(parentNode); + Q_ASSERT(parentRow >= 0); + return createIndex(parentRow, 0, parentNode); +} + +bool BookmarksModel::hasChildren(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return true; + const BookmarkNode *parentNode = node(parent); + return (parentNode->type() == BookmarkNode::Folder); +} + +Qt::ItemFlags BookmarksModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + BookmarkNode *bookmarkNode = node(index); + + if (bookmarkNode != m_bookmarksManager->menu() + && bookmarkNode != m_bookmarksManager->toolbar()) { + flags |= Qt::ItemIsDragEnabled; + if (bookmarkNode->type() != BookmarkNode::Separator) + flags |= Qt::ItemIsEditable; + } + if (hasChildren(index)) + flags |= Qt::ItemIsDropEnabled; + return flags; +} + +Qt::DropActions BookmarksModel::supportedDropActions () const +{ + return Qt::CopyAction | Qt::MoveAction; +} + +#define MIMETYPE QLatin1String("application/bookmarks.xbel") + +QStringList BookmarksModel::mimeTypes() const +{ + QStringList types; + types << MIMETYPE; + return types; +} + +QMimeData *BookmarksModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mimeData = new QMimeData(); + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + foreach (QModelIndex index, indexes) { + if (index.column() != 0 || !index.isValid()) + continue; + QByteArray encodedData; + QBuffer buffer(&encodedData); + buffer.open(QBuffer::ReadWrite); + XbelWriter writer; + const BookmarkNode *parentNode = node(index); + writer.write(&buffer, parentNode); + stream << encodedData; + } + mimeData->setData(MIMETYPE, data); + return mimeData; +} + +bool BookmarksModel::dropMimeData(const QMimeData *data, + Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + + if (!data->hasFormat(MIMETYPE) + || column > 0) + return false; + + QByteArray ba = data->data(MIMETYPE); + QDataStream stream(&ba, QIODevice::ReadOnly); + if (stream.atEnd()) + return false; + + QUndoStack *undoStack = m_bookmarksManager->undoRedoStack(); + undoStack->beginMacro(QLatin1String("Move Bookmarks")); + + while (!stream.atEnd()) { + QByteArray encodedData; + stream >> encodedData; + QBuffer buffer(&encodedData); + buffer.open(QBuffer::ReadOnly); + + XbelReader reader; + BookmarkNode *rootNode = reader.read(&buffer); + QList<BookmarkNode*> children = rootNode->children(); + for (int i = 0; i < children.count(); ++i) { + BookmarkNode *bookmarkNode = children.at(i); + rootNode->remove(bookmarkNode); + row = qMax(0, row); + BookmarkNode *parentNode = node(parent); + m_bookmarksManager->addBookmark(parentNode, bookmarkNode, row); + m_endMacro = true; + } + delete rootNode; + } + return true; +} + +bool BookmarksModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || (flags(index) & Qt::ItemIsEditable) == 0) + return false; + + BookmarkNode *item = node(index); + + switch (role) { + case Qt::EditRole: + case Qt::DisplayRole: + if (index.column() == 0) { + m_bookmarksManager->setTitle(item, value.toString()); + break; + } + if (index.column() == 1) { + m_bookmarksManager->setUrl(item, value.toString()); + break; + } + return false; + case BookmarksModel::UrlRole: + m_bookmarksManager->setUrl(item, value.toUrl().toString()); + break; + case BookmarksModel::UrlStringRole: + m_bookmarksManager->setUrl(item, value.toString()); + break; + default: + break; + return false; + } + + return true; +} + +BookmarkNode *BookmarksModel::node(const QModelIndex &index) const +{ + BookmarkNode *itemNode = static_cast<BookmarkNode*>(index.internalPointer()); + if (!itemNode) + return m_bookmarksManager->bookmarks(); + return itemNode; +} + + +AddBookmarkProxyModel::AddBookmarkProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +int AddBookmarkProxyModel::columnCount(const QModelIndex &parent) const +{ + return qMin(1, QSortFilterProxyModel::columnCount(parent)); +} + +bool AddBookmarkProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + return sourceModel()->hasChildren(idx); +} + +AddBookmarkDialog::AddBookmarkDialog(const QString &url, const QString &title, QWidget *parent, BookmarksManager *bookmarkManager) + : QDialog(parent) + , m_url(url) + , m_bookmarksManager(bookmarkManager) +{ + setWindowFlags(Qt::Sheet); + if (!m_bookmarksManager) + m_bookmarksManager = BrowserApplication::bookmarksManager(); + setupUi(this); + QTreeView *view = new QTreeView(this); + m_proxyModel = new AddBookmarkProxyModel(this); + BookmarksModel *model = m_bookmarksManager->bookmarksModel(); + m_proxyModel->setSourceModel(model); + view->setModel(m_proxyModel); + view->expandAll(); + view->header()->setStretchLastSection(true); + view->header()->hide(); + view->setItemsExpandable(false); + view->setRootIsDecorated(false); + view->setIndentation(10); + location->setModel(m_proxyModel); + view->show(); + location->setView(view); + BookmarkNode *menu = m_bookmarksManager->menu(); + QModelIndex idx = m_proxyModel->mapFromSource(model->index(menu)); + view->setCurrentIndex(idx); + location->setCurrentIndex(idx.row()); + name->setText(title); +} + +void AddBookmarkDialog::accept() +{ + QModelIndex index = location->view()->currentIndex(); + index = m_proxyModel->mapToSource(index); + if (!index.isValid()) + index = m_bookmarksManager->bookmarksModel()->index(0, 0); + BookmarkNode *parent = m_bookmarksManager->bookmarksModel()->node(index); + BookmarkNode *bookmark = new BookmarkNode(BookmarkNode::Bookmark); + bookmark->url = m_url; + bookmark->title = name->text(); + m_bookmarksManager->addBookmark(parent, bookmark); + QDialog::accept(); +} + +BookmarksMenu::BookmarksMenu(QWidget *parent) + : ModelMenu(parent) + , m_bookmarksManager(0) +{ + connect(this, SIGNAL(activated(QModelIndex)), + this, SLOT(activated(QModelIndex))); + setMaxRows(-1); + setHoverRole(BookmarksModel::UrlStringRole); + setSeparatorRole(BookmarksModel::SeparatorRole); +} + +void BookmarksMenu::activated(const QModelIndex &index) +{ + emit openUrl(index.data(BookmarksModel::UrlRole).toUrl()); +} + +bool BookmarksMenu::prePopulated() +{ + m_bookmarksManager = BrowserApplication::bookmarksManager(); + setModel(m_bookmarksManager->bookmarksModel()); + setRootIndex(m_bookmarksManager->bookmarksModel()->index(1, 0)); + // initial actions + for (int i = 0; i < m_initialActions.count(); ++i) + addAction(m_initialActions.at(i)); + if (!m_initialActions.isEmpty()) + addSeparator(); + createMenu(model()->index(0, 0), 1, this); + return true; +} + +void BookmarksMenu::setInitialActions(QList<QAction*> actions) +{ + m_initialActions = actions; + for (int i = 0; i < m_initialActions.count(); ++i) + addAction(m_initialActions.at(i)); +} + +BookmarksDialog::BookmarksDialog(QWidget *parent, BookmarksManager *manager) + : QDialog(parent) +{ + m_bookmarksManager = manager; + if (!m_bookmarksManager) + m_bookmarksManager = BrowserApplication::bookmarksManager(); + setupUi(this); + + tree->setUniformRowHeights(true); + tree->setSelectionBehavior(QAbstractItemView::SelectRows); + tree->setSelectionMode(QAbstractItemView::ContiguousSelection); + tree->setTextElideMode(Qt::ElideMiddle); + m_bookmarksModel = m_bookmarksManager->bookmarksModel(); + m_proxyModel = new TreeProxyModel(this); + connect(search, SIGNAL(textChanged(QString)), + m_proxyModel, SLOT(setFilterFixedString(QString))); + connect(removeButton, SIGNAL(clicked()), tree, SLOT(removeOne())); + m_proxyModel->setSourceModel(m_bookmarksModel); + tree->setModel(m_proxyModel); + tree->setDragDropMode(QAbstractItemView::InternalMove); + tree->setExpanded(m_proxyModel->index(0, 0), true); + tree->setAlternatingRowColors(true); + QFontMetrics fm(font()); + int header = fm.width(QLatin1Char('m')) * 40; + tree->header()->resizeSection(0, header); + tree->header()->setStretchLastSection(true); + connect(tree, SIGNAL(activated(QModelIndex)), + this, SLOT(open())); + tree->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tree, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(customContextMenuRequested(QPoint))); + connect(addFolderButton, SIGNAL(clicked()), + this, SLOT(newFolder())); + expandNodes(m_bookmarksManager->bookmarks()); + setAttribute(Qt::WA_DeleteOnClose); +} + +BookmarksDialog::~BookmarksDialog() +{ + if (saveExpandedNodes(tree->rootIndex())) + m_bookmarksManager->changeExpanded(); +} + +bool BookmarksDialog::saveExpandedNodes(const QModelIndex &parent) +{ + bool changed = false; + for (int i = 0; i < m_proxyModel->rowCount(parent); ++i) { + QModelIndex child = m_proxyModel->index(i, 0, parent); + QModelIndex sourceIndex = m_proxyModel->mapToSource(child); + BookmarkNode *childNode = m_bookmarksModel->node(sourceIndex); + bool wasExpanded = childNode->expanded; + if (tree->isExpanded(child)) { + childNode->expanded = true; + changed |= saveExpandedNodes(child); + } else { + childNode->expanded = false; + } + changed |= (wasExpanded != childNode->expanded); + } + return changed; +} + +void BookmarksDialog::expandNodes(BookmarkNode *node) +{ + for (int i = 0; i < node->children().count(); ++i) { + BookmarkNode *childNode = node->children()[i]; + if (childNode->expanded) { + QModelIndex idx = m_bookmarksModel->index(childNode); + idx = m_proxyModel->mapFromSource(idx); + tree->setExpanded(idx, true); + expandNodes(childNode); + } + } +} + +void BookmarksDialog::customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + QModelIndex index = tree->indexAt(pos); + index = index.sibling(index.row(), 0); + if (index.isValid() && !tree->model()->hasChildren(index)) { + menu.addAction(tr("Open"), this, SLOT(open())); + menu.addSeparator(); + } + menu.addAction(tr("Delete"), tree, SLOT(removeOne())); + menu.exec(QCursor::pos()); +} + +void BookmarksDialog::open() +{ + QModelIndex index = tree->currentIndex(); + if (!index.parent().isValid()) + return; + emit openUrl(index.sibling(index.row(), 1).data(BookmarksModel::UrlRole).toUrl()); +} + +void BookmarksDialog::newFolder() +{ + QModelIndex currentIndex = tree->currentIndex(); + QModelIndex idx = currentIndex; + if (idx.isValid() && !idx.model()->hasChildren(idx)) + idx = idx.parent(); + if (!idx.isValid()) + idx = tree->rootIndex(); + idx = m_proxyModel->mapToSource(idx); + BookmarkNode *parent = m_bookmarksManager->bookmarksModel()->node(idx); + BookmarkNode *node = new BookmarkNode(BookmarkNode::Folder); + node->title = tr("New Folder"); + m_bookmarksManager->addBookmark(parent, node, currentIndex.row() + 1); +} + +BookmarksToolBar::BookmarksToolBar(BookmarksModel *model, QWidget *parent) + : QToolBar(tr("Bookmark"), parent) + , m_bookmarksModel(model) +{ + connect(this, SIGNAL(actionTriggered(QAction*)), this, SLOT(triggered(QAction*))); + setRootIndex(model->index(0, 0)); + connect(m_bookmarksModel, SIGNAL(modelReset()), this, SLOT(build())); + connect(m_bookmarksModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(build())); + connect(m_bookmarksModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(build())); + connect(m_bookmarksModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(build())); + setAcceptDrops(true); +} + +void BookmarksToolBar::dragEnterEvent(QDragEnterEvent *event) +{ + const QMimeData *mimeData = event->mimeData(); + if (mimeData->hasUrls()) + event->acceptProposedAction(); + QToolBar::dragEnterEvent(event); +} + +void BookmarksToolBar::dropEvent(QDropEvent *event) +{ + const QMimeData *mimeData = event->mimeData(); + if (mimeData->hasUrls() && mimeData->hasText()) { + QList<QUrl> urls = mimeData->urls(); + QAction *action = actionAt(event->pos()); + QString dropText; + if (action) + dropText = action->text(); + int row = -1; + QModelIndex parentIndex = m_root; + for (int i = 0; i < m_bookmarksModel->rowCount(m_root); ++i) { + QModelIndex idx = m_bookmarksModel->index(i, 0, m_root); + QString title = idx.data().toString(); + if (title == dropText) { + row = i; + if (m_bookmarksModel->hasChildren(idx)) { + parentIndex = idx; + row = -1; + } + break; + } + } + BookmarkNode *bookmark = new BookmarkNode(BookmarkNode::Bookmark); + bookmark->url = urls.at(0).toString(); + bookmark->title = mimeData->text(); + + BookmarkNode *parent = m_bookmarksModel->node(parentIndex); + BookmarksManager *bookmarksManager = m_bookmarksModel->bookmarksManager(); + bookmarksManager->addBookmark(parent, bookmark, row); + event->acceptProposedAction(); + } + QToolBar::dropEvent(event); +} + + +void BookmarksToolBar::setRootIndex(const QModelIndex &index) +{ + m_root = index; + build(); +} + +QModelIndex BookmarksToolBar::rootIndex() const +{ + return m_root; +} + +void BookmarksToolBar::build() +{ + clear(); + for (int i = 0; i < m_bookmarksModel->rowCount(m_root); ++i) { + QModelIndex idx = m_bookmarksModel->index(i, 0, m_root); + if (m_bookmarksModel->hasChildren(idx)) { + QToolButton *button = new QToolButton(this); + button->setPopupMode(QToolButton::InstantPopup); + button->setArrowType(Qt::DownArrow); + button->setText(idx.data().toString()); + ModelMenu *menu = new ModelMenu(this); + connect(menu, SIGNAL(activated(QModelIndex)), + this, SLOT(activated(QModelIndex))); + menu->setModel(m_bookmarksModel); + menu->setRootIndex(idx); + menu->addAction(new QAction(menu)); + button->setMenu(menu); + button->setToolButtonStyle(Qt::ToolButtonTextOnly); + QAction *a = addWidget(button); + a->setText(idx.data().toString()); + } else { + QAction *action = addAction(idx.data().toString()); + action->setData(idx.data(BookmarksModel::UrlRole)); + } + } +} + +void BookmarksToolBar::triggered(QAction *action) +{ + QVariant v = action->data(); + if (v.canConvert<QUrl>()) { + emit openUrl(v.toUrl()); + } +} + +void BookmarksToolBar::activated(const QModelIndex &index) +{ + emit openUrl(index.data(BookmarksModel::UrlRole).toUrl()); +} diff --git a/examples/widgets/browser/bookmarks.h b/examples/widgets/browser/bookmarks.h new file mode 100644 index 000000000..be29666df --- /dev/null +++ b/examples/widgets/browser/bookmarks.h @@ -0,0 +1,309 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BOOKMARKS_H +#define BOOKMARKS_H + +#include <QtCore/QObject> +#include <QtCore/QAbstractItemModel> + +#include <QtWidgets/QUndoCommand> + +/*! + Bookmark manager, owner of the bookmarks, loads, saves and basic tasks + */ +class AutoSaver; +class BookmarkNode; +class BookmarksModel; +class BookmarksManager : public QObject +{ + Q_OBJECT + +signals: + void entryAdded(BookmarkNode *item); + void entryRemoved(BookmarkNode *parent, int row, BookmarkNode *item); + void entryChanged(BookmarkNode *item); + +public: + BookmarksManager(QObject *parent = 0); + ~BookmarksManager(); + + void addBookmark(BookmarkNode *parent, BookmarkNode *node, int row = -1); + void removeBookmark(BookmarkNode *node); + void setTitle(BookmarkNode *node, const QString &newTitle); + void setUrl(BookmarkNode *node, const QString &newUrl); + void changeExpanded(); + + BookmarkNode *bookmarks(); + BookmarkNode *menu(); + BookmarkNode *toolbar(); + + BookmarksModel *bookmarksModel(); + QUndoStack *undoRedoStack() { return &m_commands; }; + +public slots: + void importBookmarks(); + void exportBookmarks(); + +private slots: + void save() const; + +private: + void load(); + + bool m_loaded; + AutoSaver *m_saveTimer; + BookmarkNode *m_bookmarkRootNode; + BookmarksModel *m_bookmarkModel; + QUndoStack m_commands; + + friend class RemoveBookmarksCommand; + friend class ChangeBookmarkCommand; +}; + +class RemoveBookmarksCommand : public QUndoCommand +{ + +public: + RemoveBookmarksCommand(BookmarksManager *m_bookmarkManagaer, BookmarkNode *parent, int row); + ~RemoveBookmarksCommand(); + void undo(); + void redo(); + +protected: + int m_row; + BookmarksManager *m_bookmarkManagaer; + BookmarkNode *m_node; + BookmarkNode *m_parent; + bool m_done; +}; + +class InsertBookmarksCommand : public RemoveBookmarksCommand +{ + +public: + InsertBookmarksCommand(BookmarksManager *m_bookmarkManagaer, + BookmarkNode *parent, BookmarkNode *node, int row); + void undo() { RemoveBookmarksCommand::redo(); } + void redo() { RemoveBookmarksCommand::undo(); } + +}; + +class ChangeBookmarkCommand : public QUndoCommand +{ + +public: + ChangeBookmarkCommand(BookmarksManager *m_bookmarkManagaer, + BookmarkNode *node, const QString &newValue, bool title); + void undo(); + void redo(); + +private: + BookmarksManager *m_bookmarkManagaer; + bool m_title; + QString m_oldValue; + QString m_newValue; + BookmarkNode *m_node; +}; + +/*! + BookmarksModel is a QAbstractItemModel wrapper around the BookmarkManager + */ +class BookmarksModel : public QAbstractItemModel +{ + Q_OBJECT + +public slots: + void entryAdded(BookmarkNode *item); + void entryRemoved(BookmarkNode *parent, int row, BookmarkNode *item); + void entryChanged(BookmarkNode *item); + +public: + enum Roles { + TypeRole = Qt::UserRole + 1, + UrlRole = Qt::UserRole + 2, + UrlStringRole = Qt::UserRole + 3, + SeparatorRole = Qt::UserRole + 4 + }; + + BookmarksModel(BookmarksManager *bookmarkManager, QObject *parent = 0); + inline BookmarksManager *bookmarksManager() const { return m_bookmarksManager; } + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex index(int, int, const QModelIndex& = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index= QModelIndex()) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + Qt::DropActions supportedDropActions () const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + QMimeData *mimeData(const QModelIndexList &indexes) const; + QStringList mimeTypes() const; + bool dropMimeData(const QMimeData *data, + Qt::DropAction action, int row, int column, const QModelIndex &parent); + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; + + BookmarkNode *node(const QModelIndex &index) const; + QModelIndex index(BookmarkNode *node) const; + +private: + + bool m_endMacro; + BookmarksManager *m_bookmarksManager; +}; + +// Menu that is dynamically populated from the bookmarks +#include "modelmenu.h" +class BookmarksMenu : public ModelMenu +{ + Q_OBJECT + +signals: + void openUrl(const QUrl &url); + +public: + BookmarksMenu(QWidget *parent = 0); + void setInitialActions(QList<QAction*> actions); + +protected: + bool prePopulated(); + +private slots: + void activated(const QModelIndex &index); + +private: + BookmarksManager *m_bookmarksManager; + QList<QAction*> m_initialActions; +}; + +/* + Proxy model that filters out the bookmarks so only the folders + are left behind. Used in the add bookmark dialog combobox. + */ +#include <QtCore/QSortFilterProxyModel> +class AddBookmarkProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + AddBookmarkProxyModel(QObject * parent = 0); + int columnCount(const QModelIndex & parent = QModelIndex()) const; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; +}; + +/*! + Add bookmark dialog + */ +#include "ui_addbookmarkdialog.h" +class AddBookmarkDialog : public QDialog, public Ui_AddBookmarkDialog +{ + Q_OBJECT + +public: + AddBookmarkDialog(const QString &url, const QString &title, QWidget *parent = 0, BookmarksManager *bookmarkManager = 0); + +private slots: + void accept(); + +private: + QString m_url; + BookmarksManager *m_bookmarksManager; + AddBookmarkProxyModel *m_proxyModel; +}; + +#include "ui_bookmarks.h" +class TreeProxyModel; +class BookmarksDialog : public QDialog, public Ui_BookmarksDialog +{ + Q_OBJECT + +signals: + void openUrl(const QUrl &url); + +public: + BookmarksDialog(QWidget *parent = 0, BookmarksManager *manager = 0); + ~BookmarksDialog(); + +private slots: + void customContextMenuRequested(const QPoint &pos); + void open(); + void newFolder(); + +private: + void expandNodes(BookmarkNode *node); + bool saveExpandedNodes(const QModelIndex &parent); + + BookmarksManager *m_bookmarksManager; + BookmarksModel *m_bookmarksModel; + TreeProxyModel *m_proxyModel; +}; + +#include <QtWidgets/QToolBar> +class BookmarksToolBar : public QToolBar +{ + Q_OBJECT + +signals: + void openUrl(const QUrl &url); + +public: + BookmarksToolBar(BookmarksModel *model, QWidget *parent = 0); + void setRootIndex(const QModelIndex &index); + QModelIndex rootIndex() const; + +protected: + void dragEnterEvent(QDragEnterEvent *event); + void dropEvent(QDropEvent *event); + +private slots: + void triggered(QAction *action); + void activated(const QModelIndex &index); + void build(); + +private: + BookmarksModel *m_bookmarksModel; + QPersistentModelIndex m_root; +}; + +#endif // BOOKMARKS_H diff --git a/examples/widgets/browser/bookmarks.ui b/examples/widgets/browser/bookmarks.ui new file mode 100644 index 000000000..c893e941d --- /dev/null +++ b/examples/widgets/browser/bookmarks.ui @@ -0,0 +1,106 @@ +<ui version="4.0" > + <class>BookmarksDialog</class> + <widget class="QDialog" name="BookmarksDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>758</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle" > + <string>Bookmarks</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <item row="0" column="0" > + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>252</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1" > + <widget class="SearchLineEdit" name="search" /> + </item> + <item row="1" column="0" colspan="2" > + <widget class="EditTreeView" name="tree" /> + </item> + <item row="2" column="0" colspan="2" > + <layout class="QHBoxLayout" > + <item> + <widget class="QPushButton" name="removeButton" > + <property name="text" > + <string>&Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="addFolderButton" > + <property name="text" > + <string>Add Folder</string> + </property> + </widget> + </item> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="standardButtons" > + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SearchLineEdit</class> + <extends>QLineEdit</extends> + <header>searchlineedit.h</header> + </customwidget> + <customwidget> + <class>EditTreeView</class> + <extends>QTreeView</extends> + <header>edittreeview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>BookmarksDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>472</x> + <y>329</y> + </hint> + <hint type="destinationlabel" > + <x>461</x> + <y>356</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/browser.icns b/examples/widgets/browser/browser.icns new file mode 100644 index 0000000000000000000000000000000000000000..f591ae48ae47974a1264f53c31b9b0c7d804648a GIT binary patch literal 50218 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̺gop)mQ{Uf-)l1(lU*q+)o3TP*b3SjU-Df;Bf;> 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 Y+1iFX9QR50J^$Nt4LsMtf3ybv1wr&H&j0`b literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/browser.ico b/examples/widgets/browser/browser.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f9be934db9019456d6196d0cb3ceede054b8f33 GIT binary patch literal 15374 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 G_5TC?ID*Ll literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/browser.pro b/examples/widgets/browser/browser.pro new file mode 100644 index 000000000..97559ed3d --- /dev/null +++ b/examples/widgets/browser/browser.pro @@ -0,0 +1,100 @@ +TEMPLATE = app +TARGET = browser +QT += webkitwidgets network widgets printsupport + +qtHaveModule(uitools):!embedded: QT += uitools +else: DEFINES += QT_NO_UITOOLS + +FORMS += \ + addbookmarkdialog.ui \ + bookmarks.ui \ + cookies.ui \ + cookiesexceptions.ui \ + downloaditem.ui \ + downloads.ui \ + history.ui \ + passworddialog.ui \ + proxy.ui \ + settings.ui + +HEADERS += \ + autosaver.h \ + bookmarks.h \ + browserapplication.h \ + browsermainwindow.h \ + chasewidget.h \ + cookiejar.h \ + downloadmanager.h \ + edittableview.h \ + edittreeview.h \ + history.h \ + modelmenu.h \ + networkaccessmanager.h \ + searchlineedit.h \ + settings.h \ + squeezelabel.h \ + tabwidget.h \ + toolbarsearch.h \ + urllineedit.h \ + webview.h \ + xbel.h + +SOURCES += \ + autosaver.cpp \ + bookmarks.cpp \ + browserapplication.cpp \ + browsermainwindow.cpp \ + chasewidget.cpp \ + cookiejar.cpp \ + downloadmanager.cpp \ + edittableview.cpp \ + edittreeview.cpp \ + history.cpp \ + modelmenu.cpp \ + networkaccessmanager.cpp \ + searchlineedit.cpp \ + settings.cpp \ + squeezelabel.cpp \ + tabwidget.cpp \ + toolbarsearch.cpp \ + urllineedit.cpp \ + webview.cpp \ + xbel.cpp \ + main.cpp + +RESOURCES += data/data.qrc htmls/htmls.qrc + +build_all:!build_pass { + CONFIG -= build_all + CONFIG += release +} + +win32 { + RC_FILE = browser.rc +} + +mac { + ICON = browser.icns + QMAKE_INFO_PLIST = Info_mac.plist + TARGET = Browser + + # No 64-bit Flash on Mac, so build the browser 32-bit + contains(QT_CONFIG, x86) { + CONFIG -= x86_64 + CONFIG += x86 + } + contains(QT_CONFIG, ppc) { + CONFIG -= ppc64 + CONFIG += ppc + } +} + +wince*: { + DEPLOYMENT_PLUGIN += qjpeg qgif +} + +EXAMPLE_FILES = Info_mac.plist browser.icns browser.ico browser.rc + +# install +target.path = $$[QT_INSTALL_EXAMPLES]/webkitwidgets/browser +INSTALLS += target diff --git a/examples/widgets/browser/browser.rc b/examples/widgets/browser/browser.rc new file mode 100644 index 000000000..39e17e973 --- /dev/null +++ b/examples/widgets/browser/browser.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "browser.ico" diff --git a/examples/widgets/browser/browserapplication.cpp b/examples/widgets/browser/browserapplication.cpp new file mode 100644 index 000000000..5230e58f9 --- /dev/null +++ b/examples/widgets/browser/browserapplication.cpp @@ -0,0 +1,458 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "browserapplication.h" + +#include "bookmarks.h" +#include "browsermainwindow.h" +#include "cookiejar.h" +#include "downloadmanager.h" +#include "history.h" +#include "networkaccessmanager.h" +#include "tabwidget.h" +#include "webview.h" + +#include <QtCore/QBuffer> +#include <QtCore/QDir> +#include <QtCore/QLibraryInfo> +#include <QtCore/QSettings> +#include <QtCore/QTextStream> +#include <QtCore/QTranslator> + +#include <QtGui/QDesktopServices> +#include <QtGui/QFileOpenEvent> +#include <QtWidgets/QMessageBox> + +#include <QtNetwork/QLocalServer> +#include <QtNetwork/QLocalSocket> +#include <QtNetwork/QNetworkProxy> +#include <QtNetwork/QSslSocket> + +#include <QWebSettings> + +#include <QtCore/QDebug> + +DownloadManager *BrowserApplication::s_downloadManager = 0; +HistoryManager *BrowserApplication::s_historyManager = 0; +NetworkAccessManager *BrowserApplication::s_networkAccessManager = 0; +BookmarksManager *BrowserApplication::s_bookmarksManager = 0; + +BrowserApplication::BrowserApplication(int &argc, char **argv) + : QApplication(argc, argv) + , m_localServer(0) +{ + QCoreApplication::setOrganizationName(QLatin1String("Qt")); + QCoreApplication::setApplicationName(QLatin1String("demobrowser")); + QCoreApplication::setApplicationVersion(QLatin1String("0.1")); +#ifdef Q_WS_QWS + // Use a different server name for QWS so we can run an X11 + // browser and a QWS browser in parallel on the same machine for + // debugging + QString serverName = QCoreApplication::applicationName() + QLatin1String("_qws"); +#else + QString serverName = QCoreApplication::applicationName(); +#endif + QLocalSocket socket; + socket.connectToServer(serverName); + if (socket.waitForConnected(500)) { + QTextStream stream(&socket); + QStringList args = QCoreApplication::arguments(); + if (args.count() > 1) + stream << args.last(); + else + stream << QString(); + stream.flush(); + socket.waitForBytesWritten(); + return; + } + +#if defined(Q_WS_MAC) + QApplication::setQuitOnLastWindowClosed(false); +#else + QApplication::setQuitOnLastWindowClosed(true); +#endif + + m_localServer = new QLocalServer(this); + connect(m_localServer, SIGNAL(newConnection()), + this, SLOT(newLocalSocketConnection())); + if (!m_localServer->listen(serverName)) { + if (m_localServer->serverError() == QAbstractSocket::AddressInUseError + && QFile::exists(m_localServer->serverName())) { + QFile::remove(m_localServer->serverName()); + m_localServer->listen(serverName); + } + } + +#ifndef QT_NO_OPENSSL + if (!QSslSocket::supportsSsl()) { + QMessageBox::information(0, "Demo Browser", + "This system does not support OpenSSL. SSL websites will not be available."); + } +#endif + + QDesktopServices::setUrlHandler(QLatin1String("http"), this, "openUrl"); + QString localSysName = QLocale::system().name(); + + installTranslator(QLatin1String("qt_") + localSysName); + + QSettings settings; + settings.beginGroup(QLatin1String("sessions")); + m_lastSession = settings.value(QLatin1String("lastSession")).toByteArray(); + settings.endGroup(); + +#if defined(Q_WS_MAC) + connect(this, SIGNAL(lastWindowClosed()), + this, SLOT(lastWindowClosed())); +#endif + + QTimer::singleShot(0, this, SLOT(postLaunch())); +} + +BrowserApplication::~BrowserApplication() +{ + delete s_downloadManager; + for (int i = 0; i < m_mainWindows.size(); ++i) { + BrowserMainWindow *window = m_mainWindows.at(i); + delete window; + } + delete s_networkAccessManager; + delete s_bookmarksManager; +} + +#if defined(Q_WS_MAC) +void BrowserApplication::lastWindowClosed() +{ + clean(); + BrowserMainWindow *mw = new BrowserMainWindow; + mw->slotHome(); + m_mainWindows.prepend(mw); +} +#endif + +BrowserApplication *BrowserApplication::instance() +{ + return (static_cast<BrowserApplication *>(QCoreApplication::instance())); +} + +#if defined(Q_WS_MAC) +#include <QtWidgets/QMessageBox> +void BrowserApplication::quitBrowser() +{ + clean(); + int tabCount = 0; + for (int i = 0; i < m_mainWindows.count(); ++i) { + tabCount =+ m_mainWindows.at(i)->tabWidget()->count(); + } + + if (tabCount > 1) { + int ret = QMessageBox::warning(mainWindow(), QString(), + tr("There are %1 windows and %2 tabs open\n" + "Do you want to quit anyway?").arg(m_mainWindows.count()).arg(tabCount), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (ret == QMessageBox::No) + return; + } + + exit(0); +} +#endif + +/*! + Any actions that can be delayed until the window is visible + */ +void BrowserApplication::postLaunch() +{ + QString directory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (directory.isEmpty()) + directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName(); + QWebSettings::setIconDatabasePath(directory); + QWebSettings::setOfflineStoragePath(directory); + + setWindowIcon(QIcon(QLatin1String(":browser.svg"))); + + loadSettings(); + + // newMainWindow() needs to be called in main() for this to happen + if (m_mainWindows.count() > 0) { + QStringList args = QCoreApplication::arguments(); + if (args.count() > 1) + mainWindow()->loadPage(args.last()); + else + mainWindow()->slotHome(); + } + BrowserApplication::historyManager(); +} + +void BrowserApplication::loadSettings() +{ + QSettings settings; + settings.beginGroup(QLatin1String("websettings")); + + QWebSettings *defaultSettings = QWebSettings::globalSettings(); + QString standardFontFamily = defaultSettings->fontFamily(QWebSettings::StandardFont); + int standardFontSize = defaultSettings->fontSize(QWebSettings::DefaultFontSize); + QFont standardFont = QFont(standardFontFamily, standardFontSize); + standardFont = qvariant_cast<QFont>(settings.value(QLatin1String("standardFont"), standardFont)); + defaultSettings->setFontFamily(QWebSettings::StandardFont, standardFont.family()); + defaultSettings->setFontSize(QWebSettings::DefaultFontSize, standardFont.pointSize()); + + QString fixedFontFamily = defaultSettings->fontFamily(QWebSettings::FixedFont); + int fixedFontSize = defaultSettings->fontSize(QWebSettings::DefaultFixedFontSize); + QFont fixedFont = QFont(fixedFontFamily, fixedFontSize); + fixedFont = qvariant_cast<QFont>(settings.value(QLatin1String("fixedFont"), fixedFont)); + defaultSettings->setFontFamily(QWebSettings::FixedFont, fixedFont.family()); + defaultSettings->setFontSize(QWebSettings::DefaultFixedFontSize, fixedFont.pointSize()); + + defaultSettings->setAttribute(QWebSettings::JavascriptEnabled, settings.value(QLatin1String("enableJavascript"), true).toBool()); + defaultSettings->setAttribute(QWebSettings::PluginsEnabled, settings.value(QLatin1String("enablePlugins"), true).toBool()); + + QUrl url = settings.value(QLatin1String("userStyleSheet")).toUrl(); + defaultSettings->setUserStyleSheetUrl(url); + + defaultSettings->setAttribute(QWebSettings::DnsPrefetchEnabled, true); + + settings.endGroup(); +} + +QList<BrowserMainWindow*> BrowserApplication::mainWindows() +{ + clean(); + QList<BrowserMainWindow*> list; + for (int i = 0; i < m_mainWindows.count(); ++i) + list.append(m_mainWindows.at(i)); + return list; +} + +void BrowserApplication::clean() +{ + // cleanup any deleted main windows first + for (int i = m_mainWindows.count() - 1; i >= 0; --i) + if (m_mainWindows.at(i).isNull()) + m_mainWindows.removeAt(i); +} + +void BrowserApplication::saveSession() +{ + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) + return; + + clean(); + + QSettings settings; + settings.beginGroup(QLatin1String("sessions")); + + QByteArray data; + QBuffer buffer(&data); + QDataStream stream(&buffer); + buffer.open(QIODevice::ReadWrite); + + stream << m_mainWindows.count(); + for (int i = 0; i < m_mainWindows.count(); ++i) + stream << m_mainWindows.at(i)->saveState(); + settings.setValue(QLatin1String("lastSession"), data); + settings.endGroup(); +} + +bool BrowserApplication::canRestoreSession() const +{ + return !m_lastSession.isEmpty(); +} + +void BrowserApplication::restoreLastSession() +{ + QList<QByteArray> windows; + QBuffer buffer(&m_lastSession); + QDataStream stream(&buffer); + buffer.open(QIODevice::ReadOnly); + int windowCount; + stream >> windowCount; + for (int i = 0; i < windowCount; ++i) { + QByteArray windowState; + stream >> windowState; + windows.append(windowState); + } + for (int i = 0; i < windows.count(); ++i) { + BrowserMainWindow *newWindow = 0; + if (m_mainWindows.count() == 1 + && mainWindow()->tabWidget()->count() == 1 + && mainWindow()->currentTab()->url() == QUrl()) { + newWindow = mainWindow(); + } else { + newWindow = newMainWindow(); + } + newWindow->restoreState(windows.at(i)); + } +} + +bool BrowserApplication::isTheOnlyBrowser() const +{ + return (m_localServer != 0); +} + +void BrowserApplication::installTranslator(const QString &name) +{ + QTranslator *translator = new QTranslator(this); + translator->load(name, QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + QApplication::installTranslator(translator); +} + +#if defined(Q_WS_MAC) +bool BrowserApplication::event(QEvent* event) +{ + switch (event->type()) { + case QEvent::ApplicationActivate: { + clean(); + if (!m_mainWindows.isEmpty()) { + BrowserMainWindow *mw = mainWindow(); + if (mw && !mw->isMinimized()) { + mainWindow()->show(); + } + return true; + } + } + case QEvent::FileOpen: + if (!m_mainWindows.isEmpty()) { + mainWindow()->loadPage(static_cast<QFileOpenEvent *>(event)->file()); + return true; + } + default: + break; + } + return QApplication::event(event); +} +#endif + +void BrowserApplication::openUrl(const QUrl &url) +{ + mainWindow()->loadPage(url.toString()); +} + +BrowserMainWindow *BrowserApplication::newMainWindow() +{ + BrowserMainWindow *browser = new BrowserMainWindow(); + m_mainWindows.prepend(browser); + browser->show(); + return browser; +} + +BrowserMainWindow *BrowserApplication::mainWindow() +{ + clean(); + if (m_mainWindows.isEmpty()) + newMainWindow(); + return m_mainWindows[0]; +} + +void BrowserApplication::newLocalSocketConnection() +{ + QLocalSocket *socket = m_localServer->nextPendingConnection(); + if (!socket) + return; + socket->waitForReadyRead(1000); + QTextStream stream(socket); + QString url; + stream >> url; + if (!url.isEmpty()) { + QSettings settings; + settings.beginGroup(QLatin1String("general")); + int openLinksIn = settings.value(QLatin1String("openLinksIn"), 0).toInt(); + settings.endGroup(); + if (openLinksIn == 1) + newMainWindow(); + else + mainWindow()->tabWidget()->newTab(); + openUrl(url); + } + delete socket; + mainWindow()->raise(); + mainWindow()->activateWindow(); +} + +CookieJar *BrowserApplication::cookieJar() +{ + return (CookieJar*)networkAccessManager()->cookieJar(); +} + +DownloadManager *BrowserApplication::downloadManager() +{ + if (!s_downloadManager) { + s_downloadManager = new DownloadManager(); + } + return s_downloadManager; +} + +NetworkAccessManager *BrowserApplication::networkAccessManager() +{ + if (!s_networkAccessManager) { + s_networkAccessManager = new NetworkAccessManager(); + s_networkAccessManager->setCookieJar(new CookieJar); + } + return s_networkAccessManager; +} + +HistoryManager *BrowserApplication::historyManager() +{ + if (!s_historyManager) { + s_historyManager = new HistoryManager(); + QWebHistoryInterface::setDefaultInterface(s_historyManager); + } + return s_historyManager; +} + +BookmarksManager *BrowserApplication::bookmarksManager() +{ + if (!s_bookmarksManager) { + s_bookmarksManager = new BookmarksManager; + } + return s_bookmarksManager; +} + +QIcon BrowserApplication::icon(const QUrl &url) const +{ + QIcon icon = QWebSettings::iconForUrl(url); + if (!icon.isNull()) + return icon.pixmap(16, 16); + if (m_defaultIcon.isNull()) + m_defaultIcon = QIcon(QLatin1String(":defaulticon.png")); + return m_defaultIcon.pixmap(16, 16); +} diff --git a/examples/widgets/browser/browserapplication.h b/examples/widgets/browser/browserapplication.h new file mode 100644 index 000000000..b17f1cea5 --- /dev/null +++ b/examples/widgets/browser/browserapplication.h @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BROWSERAPPLICATION_H +#define BROWSERAPPLICATION_H + +#include <QtWidgets/QApplication> + +#include <QtCore/QUrl> +#include <QtCore/QPointer> + +#include <QtGui/QIcon> + +QT_BEGIN_NAMESPACE +class QLocalServer; +QT_END_NAMESPACE + +class BookmarksManager; +class BrowserMainWindow; +class CookieJar; +class DownloadManager; +class HistoryManager; +class NetworkAccessManager; +class BrowserApplication : public QApplication +{ + Q_OBJECT + +public: + BrowserApplication(int &argc, char **argv); + ~BrowserApplication(); + static BrowserApplication *instance(); + void loadSettings(); + + bool isTheOnlyBrowser() const; + BrowserMainWindow *mainWindow(); + QList<BrowserMainWindow*> mainWindows(); + QIcon icon(const QUrl &url) const; + + void saveSession(); + bool canRestoreSession() const; + + static HistoryManager *historyManager(); + static CookieJar *cookieJar(); + static DownloadManager *downloadManager(); + static NetworkAccessManager *networkAccessManager(); + static BookmarksManager *bookmarksManager(); + +#if defined(Q_WS_MAC) + bool event(QEvent *event); +#endif + +public slots: + BrowserMainWindow *newMainWindow(); + void restoreLastSession(); +#if defined(Q_WS_MAC) + void lastWindowClosed(); + void quitBrowser(); +#endif + +private slots: + void postLaunch(); + void openUrl(const QUrl &url); + void newLocalSocketConnection(); + +private: + void clean(); + void installTranslator(const QString &name); + + static HistoryManager *s_historyManager; + static DownloadManager *s_downloadManager; + static NetworkAccessManager *s_networkAccessManager; + static BookmarksManager *s_bookmarksManager; + + QList<QPointer<BrowserMainWindow> > m_mainWindows; + QLocalServer *m_localServer; + QByteArray m_lastSession; + mutable QIcon m_defaultIcon; +}; + +#endif // BROWSERAPPLICATION_H diff --git a/examples/widgets/browser/browsermainwindow.cpp b/examples/widgets/browser/browsermainwindow.cpp new file mode 100644 index 000000000..ae9896c67 --- /dev/null +++ b/examples/widgets/browser/browsermainwindow.cpp @@ -0,0 +1,948 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "browsermainwindow.h" + +#include "autosaver.h" +#include "bookmarks.h" +#include "browserapplication.h" +#include "chasewidget.h" +#include "downloadmanager.h" +#include "history.h" +#include "settings.h" +#include "tabwidget.h" +#include "toolbarsearch.h" +#include "ui_passworddialog.h" +#include "webview.h" + +#include <QtCore/QSettings> + +#include <QtWidgets/QDesktopWidget> +#include <QtWidgets/QFileDialog> +#include <QtWidgets/QPlainTextEdit> +#include <QtPrintSupport/QPrintDialog> +#include <QtPrintSupport/QPrintPreviewDialog> +#include <QtPrintSupport/QPrinter> +#include <QtWidgets/QMenuBar> +#include <QtWidgets/QMessageBox> +#include <QtWidgets/QStatusBar> +#include <QtWidgets/QToolBar> +#include <QtWidgets/QInputDialog> + +#include <QWebFrame> +#include <QWebHistory> + +#include <QtCore/QDebug> + +BrowserMainWindow::BrowserMainWindow(QWidget *parent, Qt::WindowFlags flags) + : QMainWindow(parent, flags) + , m_tabWidget(new TabWidget(this)) + , m_autoSaver(new AutoSaver(this)) + , m_historyBack(0) + , m_historyForward(0) + , m_stop(0) + , m_reload(0) +{ + setToolButtonStyle(Qt::ToolButtonFollowStyle); + setAttribute(Qt::WA_DeleteOnClose, true); + statusBar()->setSizeGripEnabled(true); + setupMenu(); + setupToolBar(); + + QWidget *centralWidget = new QWidget(this); + BookmarksModel *boomarksModel = BrowserApplication::bookmarksManager()->bookmarksModel(); + m_bookmarksToolbar = new BookmarksToolBar(boomarksModel, this); + connect(m_bookmarksToolbar, SIGNAL(openUrl(QUrl)), + m_tabWidget, SLOT(loadUrlInCurrentTab(QUrl))); + connect(m_bookmarksToolbar->toggleViewAction(), SIGNAL(toggled(bool)), + this, SLOT(updateBookmarksToolbarActionText(bool))); + + QVBoxLayout *layout = new QVBoxLayout; + layout->setSpacing(0); + layout->setMargin(0); +#if defined(Q_WS_MAC) + layout->addWidget(m_bookmarksToolbar); + layout->addWidget(new QWidget); // <- OS X tab widget style bug +#else + addToolBarBreak(); + addToolBar(m_bookmarksToolbar); +#endif + layout->addWidget(m_tabWidget); + centralWidget->setLayout(layout); + setCentralWidget(centralWidget); + + connect(m_tabWidget, SIGNAL(loadPage(QString)), + this, SLOT(loadPage(QString))); + connect(m_tabWidget, SIGNAL(setCurrentTitle(QString)), + this, SLOT(slotUpdateWindowTitle(QString))); + connect(m_tabWidget, SIGNAL(showStatusBarMessage(QString)), + statusBar(), SLOT(showMessage(QString))); + connect(m_tabWidget, SIGNAL(linkHovered(QString)), + statusBar(), SLOT(showMessage(QString))); + connect(m_tabWidget, SIGNAL(loadProgress(int)), + this, SLOT(slotLoadProgress(int))); + connect(m_tabWidget, SIGNAL(tabsChanged()), + m_autoSaver, SLOT(changeOccurred())); + connect(m_tabWidget, SIGNAL(geometryChangeRequested(QRect)), + this, SLOT(geometryChangeRequested(QRect))); + connect(m_tabWidget, SIGNAL(printRequested(QWebFrame*)), + this, SLOT(printRequested(QWebFrame*))); + connect(m_tabWidget, SIGNAL(menuBarVisibilityChangeRequested(bool)), + menuBar(), SLOT(setVisible(bool))); + connect(m_tabWidget, SIGNAL(statusBarVisibilityChangeRequested(bool)), + statusBar(), SLOT(setVisible(bool))); + connect(m_tabWidget, SIGNAL(toolBarVisibilityChangeRequested(bool)), + m_navigationBar, SLOT(setVisible(bool))); + connect(m_tabWidget, SIGNAL(toolBarVisibilityChangeRequested(bool)), + m_bookmarksToolbar, SLOT(setVisible(bool))); +#if defined(Q_WS_MAC) + connect(m_tabWidget, SIGNAL(lastTabClosed()), + this, SLOT(close())); +#else + connect(m_tabWidget, SIGNAL(lastTabClosed()), + m_tabWidget, SLOT(newTab())); +#endif + + slotUpdateWindowTitle(); + loadDefaultState(); + m_tabWidget->newTab(); + + int size = m_tabWidget->lineEditStack()->sizeHint().height(); + m_navigationBar->setIconSize(QSize(size, size)); + +} + +BrowserMainWindow::~BrowserMainWindow() +{ + m_autoSaver->changeOccurred(); + m_autoSaver->saveIfNeccessary(); +} + +void BrowserMainWindow::loadDefaultState() +{ + QSettings settings; + settings.beginGroup(QLatin1String("BrowserMainWindow")); + QByteArray data = settings.value(QLatin1String("defaultState")).toByteArray(); + restoreState(data); + settings.endGroup(); +} + +QSize BrowserMainWindow::sizeHint() const +{ + QRect desktopRect = QApplication::desktop()->screenGeometry(); + QSize size = desktopRect.size() * qreal(0.9); + return size; +} + +void BrowserMainWindow::save() +{ + BrowserApplication::instance()->saveSession(); + + QSettings settings; + settings.beginGroup(QLatin1String("BrowserMainWindow")); + QByteArray data = saveState(false); + settings.setValue(QLatin1String("defaultState"), data); + settings.endGroup(); +} + +static const qint32 BrowserMainWindowMagic = 0xba; + +QByteArray BrowserMainWindow::saveState(bool withTabs) const +{ + int version = 2; + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + + stream << qint32(BrowserMainWindowMagic); + stream << qint32(version); + + stream << size(); + stream << !m_navigationBar->isHidden(); + stream << !m_bookmarksToolbar->isHidden(); + stream << !statusBar()->isHidden(); + if (withTabs) + stream << tabWidget()->saveState(); + else + stream << QByteArray(); + return data; +} + +bool BrowserMainWindow::restoreState(const QByteArray &state) +{ + int version = 2; + QByteArray sd = state; + QDataStream stream(&sd, QIODevice::ReadOnly); + if (stream.atEnd()) + return false; + + qint32 marker; + qint32 v; + stream >> marker; + stream >> v; + if (marker != BrowserMainWindowMagic || v != version) + return false; + + QSize size; + bool showToolbar; + bool showBookmarksBar; + bool showStatusbar; + QByteArray tabState; + + stream >> size; + stream >> showToolbar; + stream >> showBookmarksBar; + stream >> showStatusbar; + stream >> tabState; + + resize(size); + + m_navigationBar->setVisible(showToolbar); + updateToolbarActionText(showToolbar); + + m_bookmarksToolbar->setVisible(showBookmarksBar); + updateBookmarksToolbarActionText(showBookmarksBar); + + statusBar()->setVisible(showStatusbar); + updateStatusbarActionText(showStatusbar); + + if (!tabWidget()->restoreState(tabState)) + return false; + + return true; +} + +void BrowserMainWindow::setupMenu() +{ + new QShortcut(QKeySequence(Qt::Key_F6), this, SLOT(slotSwapFocus())); + + // File + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); + + fileMenu->addAction(tr("&New Window"), this, SLOT(slotFileNew()), QKeySequence::New); + fileMenu->addAction(m_tabWidget->newTabAction()); + fileMenu->addAction(tr("&Open File..."), this, SLOT(slotFileOpen()), QKeySequence::Open); + fileMenu->addAction(tr("Open &Location..."), this, + SLOT(slotSelectLineEdit()), QKeySequence(Qt::ControlModifier + Qt::Key_L)); + fileMenu->addSeparator(); + fileMenu->addAction(m_tabWidget->closeTabAction()); + fileMenu->addSeparator(); + fileMenu->addAction(tr("&Save As..."), this, + SLOT(slotFileSaveAs()), QKeySequence(QKeySequence::Save)); + fileMenu->addSeparator(); + BookmarksManager *bookmarksManager = BrowserApplication::bookmarksManager(); + fileMenu->addAction(tr("&Import Bookmarks..."), bookmarksManager, SLOT(importBookmarks())); + fileMenu->addAction(tr("&Export Bookmarks..."), bookmarksManager, SLOT(exportBookmarks())); + fileMenu->addSeparator(); + fileMenu->addAction(tr("P&rint Preview..."), this, SLOT(slotFilePrintPreview())); + fileMenu->addAction(tr("&Print..."), this, SLOT(slotFilePrint()), QKeySequence::Print); + fileMenu->addSeparator(); + QAction *action = fileMenu->addAction(tr("Private &Browsing..."), this, SLOT(slotPrivateBrowsing())); + action->setCheckable(true); + fileMenu->addSeparator(); + +#if defined(Q_WS_MAC) + fileMenu->addAction(tr("&Quit"), BrowserApplication::instance(), SLOT(quitBrowser()), QKeySequence(Qt::CTRL | Qt::Key_Q)); +#else + fileMenu->addAction(tr("&Quit"), this, SLOT(close()), QKeySequence(Qt::CTRL | Qt::Key_Q)); +#endif + + // Edit + QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); + QAction *m_undo = editMenu->addAction(tr("&Undo")); + m_undo->setShortcuts(QKeySequence::Undo); + m_tabWidget->addWebAction(m_undo, QWebPage::Undo); + QAction *m_redo = editMenu->addAction(tr("&Redo")); + m_redo->setShortcuts(QKeySequence::Redo); + m_tabWidget->addWebAction(m_redo, QWebPage::Redo); + editMenu->addSeparator(); + QAction *m_cut = editMenu->addAction(tr("Cu&t")); + m_cut->setShortcuts(QKeySequence::Cut); + m_tabWidget->addWebAction(m_cut, QWebPage::Cut); + QAction *m_copy = editMenu->addAction(tr("&Copy")); + m_copy->setShortcuts(QKeySequence::Copy); + m_tabWidget->addWebAction(m_copy, QWebPage::Copy); + QAction *m_paste = editMenu->addAction(tr("&Paste")); + m_paste->setShortcuts(QKeySequence::Paste); + m_tabWidget->addWebAction(m_paste, QWebPage::Paste); + editMenu->addSeparator(); + + QAction *m_find = editMenu->addAction(tr("&Find")); + m_find->setShortcuts(QKeySequence::Find); + connect(m_find, SIGNAL(triggered()), this, SLOT(slotEditFind())); + new QShortcut(QKeySequence(Qt::Key_Slash), this, SLOT(slotEditFind())); + + QAction *m_findNext = editMenu->addAction(tr("&Find Next")); + m_findNext->setShortcuts(QKeySequence::FindNext); + connect(m_findNext, SIGNAL(triggered()), this, SLOT(slotEditFindNext())); + + QAction *m_findPrevious = editMenu->addAction(tr("&Find Previous")); + m_findPrevious->setShortcuts(QKeySequence::FindPrevious); + connect(m_findPrevious, SIGNAL(triggered()), this, SLOT(slotEditFindPrevious())); + + editMenu->addSeparator(); + editMenu->addAction(tr("&Preferences"), this, SLOT(slotPreferences()), tr("Ctrl+,")); + + // View + QMenu *viewMenu = menuBar()->addMenu(tr("&View")); + + m_viewBookmarkBar = new QAction(this); + updateBookmarksToolbarActionText(true); + m_viewBookmarkBar->setShortcut(tr("Shift+Ctrl+B")); + connect(m_viewBookmarkBar, SIGNAL(triggered()), this, SLOT(slotViewBookmarksBar())); + viewMenu->addAction(m_viewBookmarkBar); + + m_viewToolbar = new QAction(this); + updateToolbarActionText(true); + m_viewToolbar->setShortcut(tr("Ctrl+|")); + connect(m_viewToolbar, SIGNAL(triggered()), this, SLOT(slotViewToolbar())); + viewMenu->addAction(m_viewToolbar); + + m_viewStatusbar = new QAction(this); + updateStatusbarActionText(true); + m_viewStatusbar->setShortcut(tr("Ctrl+/")); + connect(m_viewStatusbar, SIGNAL(triggered()), this, SLOT(slotViewStatusbar())); + viewMenu->addAction(m_viewStatusbar); + + viewMenu->addSeparator(); + + m_stop = viewMenu->addAction(tr("&Stop")); + QList<QKeySequence> shortcuts; + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_Period)); + shortcuts.append(Qt::Key_Escape); + m_stop->setShortcuts(shortcuts); + m_tabWidget->addWebAction(m_stop, QWebPage::Stop); + + m_reload = viewMenu->addAction(tr("Reload Page")); + m_reload->setShortcuts(QKeySequence::Refresh); + m_tabWidget->addWebAction(m_reload, QWebPage::Reload); + + viewMenu->addAction(tr("Zoom &In"), this, SLOT(slotViewZoomIn()), QKeySequence(Qt::CTRL | Qt::Key_Plus)); + viewMenu->addAction(tr("Zoom &Out"), this, SLOT(slotViewZoomOut()), QKeySequence(Qt::CTRL | Qt::Key_Minus)); + viewMenu->addAction(tr("Reset &Zoom"), this, SLOT(slotViewResetZoom()), QKeySequence(Qt::CTRL | Qt::Key_0)); + QAction *zoomTextOnlyAction = viewMenu->addAction(tr("Zoom &Text Only")); + connect(zoomTextOnlyAction, SIGNAL(toggled(bool)), this, SLOT(slotViewZoomTextOnly(bool))); + zoomTextOnlyAction->setCheckable(true); + zoomTextOnlyAction->setChecked(false); + + viewMenu->addSeparator(); + viewMenu->addAction(tr("Page S&ource"), this, SLOT(slotViewPageSource()), tr("Ctrl+Alt+U")); + QAction *a = viewMenu->addAction(tr("&Full Screen"), this, SLOT(slotViewFullScreen(bool)), Qt::Key_F11); + a->setCheckable(true); + + // History + HistoryMenu *historyMenu = new HistoryMenu(this); + connect(historyMenu, SIGNAL(openUrl(QUrl)), + m_tabWidget, SLOT(loadUrlInCurrentTab(QUrl))); + connect(historyMenu, SIGNAL(hovered(QString)), this, + SLOT(slotUpdateStatusbar(QString))); + historyMenu->setTitle(tr("Hi&story")); + menuBar()->addMenu(historyMenu); + QList<QAction*> historyActions; + + m_historyBack = new QAction(tr("Back"), this); + m_tabWidget->addWebAction(m_historyBack, QWebPage::Back); + m_historyBack->setShortcuts(QKeySequence::Back); + m_historyBack->setIconVisibleInMenu(false); + + m_historyForward = new QAction(tr("Forward"), this); + m_tabWidget->addWebAction(m_historyForward, QWebPage::Forward); + m_historyForward->setShortcuts(QKeySequence::Forward); + m_historyForward->setIconVisibleInMenu(false); + + QAction *m_historyHome = new QAction(tr("Home"), this); + connect(m_historyHome, SIGNAL(triggered()), this, SLOT(slotHome())); + m_historyHome->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_H)); + + m_restoreLastSession = new QAction(tr("Restore Last Session"), this); + connect(m_restoreLastSession, SIGNAL(triggered()), BrowserApplication::instance(), SLOT(restoreLastSession())); + m_restoreLastSession->setEnabled(BrowserApplication::instance()->canRestoreSession()); + + historyActions.append(m_historyBack); + historyActions.append(m_historyForward); + historyActions.append(m_historyHome); + historyActions.append(m_tabWidget->recentlyClosedTabsAction()); + historyActions.append(m_restoreLastSession); + historyMenu->setInitialActions(historyActions); + + // Bookmarks + BookmarksMenu *bookmarksMenu = new BookmarksMenu(this); + connect(bookmarksMenu, SIGNAL(openUrl(QUrl)), + m_tabWidget, SLOT(loadUrlInCurrentTab(QUrl))); + connect(bookmarksMenu, SIGNAL(hovered(QString)), + this, SLOT(slotUpdateStatusbar(QString))); + bookmarksMenu->setTitle(tr("&Bookmarks")); + menuBar()->addMenu(bookmarksMenu); + + QList<QAction*> bookmarksActions; + + QAction *showAllBookmarksAction = new QAction(tr("Show All Bookmarks"), this); + connect(showAllBookmarksAction, SIGNAL(triggered()), this, SLOT(slotShowBookmarksDialog())); + m_addBookmark = new QAction(QIcon(QLatin1String(":addbookmark.png")), tr("Add Bookmark..."), this); + m_addBookmark->setIconVisibleInMenu(false); + + connect(m_addBookmark, SIGNAL(triggered()), this, SLOT(slotAddBookmark())); + m_addBookmark->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D)); + + bookmarksActions.append(showAllBookmarksAction); + bookmarksActions.append(m_addBookmark); + bookmarksMenu->setInitialActions(bookmarksActions); + + // Window + m_windowMenu = menuBar()->addMenu(tr("&Window")); + connect(m_windowMenu, SIGNAL(aboutToShow()), + this, SLOT(slotAboutToShowWindowMenu())); + slotAboutToShowWindowMenu(); + + QMenu *toolsMenu = menuBar()->addMenu(tr("&Tools")); + toolsMenu->addAction(tr("Web &Search"), this, SLOT(slotWebSearch()), QKeySequence(tr("Ctrl+K", "Web Search"))); + a = toolsMenu->addAction(tr("Enable Web &Inspector"), this, SLOT(slotToggleInspector(bool))); + a->setCheckable(true); + + QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); + helpMenu->addAction(tr("About &Qt"), qApp, SLOT(aboutQt())); + helpMenu->addAction(tr("About &Demo Browser"), this, SLOT(slotAboutApplication())); +} + +void BrowserMainWindow::setupToolBar() +{ + setUnifiedTitleAndToolBarOnMac(true); + m_navigationBar = addToolBar(tr("Navigation")); + connect(m_navigationBar->toggleViewAction(), SIGNAL(toggled(bool)), + this, SLOT(updateToolbarActionText(bool))); + + m_historyBack->setIcon(style()->standardIcon(QStyle::SP_ArrowBack, 0, this)); + m_historyBackMenu = new QMenu(this); + m_historyBack->setMenu(m_historyBackMenu); + connect(m_historyBackMenu, SIGNAL(aboutToShow()), + this, SLOT(slotAboutToShowBackMenu())); + connect(m_historyBackMenu, SIGNAL(triggered(QAction*)), + this, SLOT(slotOpenActionUrl(QAction*))); + m_navigationBar->addAction(m_historyBack); + + m_historyForward->setIcon(style()->standardIcon(QStyle::SP_ArrowForward, 0, this)); + m_historyForwardMenu = new QMenu(this); + connect(m_historyForwardMenu, SIGNAL(aboutToShow()), + this, SLOT(slotAboutToShowForwardMenu())); + connect(m_historyForwardMenu, SIGNAL(triggered(QAction*)), + this, SLOT(slotOpenActionUrl(QAction*))); + m_historyForward->setMenu(m_historyForwardMenu); + m_navigationBar->addAction(m_historyForward); + + m_stopReload = new QAction(this); + m_reloadIcon = style()->standardIcon(QStyle::SP_BrowserReload); + m_stopReload->setIcon(m_reloadIcon); + + m_navigationBar->addAction(m_stopReload); + + m_navigationBar->addWidget(m_tabWidget->lineEditStack()); + + m_toolbarSearch = new ToolbarSearch(m_navigationBar); + m_navigationBar->addWidget(m_toolbarSearch); + connect(m_toolbarSearch, SIGNAL(search(QUrl)), SLOT(loadUrl(QUrl))); + + m_chaseWidget = new ChaseWidget(this); + m_navigationBar->addWidget(m_chaseWidget); +} + +void BrowserMainWindow::slotShowBookmarksDialog() +{ + BookmarksDialog *dialog = new BookmarksDialog(this); + connect(dialog, SIGNAL(openUrl(QUrl)), + m_tabWidget, SLOT(loadUrlInCurrentTab(QUrl))); + dialog->show(); +} + +void BrowserMainWindow::slotAddBookmark() +{ + WebView *webView = currentTab(); + QString url = webView->url().toString(); + QString title = webView->title(); + AddBookmarkDialog dialog(url, title); + dialog.exec(); +} + +void BrowserMainWindow::slotViewToolbar() +{ + if (m_navigationBar->isVisible()) { + updateToolbarActionText(false); + m_navigationBar->close(); + } else { + updateToolbarActionText(true); + m_navigationBar->show(); + } + m_autoSaver->changeOccurred(); +} + +void BrowserMainWindow::slotViewBookmarksBar() +{ + if (m_bookmarksToolbar->isVisible()) { + updateBookmarksToolbarActionText(false); + m_bookmarksToolbar->close(); + } else { + updateBookmarksToolbarActionText(true); + m_bookmarksToolbar->show(); + } + m_autoSaver->changeOccurred(); +} + +void BrowserMainWindow::updateStatusbarActionText(bool visible) +{ + m_viewStatusbar->setText(!visible ? tr("Show Status Bar") : tr("Hide Status Bar")); +} + +void BrowserMainWindow::updateToolbarActionText(bool visible) +{ + m_viewToolbar->setText(!visible ? tr("Show Toolbar") : tr("Hide Toolbar")); +} + +void BrowserMainWindow::updateBookmarksToolbarActionText(bool visible) +{ + m_viewBookmarkBar->setText(!visible ? tr("Show Bookmarks bar") : tr("Hide Bookmarks bar")); +} + +void BrowserMainWindow::slotViewStatusbar() +{ + if (statusBar()->isVisible()) { + updateStatusbarActionText(false); + statusBar()->close(); + } else { + updateStatusbarActionText(true); + statusBar()->show(); + } + m_autoSaver->changeOccurred(); +} + +void BrowserMainWindow::loadUrl(const QUrl &url) +{ + if (!currentTab() || !url.isValid()) + return; + + m_tabWidget->currentLineEdit()->setText(QString::fromUtf8(url.toEncoded())); + m_tabWidget->loadUrlInCurrentTab(url); +} + +void BrowserMainWindow::slotDownloadManager() +{ + BrowserApplication::downloadManager()->show(); +} + +void BrowserMainWindow::slotSelectLineEdit() +{ + m_tabWidget->currentLineEdit()->selectAll(); + m_tabWidget->currentLineEdit()->setFocus(); +} + +void BrowserMainWindow::slotFileSaveAs() +{ + BrowserApplication::downloadManager()->download(currentTab()->url(), true); +} + +void BrowserMainWindow::slotPreferences() +{ + SettingsDialog *s = new SettingsDialog(this); + s->show(); +} + +void BrowserMainWindow::slotUpdateStatusbar(const QString &string) +{ + statusBar()->showMessage(string, 2000); +} + +void BrowserMainWindow::slotUpdateWindowTitle(const QString &title) +{ + if (title.isEmpty()) { + setWindowTitle(tr("Qt Demo Browser")); + } else { +#if defined(Q_WS_MAC) + setWindowTitle(title); +#else + setWindowTitle(tr("%1 - Qt Demo Browser", "Page title and Browser name").arg(title)); +#endif + } +} + +void BrowserMainWindow::slotAboutApplication() +{ + QMessageBox::about(this, tr("About"), tr( + "Version %1" + "<p>This demo demonstrates Qt's " + "webkit facilities in action, providing an example " + "browser for you to experiment with.<p>" + "<p>QtWebKit is based on the Open Source WebKit Project developed at <a href=\"http://webkit.org/\">http://webkit.org/</a>." + ).arg(QCoreApplication::applicationVersion())); +} + +void BrowserMainWindow::slotFileNew() +{ + BrowserApplication::instance()->newMainWindow(); + BrowserMainWindow *mw = BrowserApplication::instance()->mainWindow(); + mw->slotHome(); +} + +void BrowserMainWindow::slotFileOpen() +{ + QString file = QFileDialog::getOpenFileName(this, tr("Open Web Resource"), QString(), + tr("Web Resources (*.html *.htm *.svg *.png *.gif *.svgz);;All files (*.*)")); + + if (file.isEmpty()) + return; + + loadPage(file); +} + +void BrowserMainWindow::slotFilePrintPreview() +{ +#ifndef QT_NO_PRINTPREVIEWDIALOG + if (!currentTab()) + return; + QPrintPreviewDialog *dialog = new QPrintPreviewDialog(this); + connect(dialog, SIGNAL(paintRequested(QPrinter*)), + currentTab(), SLOT(print(QPrinter*))); + dialog->exec(); +#endif +} + +void BrowserMainWindow::slotFilePrint() +{ + if (!currentTab()) + return; + printRequested(currentTab()->page()->mainFrame()); +} + +void BrowserMainWindow::printRequested(QWebFrame *frame) +{ +#ifndef QT_NO_PRINTDIALOG + QPrinter printer; + QPrintDialog *dialog = new QPrintDialog(&printer, this); + dialog->setWindowTitle(tr("Print Document")); + if (dialog->exec() != QDialog::Accepted) + return; + frame->print(&printer); +#endif +} + +void BrowserMainWindow::slotPrivateBrowsing() +{ + QWebSettings *settings = QWebSettings::globalSettings(); + bool pb = settings->testAttribute(QWebSettings::PrivateBrowsingEnabled); + if (!pb) { + QString title = tr("Are you sure you want to turn on private browsing?"); + QString text = tr("<b>%1</b><br><br>When private browsing in turned on," + " webpages are not added to the history," + " items are automatically removed from the Downloads window," \ + " new cookies are not stored, current cookies can't be accessed," \ + " site icons wont be stored, session wont be saved, " \ + " and searches are not added to the pop-up menu in the Google search box." \ + " Until you close the window, you can still click the Back and Forward buttons" \ + " to return to the webpages you have opened.").arg(title); + + QMessageBox::StandardButton button = QMessageBox::question(this, QString(), text, + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Ok); + if (button == QMessageBox::Ok) { + settings->setAttribute(QWebSettings::PrivateBrowsingEnabled, true); + } + } else { + settings->setAttribute(QWebSettings::PrivateBrowsingEnabled, false); + + QList<BrowserMainWindow*> windows = BrowserApplication::instance()->mainWindows(); + for (int i = 0; i < windows.count(); ++i) { + BrowserMainWindow *window = windows.at(i); + window->m_lastSearch = QString::null; + window->tabWidget()->clear(); + } + } +} + +void BrowserMainWindow::closeEvent(QCloseEvent *event) +{ + if (m_tabWidget->count() > 1) { + int ret = QMessageBox::warning(this, QString(), + tr("Are you sure you want to close the window?" + " There are %1 tabs open").arg(m_tabWidget->count()), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (ret == QMessageBox::No) { + event->ignore(); + return; + } + } + event->accept(); + deleteLater(); +} + +void BrowserMainWindow::slotEditFind() +{ + if (!currentTab()) + return; + bool ok; + QString search = QInputDialog::getText(this, tr("Find"), + tr("Text:"), QLineEdit::Normal, + m_lastSearch, &ok); + if (ok && !search.isEmpty()) { + m_lastSearch = search; + if (!currentTab()->findText(m_lastSearch)) + slotUpdateStatusbar(tr("\"%1\" not found.").arg(m_lastSearch)); + } +} + +void BrowserMainWindow::slotEditFindNext() +{ + if (!currentTab() && !m_lastSearch.isEmpty()) + return; + currentTab()->findText(m_lastSearch); +} + +void BrowserMainWindow::slotEditFindPrevious() +{ + if (!currentTab() && !m_lastSearch.isEmpty()) + return; + currentTab()->findText(m_lastSearch, QWebPage::FindBackward); +} + +void BrowserMainWindow::slotViewZoomIn() +{ + if (!currentTab()) + return; + currentTab()->setZoomFactor(currentTab()->zoomFactor() + 0.1); +} + +void BrowserMainWindow::slotViewZoomOut() +{ + if (!currentTab()) + return; + currentTab()->setZoomFactor(currentTab()->zoomFactor() - 0.1); +} + +void BrowserMainWindow::slotViewResetZoom() +{ + if (!currentTab()) + return; + currentTab()->setZoomFactor(1.0); +} + +void BrowserMainWindow::slotViewZoomTextOnly(bool enable) +{ + if (!currentTab()) + return; + currentTab()->page()->settings()->setAttribute(QWebSettings::ZoomTextOnly, enable); +} + +void BrowserMainWindow::slotViewFullScreen(bool makeFullScreen) +{ + if (makeFullScreen) { + showFullScreen(); + } else { + if (isMinimized()) + showMinimized(); + else if (isMaximized()) + showMaximized(); + else showNormal(); + } +} + +void BrowserMainWindow::slotViewPageSource() +{ + if (!currentTab()) + return; + + QString markup = currentTab()->page()->mainFrame()->toHtml(); + QPlainTextEdit *view = new QPlainTextEdit(markup); + view->setWindowTitle(tr("Page Source of %1").arg(currentTab()->title())); + view->setMinimumWidth(640); + view->setAttribute(Qt::WA_DeleteOnClose); + view->show(); +} + +void BrowserMainWindow::slotHome() +{ + QSettings settings; + settings.beginGroup(QLatin1String("MainWindow")); + QString home = settings.value(QLatin1String("home"), QLatin1String("http://qt-project.org/")).toString(); + loadPage(home); +} + +void BrowserMainWindow::slotWebSearch() +{ + m_toolbarSearch->lineEdit()->selectAll(); + m_toolbarSearch->lineEdit()->setFocus(); +} + +void BrowserMainWindow::slotToggleInspector(bool enable) +{ + QWebSettings::globalSettings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, enable); + if (enable) { + int result = QMessageBox::question(this, tr("Web Inspector"), + tr("The web inspector will only work correctly for pages that were loaded after enabling.\n" + "Do you want to reload all pages?"), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) { + m_tabWidget->reloadAllTabs(); + } + } +} + +void BrowserMainWindow::slotSwapFocus() +{ + if (currentTab()->hasFocus()) + m_tabWidget->currentLineEdit()->setFocus(); + else + currentTab()->setFocus(); +} + +void BrowserMainWindow::loadPage(const QString &page) +{ + QUrl url = QUrl::fromUserInput(page); + loadUrl(url); +} + +TabWidget *BrowserMainWindow::tabWidget() const +{ + return m_tabWidget; +} + +WebView *BrowserMainWindow::currentTab() const +{ + return m_tabWidget->currentWebView(); +} + +void BrowserMainWindow::slotLoadProgress(int progress) +{ + if (progress < 100 && progress > 0) { + m_chaseWidget->setAnimated(true); + disconnect(m_stopReload, SIGNAL(triggered()), m_reload, SLOT(trigger())); + if (m_stopIcon.isNull()) + m_stopIcon = style()->standardIcon(QStyle::SP_BrowserStop); + m_stopReload->setIcon(m_stopIcon); + connect(m_stopReload, SIGNAL(triggered()), m_stop, SLOT(trigger())); + m_stopReload->setToolTip(tr("Stop loading the current page")); + } else { + m_chaseWidget->setAnimated(false); + disconnect(m_stopReload, SIGNAL(triggered()), m_stop, SLOT(trigger())); + m_stopReload->setIcon(m_reloadIcon); + connect(m_stopReload, SIGNAL(triggered()), m_reload, SLOT(trigger())); + m_stopReload->setToolTip(tr("Reload the current page")); + } +} + +void BrowserMainWindow::slotAboutToShowBackMenu() +{ + m_historyBackMenu->clear(); + if (!currentTab()) + return; + QWebHistory *history = currentTab()->history(); + int historyCount = history->count(); + for (int i = history->backItems(historyCount).count() - 1; i >= 0; --i) { + QWebHistoryItem item = history->backItems(history->count()).at(i); + QAction *action = new QAction(this); + action->setData(-1*(historyCount-i-1)); + QIcon icon = BrowserApplication::instance()->icon(item.url()); + action->setIcon(icon); + action->setText(item.title()); + m_historyBackMenu->addAction(action); + } +} + +void BrowserMainWindow::slotAboutToShowForwardMenu() +{ + m_historyForwardMenu->clear(); + if (!currentTab()) + return; + QWebHistory *history = currentTab()->history(); + int historyCount = history->count(); + for (int i = 0; i < history->forwardItems(history->count()).count(); ++i) { + QWebHistoryItem item = history->forwardItems(historyCount).at(i); + QAction *action = new QAction(this); + action->setData(historyCount-i); + QIcon icon = BrowserApplication::instance()->icon(item.url()); + action->setIcon(icon); + action->setText(item.title()); + m_historyForwardMenu->addAction(action); + } +} + +void BrowserMainWindow::slotAboutToShowWindowMenu() +{ + m_windowMenu->clear(); + m_windowMenu->addAction(m_tabWidget->nextTabAction()); + m_windowMenu->addAction(m_tabWidget->previousTabAction()); + m_windowMenu->addSeparator(); + m_windowMenu->addAction(tr("Downloads"), this, SLOT(slotDownloadManager()), QKeySequence(tr("Alt+Ctrl+L", "Download Manager"))); + + m_windowMenu->addSeparator(); + QList<BrowserMainWindow*> windows = BrowserApplication::instance()->mainWindows(); + for (int i = 0; i < windows.count(); ++i) { + BrowserMainWindow *window = windows.at(i); + QAction *action = m_windowMenu->addAction(window->windowTitle(), this, SLOT(slotShowWindow())); + action->setData(i); + action->setCheckable(true); + if (window == this) + action->setChecked(true); + } +} + +void BrowserMainWindow::slotShowWindow() +{ + if (QAction *action = qobject_cast<QAction*>(sender())) { + QVariant v = action->data(); + if (v.canConvert<int>()) { + int offset = qvariant_cast<int>(v); + QList<BrowserMainWindow*> windows = BrowserApplication::instance()->mainWindows(); + windows.at(offset)->activateWindow(); + windows.at(offset)->currentTab()->setFocus(); + } + } +} + +void BrowserMainWindow::slotOpenActionUrl(QAction *action) +{ + int offset = action->data().toInt(); + QWebHistory *history = currentTab()->history(); + if (offset < 0) + history->goToItem(history->backItems(-1*offset).first()); // back + else if (offset > 0) + history->goToItem(history->forwardItems(history->count() - offset + 1).back()); // forward + } + +void BrowserMainWindow::geometryChangeRequested(const QRect &geometry) +{ + setGeometry(geometry); +} diff --git a/examples/widgets/browser/browsermainwindow.h b/examples/widgets/browser/browsermainwindow.h new file mode 100644 index 000000000..a6905e0bc --- /dev/null +++ b/examples/widgets/browser/browsermainwindow.h @@ -0,0 +1,167 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BROWSERMAINWINDOW_H +#define BROWSERMAINWINDOW_H + +#include <QtWidgets/QMainWindow> +#include <QtGui/QIcon> +#include <QtCore/QUrl> + +class AutoSaver; +class BookmarksToolBar; +class ChaseWidget; +class QWebFrame; +class TabWidget; +class ToolbarSearch; +class WebView; + +/*! + The MainWindow of the Browser Application. + + Handles the tab widget and all the actions + */ +class BrowserMainWindow : public QMainWindow { + Q_OBJECT + +public: + BrowserMainWindow(QWidget *parent = 0, Qt::WindowFlags flags = 0); + ~BrowserMainWindow(); + QSize sizeHint() const; + +public: + TabWidget *tabWidget() const; + WebView *currentTab() const; + QByteArray saveState(bool withTabs = true) const; + bool restoreState(const QByteArray &state); + +public slots: + void loadPage(const QString &url); + void slotHome(); + +protected: + void closeEvent(QCloseEvent *event); + +private slots: + void save(); + + void slotLoadProgress(int); + void slotUpdateStatusbar(const QString &string); + void slotUpdateWindowTitle(const QString &title = QString()); + + void loadUrl(const QUrl &url); + void slotPreferences(); + + void slotFileNew(); + void slotFileOpen(); + void slotFilePrintPreview(); + void slotFilePrint(); + void slotPrivateBrowsing(); + void slotFileSaveAs(); + void slotEditFind(); + void slotEditFindNext(); + void slotEditFindPrevious(); + void slotShowBookmarksDialog(); + void slotAddBookmark(); + void slotViewZoomIn(); + void slotViewZoomOut(); + void slotViewResetZoom(); + void slotViewZoomTextOnly(bool enable); + void slotViewToolbar(); + void slotViewBookmarksBar(); + void slotViewStatusbar(); + void slotViewPageSource(); + void slotViewFullScreen(bool enable); + + void slotWebSearch(); + void slotToggleInspector(bool enable); + void slotAboutApplication(); + void slotDownloadManager(); + void slotSelectLineEdit(); + + void slotAboutToShowBackMenu(); + void slotAboutToShowForwardMenu(); + void slotAboutToShowWindowMenu(); + void slotOpenActionUrl(QAction *action); + void slotShowWindow(); + void slotSwapFocus(); + + void printRequested(QWebFrame *frame); + void geometryChangeRequested(const QRect &geometry); + void updateToolbarActionText(bool visible); + void updateBookmarksToolbarActionText(bool visible); + +private: + void loadDefaultState(); + void setupMenu(); + void setupToolBar(); + void updateStatusbarActionText(bool visible); + +private: + QToolBar *m_navigationBar; + ToolbarSearch *m_toolbarSearch; + BookmarksToolBar *m_bookmarksToolbar; + ChaseWidget *m_chaseWidget; + TabWidget *m_tabWidget; + AutoSaver *m_autoSaver; + + QAction *m_historyBack; + QMenu *m_historyBackMenu; + QAction *m_historyForward; + QMenu *m_historyForwardMenu; + QMenu *m_windowMenu; + + QAction *m_stop; + QAction *m_reload; + QAction *m_stopReload; + QAction *m_viewToolbar; + QAction *m_viewBookmarkBar; + QAction *m_viewStatusbar; + QAction *m_restoreLastSession; + QAction *m_addBookmark; + + QIcon m_reloadIcon; + QIcon m_stopIcon; + + QString m_lastSearch; +}; + +#endif // BROWSERMAINWINDOW_H diff --git a/examples/widgets/browser/chasewidget.cpp b/examples/widgets/browser/chasewidget.cpp new file mode 100644 index 000000000..c34dde739 --- /dev/null +++ b/examples/widgets/browser/chasewidget.cpp @@ -0,0 +1,141 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "chasewidget.h" + +#include <QtCore/QPoint> + +#include <QtWidgets/QApplication> +#include <QtGui/QHideEvent> +#include <QtGui/QPainter> +#include <QtGui/QPaintEvent> +#include <QtGui/QShowEvent> + +ChaseWidget::ChaseWidget(QWidget *parent, QPixmap pixmap, bool pixmapEnabled) + : QWidget(parent) + , m_segment(0) + , m_delay(100) + , m_step(40) + , m_timerId(-1) + , m_animated(false) + , m_pixmap(pixmap) + , m_pixmapEnabled(pixmapEnabled) +{ +} + +void ChaseWidget::setAnimated(bool value) +{ + if (m_animated == value) + return; + m_animated = value; + if (m_timerId != -1) { + killTimer(m_timerId); + m_timerId = -1; + } + if (m_animated) { + m_segment = 0; + m_timerId = startTimer(m_delay); + } + update(); +} + +void ChaseWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter p(this); + if (m_pixmapEnabled && !m_pixmap.isNull()) { + p.drawPixmap(0, 0, m_pixmap); + return; + } + + const int extent = qMin(width() - 8, height() - 8); + const int displ = extent / 4; + const int ext = extent / 4 - 1; + + p.setRenderHint(QPainter::Antialiasing, true); + + if (m_animated) + p.setPen(Qt::gray); + else + p.setPen(QPen(palette().dark().color())); + + p.translate(width() / 2, height() / 2); // center + + for (int segment = 0; segment < segmentCount(); ++segment) { + p.rotate(QApplication::isRightToLeft() ? m_step : -m_step); + if (m_animated) + p.setBrush(colorForSegment(segment)); + else + p.setBrush(palette().background()); + p.drawEllipse(QRect(displ, -ext / 2, ext, ext)); + } +} + +QSize ChaseWidget::sizeHint() const +{ + return QSize(32, 32); +} + +void ChaseWidget::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timerId) { + ++m_segment; + update(); + } + QWidget::timerEvent(event); +} + +QColor ChaseWidget::colorForSegment(int seg) const +{ + int index = ((seg + m_segment) % segmentCount()); + int comp = qMax(0, 255 - (index * (255 / segmentCount()))); + return QColor(comp, comp, comp, 255); +} + +int ChaseWidget::segmentCount() const +{ + return 360 / m_step; +} + +void ChaseWidget::setPixmapEnabled(bool enable) +{ + m_pixmapEnabled = enable; +} diff --git a/examples/widgets/browser/chasewidget.h b/examples/widgets/browser/chasewidget.h new file mode 100644 index 000000000..2c57d08ac --- /dev/null +++ b/examples/widgets/browser/chasewidget.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CHASEWIDGET_H +#define CHASEWIDGET_H + +#include <QtWidgets/QWidget> + +#include <QtCore/QSize> +#include <QtGui/QColor> +#include <QtGui/QPixmap> + +QT_BEGIN_NAMESPACE +class QHideEvent; +class QShowEvent; +class QPaintEvent; +class QTimerEvent; +QT_END_NAMESPACE + +class ChaseWidget : public QWidget +{ + Q_OBJECT +public: + ChaseWidget(QWidget *parent = 0, QPixmap pixmap = QPixmap(), bool pixmapEnabled = false); + + void setAnimated(bool value); + void setPixmapEnabled(bool enable); + QSize sizeHint() const; + +protected: + void paintEvent(QPaintEvent *event); + void timerEvent(QTimerEvent *event); + +private: + int segmentCount() const; + QColor colorForSegment(int segment) const; + + int m_segment; + int m_delay; + int m_step; + int m_timerId; + bool m_animated; + QPixmap m_pixmap; + bool m_pixmapEnabled; +}; + +#endif diff --git a/examples/widgets/browser/cookiejar.cpp b/examples/widgets/browser/cookiejar.cpp new file mode 100644 index 000000000..28eae7c89 --- /dev/null +++ b/examples/widgets/browser/cookiejar.cpp @@ -0,0 +1,737 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "cookiejar.h" + +#include "autosaver.h" + +#include <QtCore/QDateTime> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QMetaEnum> +#include <QtCore/QSettings> +#include <QtCore/QUrl> + +#include <QtWidgets/QCompleter> +#include <QtGui/QDesktopServices> +#include <QtGui/QFont> +#include <QtGui/QFontMetrics> +#include <QtWidgets/QHeaderView> +#include <QtGui/QKeyEvent> +#include <QtCore/QSortFilterProxyModel> +#include <QtNetwork/QNetworkCookie> + +#include <QWebSettings> + +#include <QtCore/QDebug> + +static const unsigned int JAR_VERSION = 23; + +QT_BEGIN_NAMESPACE +QDataStream &operator<<(QDataStream &stream, const QList<QNetworkCookie> &list) +{ + stream << JAR_VERSION; + stream << quint32(list.size()); + for (int i = 0; i < list.size(); ++i) + stream << list.at(i).toRawForm(); + return stream; +} + +QDataStream &operator>>(QDataStream &stream, QList<QNetworkCookie> &list) +{ + list.clear(); + + quint32 version; + stream >> version; + + if (version != JAR_VERSION) + return stream; + + quint32 count; + stream >> count; + for (quint32 i = 0; i < count; ++i) + { + QByteArray value; + stream >> value; + QList<QNetworkCookie> newCookies = QNetworkCookie::parseCookies(value); + if (newCookies.count() == 0 && value.length() != 0) { + qWarning() << "CookieJar: Unable to parse saved cookie:" << value; + } + for (int j = 0; j < newCookies.count(); ++j) + list.append(newCookies.at(j)); + if (stream.atEnd()) + break; + } + return stream; +} +QT_END_NAMESPACE + +CookieJar::CookieJar(QObject *parent) + : QNetworkCookieJar(parent) + , m_loaded(false) + , m_saveTimer(new AutoSaver(this)) + , m_acceptCookies(AcceptOnlyFromSitesNavigatedTo) +{ +} + +CookieJar::~CookieJar() +{ + if (m_keepCookies == KeepUntilExit) + clear(); + m_saveTimer->saveIfNeccessary(); +} + +void CookieJar::clear() +{ + setAllCookies(QList<QNetworkCookie>()); + m_saveTimer->changeOccurred(); + emit cookiesChanged(); +} + +void CookieJar::load() +{ + if (m_loaded) + return; + // load cookies and exceptions + qRegisterMetaTypeStreamOperators<QList<QNetworkCookie> >("QList<QNetworkCookie>"); + QSettings cookieSettings(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/cookies.ini"), QSettings::IniFormat); + setAllCookies(qvariant_cast<QList<QNetworkCookie> >(cookieSettings.value(QLatin1String("cookies")))); + cookieSettings.beginGroup(QLatin1String("Exceptions")); + m_exceptions_block = cookieSettings.value(QLatin1String("block")).toStringList(); + m_exceptions_allow = cookieSettings.value(QLatin1String("allow")).toStringList(); + m_exceptions_allowForSession = cookieSettings.value(QLatin1String("allowForSession")).toStringList(); + qSort(m_exceptions_block.begin(), m_exceptions_block.end()); + qSort(m_exceptions_allow.begin(), m_exceptions_allow.end()); + qSort(m_exceptions_allowForSession.begin(), m_exceptions_allowForSession.end()); + + loadSettings(); +} + +void CookieJar::loadSettings() +{ + QSettings settings; + settings.beginGroup(QLatin1String("cookies")); + QByteArray value = settings.value(QLatin1String("acceptCookies"), + QLatin1String("AcceptOnlyFromSitesNavigatedTo")).toByteArray(); + QMetaEnum acceptPolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("AcceptPolicy")); + m_acceptCookies = acceptPolicyEnum.keyToValue(value) == -1 ? + AcceptOnlyFromSitesNavigatedTo : + static_cast<AcceptPolicy>(acceptPolicyEnum.keyToValue(value)); + + value = settings.value(QLatin1String("keepCookiesUntil"), QLatin1String("KeepUntilExpire")).toByteArray(); + QMetaEnum keepPolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("KeepPolicy")); + m_keepCookies = keepPolicyEnum.keyToValue(value) == -1 ? + KeepUntilExpire : + static_cast<KeepPolicy>(keepPolicyEnum.keyToValue(value)); + + if (m_keepCookies == KeepUntilExit) + setAllCookies(QList<QNetworkCookie>()); + + m_loaded = true; + emit cookiesChanged(); +} + +void CookieJar::save() +{ + if (!m_loaded) + return; + purgeOldCookies(); + QString directory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (directory.isEmpty()) + directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName(); + if (!QFile::exists(directory)) { + QDir dir; + dir.mkpath(directory); + } + QSettings cookieSettings(directory + QLatin1String("/cookies.ini"), QSettings::IniFormat); + QList<QNetworkCookie> cookies = allCookies(); + for (int i = cookies.count() - 1; i >= 0; --i) { + if (cookies.at(i).isSessionCookie()) + cookies.removeAt(i); + } + cookieSettings.setValue(QLatin1String("cookies"), QVariant::fromValue<QList<QNetworkCookie> >(cookies)); + cookieSettings.beginGroup(QLatin1String("Exceptions")); + cookieSettings.setValue(QLatin1String("block"), m_exceptions_block); + cookieSettings.setValue(QLatin1String("allow"), m_exceptions_allow); + cookieSettings.setValue(QLatin1String("allowForSession"), m_exceptions_allowForSession); + + // save cookie settings + QSettings settings; + settings.beginGroup(QLatin1String("cookies")); + QMetaEnum acceptPolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("AcceptPolicy")); + settings.setValue(QLatin1String("acceptCookies"), QLatin1String(acceptPolicyEnum.valueToKey(m_acceptCookies))); + + QMetaEnum keepPolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("KeepPolicy")); + settings.setValue(QLatin1String("keepCookiesUntil"), QLatin1String(keepPolicyEnum.valueToKey(m_keepCookies))); +} + +void CookieJar::purgeOldCookies() +{ + QList<QNetworkCookie> cookies = allCookies(); + if (cookies.isEmpty()) + return; + int oldCount = cookies.count(); + QDateTime now = QDateTime::currentDateTime(); + for (int i = cookies.count() - 1; i >= 0; --i) { + if (!cookies.at(i).isSessionCookie() && cookies.at(i).expirationDate() < now) + cookies.removeAt(i); + } + if (oldCount == cookies.count()) + return; + setAllCookies(cookies); + emit cookiesChanged(); +} + +QList<QNetworkCookie> CookieJar::cookiesForUrl(const QUrl &url) const +{ + CookieJar *that = const_cast<CookieJar*>(this); + if (!m_loaded) + that->load(); + + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) { + QList<QNetworkCookie> noCookies; + return noCookies; + } + + return QNetworkCookieJar::cookiesForUrl(url); +} + +bool CookieJar::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl &url) +{ + if (!m_loaded) + load(); + + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) + return false; + + QString host = url.host(); + bool eBlock = qBinaryFind(m_exceptions_block.begin(), m_exceptions_block.end(), host) != m_exceptions_block.end(); + bool eAllow = qBinaryFind(m_exceptions_allow.begin(), m_exceptions_allow.end(), host) != m_exceptions_allow.end(); + bool eAllowSession = qBinaryFind(m_exceptions_allowForSession.begin(), m_exceptions_allowForSession.end(), host) != m_exceptions_allowForSession.end(); + + bool addedCookies = false; + // pass exceptions + bool acceptInitially = (m_acceptCookies != AcceptNever); + if ((acceptInitially && !eBlock) + || (!acceptInitially && (eAllow || eAllowSession))) { + // pass url domain == cookie domain + QDateTime soon = QDateTime::currentDateTime(); + soon = soon.addDays(90); + foreach (QNetworkCookie cookie, cookieList) { + QList<QNetworkCookie> lst; + if (m_keepCookies == KeepUntilTimeLimit + && !cookie.isSessionCookie() + && cookie.expirationDate() > soon) { + cookie.setExpirationDate(soon); + } + lst += cookie; + if (QNetworkCookieJar::setCookiesFromUrl(lst, url)) { + addedCookies = true; + } else { + // finally force it in if wanted + if (m_acceptCookies == AcceptAlways) { + QList<QNetworkCookie> cookies = allCookies(); + cookies += cookie; + setAllCookies(cookies); + addedCookies = true; + } +#if 0 + else + qWarning() << "setCookiesFromUrl failed" << url << cookieList.value(0).toRawForm(); +#endif + } + } + } + + if (addedCookies) { + m_saveTimer->changeOccurred(); + emit cookiesChanged(); + } + return addedCookies; +} + +CookieJar::AcceptPolicy CookieJar::acceptPolicy() const +{ + if (!m_loaded) + (const_cast<CookieJar*>(this))->load(); + return m_acceptCookies; +} + +void CookieJar::setAcceptPolicy(AcceptPolicy policy) +{ + if (!m_loaded) + load(); + if (policy == m_acceptCookies) + return; + m_acceptCookies = policy; + m_saveTimer->changeOccurred(); +} + +CookieJar::KeepPolicy CookieJar::keepPolicy() const +{ + if (!m_loaded) + (const_cast<CookieJar*>(this))->load(); + return m_keepCookies; +} + +void CookieJar::setKeepPolicy(KeepPolicy policy) +{ + if (!m_loaded) + load(); + if (policy == m_keepCookies) + return; + m_keepCookies = policy; + m_saveTimer->changeOccurred(); +} + +QStringList CookieJar::blockedCookies() const +{ + if (!m_loaded) + (const_cast<CookieJar*>(this))->load(); + return m_exceptions_block; +} + +QStringList CookieJar::allowedCookies() const +{ + if (!m_loaded) + (const_cast<CookieJar*>(this))->load(); + return m_exceptions_allow; +} + +QStringList CookieJar::allowForSessionCookies() const +{ + if (!m_loaded) + (const_cast<CookieJar*>(this))->load(); + return m_exceptions_allowForSession; +} + +void CookieJar::setBlockedCookies(const QStringList &list) +{ + if (!m_loaded) + load(); + m_exceptions_block = list; + qSort(m_exceptions_block.begin(), m_exceptions_block.end()); + m_saveTimer->changeOccurred(); +} + +void CookieJar::setAllowedCookies(const QStringList &list) +{ + if (!m_loaded) + load(); + m_exceptions_allow = list; + qSort(m_exceptions_allow.begin(), m_exceptions_allow.end()); + m_saveTimer->changeOccurred(); +} + +void CookieJar::setAllowForSessionCookies(const QStringList &list) +{ + if (!m_loaded) + load(); + m_exceptions_allowForSession = list; + qSort(m_exceptions_allowForSession.begin(), m_exceptions_allowForSession.end()); + m_saveTimer->changeOccurred(); +} + +CookieModel::CookieModel(CookieJar *cookieJar, QObject *parent) + : QAbstractTableModel(parent) + , m_cookieJar(cookieJar) +{ + connect(m_cookieJar, SIGNAL(cookiesChanged()), this, SLOT(cookiesChanged())); + m_cookieJar->load(); +} + +QVariant CookieModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::SizeHintRole) { + QFont font; + font.setPointSize(10); + QFontMetrics fm(font); + int height = fm.height() + fm.height()/3; + int width = fm.width(headerData(section, orientation, Qt::DisplayRole).toString()); + return QSize(width, height); + } + + if (orientation == Qt::Horizontal) { + if (role != Qt::DisplayRole) + return QVariant(); + + switch (section) { + case 0: + return tr("Website"); + case 1: + return tr("Name"); + case 2: + return tr("Path"); + case 3: + return tr("Secure"); + case 4: + return tr("Expires"); + case 5: + return tr("Contents"); + default: + return QVariant(); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +QVariant CookieModel::data(const QModelIndex &index, int role) const +{ + QList<QNetworkCookie> lst; + if (m_cookieJar) + lst = m_cookieJar->allCookies(); + if (index.row() < 0 || index.row() >= lst.size()) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: { + QNetworkCookie cookie = lst.at(index.row()); + switch (index.column()) { + case 0: + return cookie.domain(); + case 1: + return cookie.name(); + case 2: + return cookie.path(); + case 3: + return cookie.isSecure(); + case 4: + return cookie.expirationDate(); + case 5: + return cookie.value(); + } + } + case Qt::FontRole:{ + QFont font; + font.setPointSize(10); + return font; + } + } + + return QVariant(); +} + +int CookieModel::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 6; +} + +int CookieModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid() || !m_cookieJar) ? 0 : m_cookieJar->allCookies().count(); +} + +bool CookieModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid() || !m_cookieJar) + return false; + int lastRow = row + count - 1; + beginRemoveRows(parent, row, lastRow); + QList<QNetworkCookie> lst = m_cookieJar->allCookies(); + for (int i = lastRow; i >= row; --i) { + lst.removeAt(i); + } + m_cookieJar->setAllCookies(lst); + endRemoveRows(); + return true; +} + +void CookieModel::cookiesChanged() +{ + beginResetModel(); + endResetModel(); +} + +CookiesDialog::CookiesDialog(CookieJar *cookieJar, QWidget *parent) : QDialog(parent) +{ + setupUi(this); + setWindowFlags(Qt::Sheet); + CookieModel *model = new CookieModel(cookieJar, this); + m_proxyModel = new QSortFilterProxyModel(this); + connect(search, SIGNAL(textChanged(QString)), + m_proxyModel, SLOT(setFilterFixedString(QString))); + connect(removeButton, SIGNAL(clicked()), cookiesTable, SLOT(removeOne())); + connect(removeAllButton, SIGNAL(clicked()), cookiesTable, SLOT(removeAll())); + m_proxyModel->setSourceModel(model); + cookiesTable->verticalHeader()->hide(); + cookiesTable->setSelectionBehavior(QAbstractItemView::SelectRows); + cookiesTable->setModel(m_proxyModel); + cookiesTable->setAlternatingRowColors(true); + cookiesTable->setTextElideMode(Qt::ElideMiddle); + cookiesTable->setShowGrid(false); + cookiesTable->setSortingEnabled(true); + QFont f = font(); + f.setPointSize(10); + QFontMetrics fm(f); + int height = fm.height() + fm.height()/3; + cookiesTable->verticalHeader()->setDefaultSectionSize(height); + cookiesTable->verticalHeader()->setMinimumSectionSize(-1); + for (int i = 0; i < model->columnCount(); ++i){ + int header = cookiesTable->horizontalHeader()->sectionSizeHint(i); + switch (i) { + case 0: + header = fm.width(QLatin1String("averagehost.domain.com")); + break; + case 1: + header = fm.width(QLatin1String("_session_id")); + break; + case 4: + header = fm.width(QDateTime::currentDateTime().toString(Qt::LocalDate)); + break; + } + int buffer = fm.width(QLatin1String("xx")); + header += buffer; + cookiesTable->horizontalHeader()->resizeSection(i, header); + } + cookiesTable->horizontalHeader()->setStretchLastSection(true); +} + + + +CookieExceptionsModel::CookieExceptionsModel(CookieJar *cookiejar, QObject *parent) + : QAbstractTableModel(parent) + , m_cookieJar(cookiejar) +{ + m_allowedCookies = m_cookieJar->allowedCookies(); + m_blockedCookies = m_cookieJar->blockedCookies(); + m_sessionCookies = m_cookieJar->allowForSessionCookies(); +} + +QVariant CookieExceptionsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::SizeHintRole) { + QFont font; + font.setPointSize(10); + QFontMetrics fm(font); + int height = fm.height() + fm.height()/3; + int width = fm.width(headerData(section, orientation, Qt::DisplayRole).toString()); + return QSize(width, height); + } + + if (orientation == Qt::Horizontal + && role == Qt::DisplayRole) { + switch (section) { + case 0: + return tr("Website"); + case 1: + return tr("Status"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +QVariant CookieExceptionsModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= rowCount()) + return QVariant(); + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: { + int row = index.row(); + if (row < m_allowedCookies.count()) { + switch (index.column()) { + case 0: + return m_allowedCookies.at(row); + case 1: + return tr("Allow"); + } + } + row = row - m_allowedCookies.count(); + if (row < m_blockedCookies.count()) { + switch (index.column()) { + case 0: + return m_blockedCookies.at(row); + case 1: + return tr("Block"); + } + } + row = row - m_blockedCookies.count(); + if (row < m_sessionCookies.count()) { + switch (index.column()) { + case 0: + return m_sessionCookies.at(row); + case 1: + return tr("Allow For Session"); + } + } + } + case Qt::FontRole:{ + QFont font; + font.setPointSize(10); + return font; + } + } + return QVariant(); +} + +int CookieExceptionsModel::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 2; +} + +int CookieExceptionsModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid() || !m_cookieJar) ? 0 : m_allowedCookies.count() + m_blockedCookies.count() + m_sessionCookies.count(); +} + +bool CookieExceptionsModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid() || !m_cookieJar) + return false; + + int lastRow = row + count - 1; + beginRemoveRows(parent, row, lastRow); + for (int i = lastRow; i >= row; --i) { + if (i < m_allowedCookies.count()) { + m_allowedCookies.removeAt(row); + continue; + } + i = i - m_allowedCookies.count(); + if (i < m_blockedCookies.count()) { + m_blockedCookies.removeAt(row); + continue; + } + i = i - m_blockedCookies.count(); + if (i < m_sessionCookies.count()) { + m_sessionCookies.removeAt(row); + continue; + } + } + m_cookieJar->setAllowedCookies(m_allowedCookies); + m_cookieJar->setBlockedCookies(m_blockedCookies); + m_cookieJar->setAllowForSessionCookies(m_sessionCookies); + endRemoveRows(); + return true; +} + +CookiesExceptionsDialog::CookiesExceptionsDialog(CookieJar *cookieJar, QWidget *parent) + : QDialog(parent) + , m_cookieJar(cookieJar) +{ + setupUi(this); + setWindowFlags(Qt::Sheet); + connect(removeButton, SIGNAL(clicked()), exceptionTable, SLOT(removeOne())); + connect(removeAllButton, SIGNAL(clicked()), exceptionTable, SLOT(removeAll())); + exceptionTable->verticalHeader()->hide(); + exceptionTable->setSelectionBehavior(QAbstractItemView::SelectRows); + exceptionTable->setAlternatingRowColors(true); + exceptionTable->setTextElideMode(Qt::ElideMiddle); + exceptionTable->setShowGrid(false); + exceptionTable->setSortingEnabled(true); + m_exceptionsModel = new CookieExceptionsModel(cookieJar, this); + m_proxyModel = new QSortFilterProxyModel(this); + m_proxyModel->setSourceModel(m_exceptionsModel); + connect(search, SIGNAL(textChanged(QString)), + m_proxyModel, SLOT(setFilterFixedString(QString))); + exceptionTable->setModel(m_proxyModel); + + CookieModel *cookieModel = new CookieModel(cookieJar, this); + domainLineEdit->setCompleter(new QCompleter(cookieModel, domainLineEdit)); + + connect(domainLineEdit, SIGNAL(textChanged(QString)), + this, SLOT(textChanged(QString))); + connect(blockButton, SIGNAL(clicked()), this, SLOT(block())); + connect(allowButton, SIGNAL(clicked()), this, SLOT(allow())); + connect(allowForSessionButton, SIGNAL(clicked()), this, SLOT(allowForSession())); + + QFont f = font(); + f.setPointSize(10); + QFontMetrics fm(f); + int height = fm.height() + fm.height()/3; + exceptionTable->verticalHeader()->setDefaultSectionSize(height); + exceptionTable->verticalHeader()->setMinimumSectionSize(-1); + for (int i = 0; i < m_exceptionsModel->columnCount(); ++i){ + int header = exceptionTable->horizontalHeader()->sectionSizeHint(i); + switch (i) { + case 0: + header = fm.width(QLatin1String("averagebiglonghost.domain.com")); + break; + case 1: + header = fm.width(QLatin1String("Allow For Session")); + break; + } + int buffer = fm.width(QLatin1String("xx")); + header += buffer; + exceptionTable->horizontalHeader()->resizeSection(i, header); + } +} + +void CookiesExceptionsDialog::textChanged(const QString &text) +{ + bool enabled = !text.isEmpty(); + blockButton->setEnabled(enabled); + allowButton->setEnabled(enabled); + allowForSessionButton->setEnabled(enabled); +} + +void CookiesExceptionsDialog::block() +{ + if (domainLineEdit->text().isEmpty()) + return; + m_exceptionsModel->m_blockedCookies.append(domainLineEdit->text()); + m_cookieJar->setBlockedCookies(m_exceptionsModel->m_blockedCookies); + m_exceptionsModel->beginResetModel(); + m_exceptionsModel->endResetModel(); +} + +void CookiesExceptionsDialog::allow() +{ + if (domainLineEdit->text().isEmpty()) + return; + m_exceptionsModel->m_allowedCookies.append(domainLineEdit->text()); + m_cookieJar->setAllowedCookies(m_exceptionsModel->m_allowedCookies); + m_exceptionsModel->beginResetModel(); + m_exceptionsModel->endResetModel(); +} + +void CookiesExceptionsDialog::allowForSession() +{ + if (domainLineEdit->text().isEmpty()) + return; + m_exceptionsModel->m_sessionCookies.append(domainLineEdit->text()); + m_cookieJar->setAllowForSessionCookies(m_exceptionsModel->m_sessionCookies); + m_exceptionsModel->beginResetModel(); + m_exceptionsModel->endResetModel(); +} diff --git a/examples/widgets/browser/cookiejar.h b/examples/widgets/browser/cookiejar.h new file mode 100644 index 000000000..1cc7800e1 --- /dev/null +++ b/examples/widgets/browser/cookiejar.h @@ -0,0 +1,203 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef COOKIEJAR_H +#define COOKIEJAR_H + +#include <QtNetwork/QNetworkCookieJar> + +#include <QtCore/QAbstractItemModel> +#include <QtCore/QStringList> + +#include <QtWidgets/QDialog> +#include <QtWidgets/QTableView> + +QT_BEGIN_NAMESPACE +class QSortFilterProxyModel; +class QKeyEvent; +QT_END_NAMESPACE + +class AutoSaver; + +class CookieJar : public QNetworkCookieJar +{ + friend class CookieModel; + Q_OBJECT + Q_PROPERTY(AcceptPolicy acceptPolicy READ acceptPolicy WRITE setAcceptPolicy) + Q_PROPERTY(KeepPolicy keepPolicy READ keepPolicy WRITE setKeepPolicy) + Q_PROPERTY(QStringList blockedCookies READ blockedCookies WRITE setBlockedCookies) + Q_PROPERTY(QStringList allowedCookies READ allowedCookies WRITE setAllowedCookies) + Q_PROPERTY(QStringList allowForSessionCookies READ allowForSessionCookies WRITE setAllowForSessionCookies) + Q_ENUMS(KeepPolicy) + Q_ENUMS(AcceptPolicy) + +signals: + void cookiesChanged(); + +public: + enum AcceptPolicy { + AcceptAlways, + AcceptNever, + AcceptOnlyFromSitesNavigatedTo + }; + + enum KeepPolicy { + KeepUntilExpire, + KeepUntilExit, + KeepUntilTimeLimit + }; + + CookieJar(QObject *parent = 0); + ~CookieJar(); + + QList<QNetworkCookie> cookiesForUrl(const QUrl &url) const; + bool setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl &url); + + AcceptPolicy acceptPolicy() const; + void setAcceptPolicy(AcceptPolicy policy); + + KeepPolicy keepPolicy() const; + void setKeepPolicy(KeepPolicy policy); + + QStringList blockedCookies() const; + QStringList allowedCookies() const; + QStringList allowForSessionCookies() const; + + void setBlockedCookies(const QStringList &list); + void setAllowedCookies(const QStringList &list); + void setAllowForSessionCookies(const QStringList &list); + +public slots: + void clear(); + void loadSettings(); + +private slots: + void save(); + +private: + void purgeOldCookies(); + void load(); + bool m_loaded; + AutoSaver *m_saveTimer; + + AcceptPolicy m_acceptCookies; + KeepPolicy m_keepCookies; + + QStringList m_exceptions_block; + QStringList m_exceptions_allow; + QStringList m_exceptions_allowForSession; +}; + +class CookieModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + CookieModel(CookieJar *jar, QObject *parent = 0); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + +private slots: + void cookiesChanged(); + +private: + CookieJar *m_cookieJar; +}; + +#include "ui_cookies.h" +#include "ui_cookiesexceptions.h" + +class CookiesDialog : public QDialog, public Ui_CookiesDialog +{ + Q_OBJECT + +public: + CookiesDialog(CookieJar *cookieJar, QWidget *parent = 0); + +private: + QSortFilterProxyModel *m_proxyModel; +}; + +class CookieExceptionsModel : public QAbstractTableModel +{ + Q_OBJECT + friend class CookiesExceptionsDialog; + +public: + CookieExceptionsModel(CookieJar *cookieJar, QObject *parent = 0); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + +private: + CookieJar *m_cookieJar; + + // Domains we allow, Domains we block, Domains we allow for this session + QStringList m_allowedCookies; + QStringList m_blockedCookies; + QStringList m_sessionCookies; +}; + +class CookiesExceptionsDialog : public QDialog, public Ui_CookiesExceptionsDialog +{ + Q_OBJECT + +public: + CookiesExceptionsDialog(CookieJar *cookieJar, QWidget *parent = 0); + +private slots: + void block(); + void allow(); + void allowForSession(); + void textChanged(const QString &text); + +private: + CookieExceptionsModel *m_exceptionsModel; + QSortFilterProxyModel *m_proxyModel; + CookieJar *m_cookieJar; +}; + +#endif // COOKIEJAR_H diff --git a/examples/widgets/browser/cookies.ui b/examples/widgets/browser/cookies.ui new file mode 100644 index 000000000..c4bccc548 --- /dev/null +++ b/examples/widgets/browser/cookies.ui @@ -0,0 +1,106 @@ +<ui version="4.0" > + <class>CookiesDialog</class> + <widget class="QDialog" name="CookiesDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>550</width> + <height>370</height> + </rect> + </property> + <property name="windowTitle" > + <string>Cookies</string> + </property> + <layout class="QGridLayout" > + <item row="0" column="0" > + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>252</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1" > + <widget class="SearchLineEdit" name="search" /> + </item> + <item row="1" column="0" colspan="2" > + <widget class="EditTableView" name="cookiesTable" /> + </item> + <item row="2" column="0" colspan="2" > + <layout class="QHBoxLayout" > + <item> + <widget class="QPushButton" name="removeButton" > + <property name="text" > + <string>&Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton" > + <property name="text" > + <string>Remove &All Cookies</string> + </property> + </widget> + </item> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="standardButtons" > + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SearchLineEdit</class> + <extends>QLineEdit</extends> + <header>searchlineedit.h</header> + </customwidget> + <customwidget> + <class>EditTableView</class> + <extends>QTableView</extends> + <header>edittableview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookiesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>472</x> + <y>329</y> + </hint> + <hint type="destinationlabel" > + <x>461</x> + <y>356</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/cookiesexceptions.ui b/examples/widgets/browser/cookiesexceptions.ui new file mode 100644 index 000000000..3d9ef6241 --- /dev/null +++ b/examples/widgets/browser/cookiesexceptions.ui @@ -0,0 +1,184 @@ +<ui version="4.0" > + <class>CookiesExceptionsDialog</class> + <widget class="QDialog" name="CookiesExceptionsDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>466</width> + <height>446</height> + </rect> + </property> + <property name="windowTitle" > + <string>Cookie Exceptions</string> + </property> + <layout class="QVBoxLayout" > + <item> + <widget class="QGroupBox" name="newExceptionGroupBox" > + <property name="title" > + <string>New Exception</string> + </property> + <layout class="QGridLayout" > + <item row="0" column="0" > + <layout class="QHBoxLayout" > + <item> + <widget class="QLabel" name="label" > + <property name="text" > + <string>Domain:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="domainLineEdit" /> + </item> + </layout> + </item> + <item row="1" column="0" > + <layout class="QHBoxLayout" > + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>81</width> + <height>25</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="blockButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>Block</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="allowForSessionButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>Allow For Session</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="allowButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>Allow</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="ExceptionsGroupBox" > + <property name="title" > + <string>Exceptions</string> + </property> + <layout class="QGridLayout" > + <item row="0" column="0" colspan="3" > + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>252</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="3" > + <widget class="SearchLineEdit" name="search" /> + </item> + <item row="1" column="0" colspan="4" > + <widget class="EditTableView" name="exceptionTable" /> + </item> + <item row="2" column="0" > + <widget class="QPushButton" name="removeButton" > + <property name="text" > + <string>&Remove</string> + </property> + </widget> + </item> + <item row="2" column="1" > + <widget class="QPushButton" name="removeAllButton" > + <property name="text" > + <string>Remove &All</string> + </property> + </widget> + </item> + <item row="2" column="2" colspan="2" > + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SearchLineEdit</class> + <extends>QLineEdit</extends> + <header>searchlineedit.h</header> + </customwidget> + <customwidget> + <class>EditTableView</class> + <extends>QTableView</extends> + <header>edittableview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CookiesExceptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>381</x> + <y>428</y> + </hint> + <hint type="destinationlabel" > + <x>336</x> + <y>443</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/data/addtab.png b/examples/widgets/browser/data/addtab.png new file mode 100644 index 0000000000000000000000000000000000000000..20928fb402aab20c838fca4dae2e9c1db2c54373 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf_5hy{*Z=?jtE;Q)=;&BlTH4v$ z+1uL#*}A&A4h{~Ej*i;e+Q!Dlc6N3yF0McT6t}Xn0*ZThco-QOnVXxtySsaOdg|-z z12sB1IeB<^dU$%8nVETcc?ARnL`Fse&G+;31F8uM3i9#s@%8luni&`vsHUa{RO9dO z4>ZKW!otSJ251Y=5Qx1lE-oP<A>m=+Kue>eqhn%XQc_Y<Q&Y3DvJ4Fk!^6XQdhf^r zy&zW-<QL3f0s`qk&;SAlfZ!?+yaj=eAETt-{s5|6@9E+gA|aW3@TFK|fC$@#^ExwC zgYOpaYUU6XJoVdNeDziTYUckYY?H5M8D%WE7CT3;u*On%*NQKDLfE`mjlNd>$h0zd zv@G^}^vs0uM)=2?=VyNY)?8=S@N!}tt6}iRMUO=eSShUw+`_gYuqoRqm*I>{!9DpI z{J)D8(iqy@T&)uQN|u}vaLAUd^Dy$>)qYy6?pNK6lbfbQzW5%vF=nwme{Kx-)^8d6 gZl60Y`{y%j_;1m)-KBXCflg=eboFyt=akR{0MYZR8~^|S literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/data/browser.svg b/examples/widgets/browser/data/browser.svg new file mode 100644 index 000000000..8795187b1 --- /dev/null +++ b/examples/widgets/browser/data/browser.svg @@ -0,0 +1,411 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48px" + height="48px" + id="svg2160" + sodipodi:version="0.32" + inkscape:version="0.46" + inkscape:export-filename="c:\icons\qtbrowser48.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + sodipodi:docbase="C:\icons" + sodipodi:docname="browser.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> + <defs + id="defs2162"><linearGradient + id="linearGradient3808"> + <stop + id="stop3810" + offset="0" + style="stop-color:#000000;stop-opacity:0.54263568;" /> + <stop + id="stop3812" + offset="1" + style="stop-color:#000000;stop-opacity:0;" /> +</linearGradient> +<inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 24 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="48 : 24 : 1" + inkscape:persp3d-origin="24 : 16 : 1" + id="perspective63" /> +<linearGradient + id="linearGradient3326"> + <stop + style="stop-color:#000000;stop-opacity:0.3137255;" + offset="0" + id="stop3328" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop3330" /> +</linearGradient> +<linearGradient + id="linearGradient3318"> + <stop + style="stop-color:#000000;stop-opacity:0.3137255;" + offset="0" + id="stop3320" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop3322" /> +</linearGradient> +<linearGradient + id="linearGradient3302"> + <stop + style="stop-color:#000000;stop-opacity:0.3137255;" + offset="0" + id="stop3304" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop3306" /> +</linearGradient> +<linearGradient + id="linearGradient3267"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop3269" /> + <stop + id="stop3275" + offset="0.79661018" + style="stop-color:#000000;stop-opacity:1;" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop3271" /> +</linearGradient> +<linearGradient + id="linearGradient3745"> + <stop + style="stop-color:#ffffff;stop-opacity:0.19587629;" + offset="0" + id="stop3747" /> + <stop + style="stop-color:#7cb2ff;stop-opacity:0.07216495;" + offset="1" + id="stop3749" /> +</linearGradient> +<linearGradient + inkscape:collect="always" + id="linearGradient3561"> + <stop + style="stop-color:#b1d0ff;stop-opacity:1;" + offset="0" + id="stop3563" /> + <stop + style="stop-color:#b1d0ff;stop-opacity:0;" + offset="1" + id="stop3565" /> +</linearGradient> +<linearGradient + id="linearGradient3181"> + <stop + style="stop-color:#4f7a33;stop-opacity:1;" + offset="0" + id="stop3183" /> + <stop + style="stop-color:#204712;stop-opacity:1;" + offset="1" + id="stop3185" /> +</linearGradient> +<linearGradient + id="linearGradient3143"> + <stop + style="stop-color:#c1dbff;stop-opacity:1;" + offset="0" + id="stop3145" /> + <stop + style="stop-color:#004e92;stop-opacity:1;" + offset="1" + id="stop3147" /> +</linearGradient> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3143" + id="radialGradient3149" + cx="9.1428566" + cy="15.142858" + fx="9.1428566" + fy="15.142858" + r="20.121096" + gradientUnits="userSpaceOnUse" /> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3181" + id="radialGradient3187" + cx="10.739879" + cy="18.250999" + fx="10.739879" + fy="18.250999" + r="7.4191086" + gradientTransform="matrix(1.0504709,0,0,1.5077925,-0.3797113,-9.2677171)" + gradientUnits="userSpaceOnUse" /> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3181" + id="radialGradient3195" + cx="14.947268" + cy="35.920116" + fx="14.947268" + fy="35.920116" + r="6.0472684" + gradientTransform="matrix(1,0,0,0.7248478,0,9.8834985)" + gradientUnits="userSpaceOnUse" /> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3181" + id="radialGradient3203" + cx="34.227203" + cy="24.681196" + fx="34.227203" + fy="24.681196" + r="6.7517419" + gradientTransform="matrix(0.9941509,-0.1079997,0.2962199,2.7267411,-7.1108629,-38.921508)" + gradientUnits="userSpaceOnUse" /> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3561" + id="radialGradient3567" + cx="22.714285" + cy="23.571428" + fx="22.714285" + fy="23.571428" + r="19.828572" + gradientUnits="userSpaceOnUse" /> +<linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3745" + id="linearGradient3751" + x1="0.84126461" + y1="13.678415" + x2="31.397495" + y2="13.678415" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.8791332,0.7829527,-0.6285195,1.0951445,14.147627,-10.49311)" /> +<filter + inkscape:collect="always" + id="filter4176"> + <feGaussianBlur + inkscape:collect="always" + stdDeviation="0.27747502" + id="feGaussianBlur4178" /> +</filter> +<radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3267" + id="radialGradient3273" + cx="22.714285" + cy="23.571428" + fx="22.714285" + fy="23.571428" + r="19.428572" + gradientUnits="userSpaceOnUse" /> +<inkscape:perspective + id="perspective136" + inkscape:persp3d-origin="138.6795 : 92.479329 : 1" + inkscape:vp_z="277.35901 : 138.71899 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 138.71899 : 1" + sodipodi:type="inkscape:persp3d" /> + + + + + + + + + + +<linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3808" + id="linearGradient3806" + x1="32.829472" + y1="32.055603" + x2="34.522324" + y2="-1.0290829" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.8832227,0,0,1,-8.0103007,9.1923882)" /> +</defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="5.6568542" + inkscape:cx="30.924085" + inkscape:cy="24.59691" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:window-width="1299" + inkscape:window-height="883" + inkscape:window-x="373" + inkscape:window-y="89" + showguides="false" /> + <metadata + id="metadata2165"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Qt Browser</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Jens Bache-Wiig</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).</dc:title> + </cc:Agent> + </dc:rights> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer"> + <path + sodipodi:type="arc" + style="opacity:0.78108437;fill:url(#radialGradient3273);fill-opacity:1;stroke:none;stroke-width:0.80000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path3407" + sodipodi:cx="22.714285" + sodipodi:cy="23.571428" + sodipodi:rx="19.428572" + sodipodi:ry="19.428572" + d="M 42.142857,23.571428 A 19.428572,19.428572 0 1 1 3.2857132,23.571428 A 19.428572,19.428572 0 1 1 42.142857,23.571428 z" + transform="matrix(1.0818892,0,0,1.0409446,-2.4313375,0.4303723)" /> + <path + sodipodi:type="arc" + style="fill:url(#radialGradient3149);fill-opacity:1;stroke:none;stroke-width:0.80000000000000004;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path2170" + sodipodi:cx="22.714285" + sodipodi:cy="23.571428" + sodipodi:rx="19.428572" + sodipodi:ry="19.428572" + d="M 42.142857 23.571428 A 19.428572 19.428572 0 1 1 3.2857132,23.571428 A 19.428572 19.428572 0 1 1 42.142857 23.571428 z" /> + <path + d="M 26.602136,8.2160843 C 26.322653,8.1637524 26.048884,8.1512446 25.78375,8.1745351 L 25.783243,8.1743913 C 25.783243,8.1743913 23.973525,8.3138471 23.891496,8.3211793 C 22.239361,8.4705552 20.985434,10.008307 20.985434,12.131916 L 20.985434,37.174579 L 22.83515,39.126673 L 41.425135,33.998394 C 42.704203,33.746799 43.714709,33.629384 43.714709,31.78483 L 43.714709,11.392226 L 26.602136,8.2160843 z" + id="path2998" + style="fill:url(#linearGradient3806);fill-opacity:1" + sodipodi:nodetypes="cccsccccccc" /> + <path + style="fill:url(#radialGradient3203);fill-opacity:1;fill-rule:evenodd;stroke:#1d3215;stroke-width:0.51392877000000003;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 37.535517,11.721122 C 32.782916,8.7478602 30.602351,6.3542385 32.09957,13.4346 C 32.320572,14.27055 33.291276,13.739232 33.291276,14.862228 C 33.291276,16.155819 32.607502,17.380765 31.797574,18.146663 C 30.959323,18.939344 31.011357,20.258984 31.797574,21.002459 C 33.06234,22.198469 33.942515,22.715936 35.572536,22.715936 C 36.6448,22.715936 37.003629,23.274262 37.23352,24.143834 C 37.362263,24.630808 38.410486,25.085663 38.894503,25.428942 C 38.938905,25.460433 38.139512,26.551348 38.139512,27.999158 C 38.139512,29.113512 38.405167,29.358325 38.743505,29.998215 C 38.949111,30.387072 36.418877,30.283794 36.025532,30.283794 C 35.005751,30.283794 34.181701,30.712163 33.15656,30.712163 C 32.264543,30.712163 31.099578,30.3566 31.344578,31.283323 C 31.763542,32.868074 32.552566,33.932342 32.552566,35.709806 C 32.552566,36.862272 31.047367,37.598377 30.287588,38.137232 C 29.30273,38.835721 29.133207,39.307154 28.475606,40.136289 C 28.132145,40.569341 26.990548,41.409612 28.475606,40.707448 C 29.476144,40.234375 31.192063,39.423774 32.09957,38.565601 C 33.257846,37.470293 34.527421,37.269266 35.723534,36.138176 C 36.659137,35.253436 37.512933,34.691155 38.29051,33.710749 C 39.024031,32.785889 39.498498,31.90347 39.498498,30.712163 C 39.498498,29.682482 39.308098,28.750366 39.951493,28.141948 C 40.902684,24.235856 42.225874,19.789742 39.751646,16.005086 C 38.569376,15.014407 37.717516,13.109859 37.535517,11.721122 z " + id="path3151" + sodipodi:nodetypes="ccsssssssssssssssssssccc" /> + <path + style="fill:url(#radialGradient3187);fill-opacity:1;fill-rule:evenodd;stroke:#063a0a;stroke-width:0.51231807;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 14.777083,7.8630009 C 14.047432,8.4403746 12.751987,10.898939 13.27641,12.146301 C 13.709874,13.177316 14.920827,13.613143 15.827553,13.859622 C 16.568703,14.061091 17.049015,14.457271 17.478293,15.001835 C 17.832696,15.451415 17.971105,16.346745 18.078563,16.857932 C 18.298637,17.904845 18.947911,17.058563 17.62836,18.000145 C 17.234352,18.281296 14.875696,18.000145 14.476948,18.000145 C 11.976825,18.384083 14.297504,19.464893 14.92715,20.712903 C 15.204987,21.770261 15.377352,22.405336 15.377352,23.711213 C 15.377352,24.875672 15.377352,24.78389 15.377352,25.99564 C 15.377352,27.194757 15.044241,27.28063 13.876679,27.28063 C 13.023055,27.28063 12.647321,26.423969 11.625669,26.423969 C 10.400599,26.423969 11.303539,27.667106 11.475602,27.994513 C 12.006402,29.004538 11.662121,29.599737 10.875334,28.851174 C 9.855722,27.881096 8.8280305,26.760556 8.0240557,25.99564 C 2.8789379,25.807372 4.5677903,23.466499 3.9722395,18.999582 C 5.041259,16.526382 4.7558935,17.248897 7.2737194,12.574632 C 10.149914,9.5491592 13.589212,5.9532919 14.777083,7.8630009 z" + id="path3159" + sodipodi:nodetypes="csssssccsssssscccc" /> + <path + style="fill:url(#radialGradient3195);fill-opacity:1;fill-rule:evenodd;stroke:#163c0c;stroke-width:0.59999999999999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 10.265966,34.571429 C 9.245427,35.081699 8.6225774,36.042538 9.980252,36.857143 C 10.637564,37.25153 11.478587,37.606311 12.265966,38 C 13.258976,38.496505 14.481138,39.018522 15.408823,39.714286 C 16.227572,40.328348 15.587589,39.928184 16.123109,38.857143 C 16.827927,37.447507 18.14516,38.79674 18.837395,39.142857 C 20.044787,39.746554 20.46001,38.652394 20.694537,37.714286 C 20.459863,35.791335 18.579948,34.625723 17.123109,33.285715 C 16.704922,32.588736 15.507117,31.689713 14.837395,31.857143 C 13.49505,33.304042 12.350312,33.960279 10.265966,34.571429 z " + id="path3161" + sodipodi:nodetypes="cssssscccc" /> + <path + sodipodi:type="arc" + style="fill:none;fill-opacity:1;stroke:url(#radialGradient3567);stroke-width:0.80000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.6502732" + id="path3557" + sodipodi:cx="22.714285" + sodipodi:cy="23.571428" + sodipodi:rx="19.428572" + sodipodi:ry="19.428572" + d="M 42.142857 23.571428 A 19.428572 19.428572 0 1 1 3.2857132,23.571428 A 19.428572 19.428572 0 1 1 42.142857 23.571428 z" + transform="matrix(0.95317,0,0,0.95317,0.9922816,1.1752786)" /> + <path + style="fill:url(#linearGradient3751);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 39.916926,27.786316 C 44.588637,26.790847 38.225604,13.201712 32.946381,8.5000566 C 18.135275,-0.40265528 10.844456,5.6490056 3.6645529,16.333771 C 5.7478288,18.189127 14.704728,33.158645 39.916926,27.786316 z" + id="path3578" + sodipodi:nodetypes="cccs" /> + <path + d="M 45.902562,20.610592 C 46.007701,20.610592 46.120332,20.603354 46.240455,20.590275 L 45.609873,20.590275 C 45.697743,20.603608 45.798946,20.610592 45.902562,20.610592 z" + id="path3012" + style="fill:#0a6333" /> + <path + sodipodi:type="arc" + style="fill:none;fill-opacity:1;stroke:#273e5e;stroke-width:0.80000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path3818" + sodipodi:cx="22.714285" + sodipodi:cy="23.571428" + sodipodi:rx="19.428572" + sodipodi:ry="19.428572" + d="M 42.142857,23.571428 A 19.428572,19.428572 0 1 1 3.2857132,23.571428 A 19.428572,19.428572 0 1 1 42.142857,23.571428 z" + transform="matrix(0.9754581,0,0,0.9754581,0.3821951,0.7002631)" /> + <g + transform="matrix(0.1269799,0,0,0.1269799,23.283534,9.5774104)" + id="g236"> + <path + style="fill:#024c1c" + id="path238" + d="M 44.233,0.368 C 42.032,0.004 39.876,-0.083 37.788,0.079 L 37.784,0.078 C 37.784,0.078 23.532,1.048 22.886,1.099 C 9.875,2.138 0,12.834 0,27.605 L 0,201.792 L 14.567,215.37 L 160.968,190.766 C 171.041,189.016 178.999,177.133 178.999,164.303 L 178.999,22.46 L 44.233,0.368 z" /> + + <path + style="fill:#66b036" + id="path240" + d="M 179,164.304 C 179,177.134 171.042,189.017 160.969,190.767 L 14.567,215.37 L 14.567,26.683 C 14.567,9.52 28.263,-2.264 44.231,0.368 L 179,22.462 L 179,164.304 z" /> + + <g + id="g242"> + <path + style="fill:#ffffff" + id="path244" + d="M 133.897,47.137 L 145.72,48.411 L 145.72,69.158 L 159.025,70.099 L 159.025,83.113 L 145.72,82.502 L 145.72,130.066 C 145.72,134.207 146.176,136.869 147.093,138.064 C 147.919,139.158 149.195,139.697 150.907,139.697 C 151.069,139.697 151.24,139.695 151.414,139.683 C 154.031,139.533 156.878,138.728 159.98,137.314 L 159.98,149.275 C 154.707,151.591 149.532,152.966 144.452,153.398 C 143.716,153.457 143.005,153.486 142.317,153.486 C 137.716,153.486 134.199,152.152 131.797,149.451 C 128.998,146.318 127.598,141.285 127.598,134.387 L 127.598,81.661 L 121.209,81.368 L 121.209,67.424 L 129,67.985 L 133.897,47.137 z" /> + + </g> + + <polygon + style="fill:#0a6333" + id="polygon246" + points="159.027,83.112 145.722,82.501 145.722,82.785 152.854,83.112 159.027,83.112 " /> + + <path + style="fill:#024c1c" + id="path248" + d="M 148.488,139.21 C 149.168,139.548 149.96,139.696 150.908,139.696 C 151.07,139.696 151.241,139.694 151.415,139.682 C 154.032,139.532 156.879,138.727 159.981,137.313 L 153.806,137.313 C 151.938,138.169 150.178,138.808 148.488,139.21 z" /> + + <path + style="fill:#024c1c" + id="path250" + d="M 133.897,47.137 L 127.723,47.137 L 122.93,67.549 L 129,67.985 L 133.897,47.137 z M 131.799,149.45 C 129,146.317 127.6,141.284 127.6,134.386 L 127.6,81.661 L 121.211,81.368 L 121.211,67.424 L 115.03,67.424 L 115.03,70.539 C 115.926,73.897 116.63,77.539 117.149,81.465 L 121.426,81.661 L 121.426,134.386 C 121.426,141.284 122.827,146.318 125.625,149.45 C 128.029,152.151 131.541,153.485 136.141,153.485 L 142.318,153.485 C 137.718,153.485 134.2,152.151 131.799,149.45 z" /> + + <path + style="fill:#0a6333" + id="path252" + d="M 102.954,170.419 C 103.782,170.419 104.669,170.362 105.615,170.259 L 100.649,170.259 C 101.341,170.364 102.138,170.419 102.954,170.419 z" /> + + <path + style="fill:#ffffff" + id="path254" + d="M 112.036,139.78 C 107.81,149.749 101.365,156.27 92.542,159.288 C 93.43,163.856 94.778,166.929 96.567,168.55 C 97.955,169.796 100.094,170.419 102.958,170.419 C 103.782,170.419 104.671,170.362 105.615,170.259 L 105.615,183.736 L 99.497,184.539 C 97.692,184.771 95.98,184.889 94.361,184.889 C 89.001,184.889 84.665,183.59 81.402,180.961 C 77.085,177.496 73.899,170.805 71.857,160.908 C 62.48,158.91 55.166,152.945 50.103,142.937 C 44.965,132.769 42.349,117.895 42.349,98.441 C 42.349,77.466 45.927,61.985 52.971,52.169 C 58.912,43.885 67.202,39.812 77.634,39.812 C 79.306,39.812 81.033,39.916 82.809,40.124 C 95.081,41.539 103.977,47.329 109.77,57.362 C 115.453,67.177 118.243,81.244 118.243,99.721 C 118.242,116.643 116.186,129.954 112.036,139.78 z M 93.582,135.933 C 95.996,129.724 97.189,117.54 97.189,99.37 C 97.189,83.054 96.007,71.837 93.608,65.682 C 91.21,59.496 87.622,56.153 82.808,55.731 C 82.441,55.7 82.075,55.681 81.724,55.681 C 77.264,55.681 73.84,58.283 71.447,63.508 C 68.863,69.201 67.555,81.003 67.555,98.866 C 67.555,116.129 68.826,128.379 71.388,135.569 C 73.804,142.419 77.423,145.813 82.174,145.813 C 82.384,145.813 82.593,145.805 82.809,145.79 C 87.566,145.489 91.148,142.202 93.582,135.933" /> + + <path + style="fill:#024c1c" + id="path256" + d="M 84.708,183.003 C 84.59,182.95 84.477,182.896 84.361,182.839 C 84.349,182.835 84.336,182.829 84.323,182.821 C 84.218,182.77 84.115,182.716 84.011,182.663 C 83.991,182.653 83.971,182.642 83.948,182.63 C 83.854,182.579 83.761,182.528 83.667,182.476 C 83.636,182.46 83.609,182.443 83.579,182.427 C 83.494,182.38 83.412,182.331 83.328,182.284 C 83.286,182.263 83.25,182.239 83.209,182.214 C 83.137,182.171 83.062,182.128 82.994,182.083 C 82.943,182.054 82.897,182.024 82.848,181.993 C 82.785,181.954 82.726,181.915 82.663,181.876 C 82.606,181.837 82.552,181.798 82.492,181.759 C 82.442,181.726 82.392,181.693 82.342,181.659 C 82.272,181.612 82.206,181.563 82.141,181.518 C 82.101,181.489 82.061,181.463 82.021,181.432 C 81.943,181.377 81.866,181.319 81.79,181.26 C 81.764,181.239 81.735,181.221 81.708,181.199 C 81.607,181.121 81.505,181.039 81.402,180.959 C 77.085,177.494 73.899,170.803 71.857,160.906 C 62.48,158.908 55.166,152.943 50.103,142.935 C 44.965,132.767 42.349,117.893 42.349,98.439 C 42.349,77.464 45.927,61.983 52.971,52.167 C 58.912,43.883 67.202,39.81 77.634,39.81 C 77.67,39.81 71.114,39.806 71.114,39.806 L 71.114,39.81 C 60.694,39.818 52.411,43.89 46.476,52.167 C 39.434,61.984 35.855,77.465 35.855,98.439 C 35.855,117.892 38.469,132.767 43.609,142.935 C 48.671,152.943 55.983,158.908 65.361,160.906 C 67.403,170.802 70.588,177.494 74.904,180.959 C 78.168,183.588 82.507,184.887 87.867,184.887 C 87.967,184.887 88.07,184.887 88.17,184.885 L 93.861,184.885 C 90.361,184.828 87.306,184.203 84.716,183.006 C 84.712,183.007 84.708,183.007 84.708,183.003 z M 87.113,65.681 C 89.511,71.837 90.69,83.054 90.69,99.369 C 90.69,117.539 89.502,129.723 87.083,135.932 C 85.142,140.942 82.439,144.047 79.013,145.248 C 79.999,145.621 81.058,145.81 82.173,145.81 C 82.383,145.81 82.592,145.802 82.808,145.787 C 87.567,145.488 91.149,142.201 93.582,135.932 C 95.996,129.723 97.189,117.539 97.189,99.369 C 97.189,83.053 96.007,71.836 93.608,65.681 C 91.21,59.495 87.622,56.152 82.808,55.73 C 82.441,55.699 82.075,55.68 81.724,55.68 C 80.601,55.68 79.549,55.845 78.556,56.173 L 78.556,56.175 L 78.556,56.175 C 82.254,57.322 85.104,60.5 87.113,65.681 z" /> + +</g> + </g> +</svg> diff --git a/examples/widgets/browser/data/closetab.png b/examples/widgets/browser/data/closetab.png new file mode 100644 index 0000000000000000000000000000000000000000..ab9d669eeebed9718363bd06ba84fdfd9e7cd6b5 GIT binary patch literal 516 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf)c~Im*Z=?jtE;Q)=;&BlTH4uz z0FbS#tLxz4;OOY6t*vcrY;0#|=i=((>goy<x3aPVihH<w0D+N_k-53KyE}-Zudff( z>*VC*;Rym}W@bPVXt<Y`m%qP%KtMoHP>_v{4bWI$Utd2zKcFE%#ZggFv9Yla9z1yT z=+WcHkDoq$`uzEGpmovF(ca$PK*OD#o#W!-l9G~KTwJoUva++Yi;Iif+uLW(oH={; z>^XDh+`D%V=rEugfYybCgoK8MIyyQW>XKdu^q_o6kY6x^2?(SEK?4XJ0D-GluU>l# z0v|tqT5?z%sC2)li(`m{WbVQ1Vh0^WST8Itzqt7R8J}4qF1|`02mjX#o&J=O|6qPh ztW^KfDn9$C2OYoKd^ON`yn53GBWDif%VMH6PulHvw?8kgNdITRzaVjwbgtfX-n%ny zGF%9px6S#suI0Djmkcuuyx+tsmFw%6F&c<|%X}`hbN6K3WemISa9w+1v4v0P9_xiG z^5I>frpsPC=QC;a^n{j}#2?Q{?cw^dLN8eNq0PlhsiN_wkryqmi|%ZjpS0V4_V*~& bKcCs&oRdi2E+elG^a_KgtDnm{r-UW|i^$5o literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/data/data.qrc b/examples/widgets/browser/data/data.qrc new file mode 100644 index 000000000..c7d0294c1 --- /dev/null +++ b/examples/widgets/browser/data/data.qrc @@ -0,0 +1,11 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>addtab.png</file> + <file>closetab.png</file> + <file>history.png</file> + <file>browser.svg</file> + <file>defaultbookmarks.xbel</file> + <file>loading.gif</file> + <file>defaulticon.png</file> +</qresource> +</RCC> diff --git a/examples/widgets/browser/data/defaultbookmarks.xbel b/examples/widgets/browser/data/defaultbookmarks.xbel new file mode 100644 index 000000000..7a95e36b3 --- /dev/null +++ b/examples/widgets/browser/data/defaultbookmarks.xbel @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE xbel> +<xbel version="1.0"> + <folder folded="yes"> + <title>Bookmarks Bar</title> + <bookmark href="http://qt-project.org/"> + <title>Qt Home Page</title> + </bookmark> + <bookmark href="http://webkit.org/"> + <title>WebKit.org</title> + </bookmark> + <bookmark href="http://qt-project.org/doc/"> + <title>Qt Documentation</title> + </bookmark> + <bookmark href="http://qt-project.org/quarterly/"> + <title>Qt Quarterly</title> + </bookmark> + <bookmark href="http://planet.qt-project.org/"> + <title>Qt Blog</title> + </bookmark> + <bookmark href="http://www.qtcentre.org/"> + <title>Qt Centre</title> + </bookmark> + <bookmark href="http://qt-apps.org/"> + <title>Qt-Apps.org</title> + </bookmark> + <bookmark href="http://qt-project.org/wiki/OnlineCommunities/"> + <title>Online Communities</title> + </bookmark> + <bookmark href="http://xkcd.com/"> + <title>xkcd</title> + </bookmark> + <bookmark href="http://twitter.com/qtproject"> + <title>Twitter</title> + </bookmark> + </folder> + <folder folded="yes"> + <title>Bookmarks Menu</title> + <bookmark href="http://reddit.com/"> + <title>reddit.com: what's new online!</title> + </bookmark> + </folder> +</xbel> diff --git a/examples/widgets/browser/data/defaulticon.png b/examples/widgets/browser/data/defaulticon.png new file mode 100644 index 0000000000000000000000000000000000000000..01a0920c93c67e94ee3c9b8317fd6dd1437e8e4d GIT binary patch literal 1473 zcmV;y1wQ(TP)<h;3K|Lk000e1NJLTq000&M000&U1^@s6#I$TX000GuNkl<ZIE{Uk zU5H$F8ODGA|M{Oe=giER&z+f_-OcW1lTBi1+>#iI23lH6uvH4ZNN=n~#N4zON-uJu zc;k&A(yQ8Bh$zJyTM<kUg(j_v3T-JQ{fOCRH*WSL+1=SQpJ!&y_wiz}Acnm6&+mO7 zczGU?rYR4a)0aLe4*kxv#|}@u>h$`vx0g5bnpASSS&5`B)`BGY&1Wm^U!=ePM(;s- z@gVRsudV*j?F@gsx!;kC(-Z9Wx@05>P3Op=IeN}Mj<d%o2-D`=)a7?BoqFY=;OAan zsjh5#*R?cV(xk^;yG^}dGLg?<DUHKNj$mX=%#4YQHCFH3VtH+YiRPqpV|DlB-mjn8 z`#<nEf4N!x@YCJRqtg@C`K2j-^42vL8+j&dlbNzf&d&4r7w2)@1SJ&hA?&*;o0}`# z-QL5CMY8PnCtI&S+X7Jk-LSTo++3QgTjx(Vv28`OWU)Az=j_o6jgrAVCuC*2!&ero zbjAmoI1vQ(8GLU**F~PIm6m}DfCM=IbLXcyzd!xu6E!>~`DCX@(Nr{wCXJH8Of}0F zW(%A<W}~!(jkZhA8`1N6bi;sBsY<m{#A)@*_3vEyE`YY%c%`(ywfTp$$7e)jDSr0W zUs=0zk9xsms%%os8c0oJyA#o@7}RWy>5@*A92mBZJg%SM`7X6`jpfzVr{DPV-W$@< zv<sE2E{tN9-@SX2et(3n38a8+$K!rC;MUfdQqCZV1Z7K-%}7qqW_V)JpiuzJF!58c z3nplKHWcyGY8d!0*|{8_t?w}QLxcb!#OI&Vc0(qL8T#XdCz}c-Ax_|47rY1->p-gG z4^lFkVq&63uRnM~%I>})Or2wgswgQ5<CHK?h~ktuN%4XhKZvN>2iHc)0XRv)apBf| z$Y?3Yr}Ic9al;6a@|bkTgEM(cF;U2I>B1QnnnlJz%s7nkqlA$c;En_C><$@)DWd?q z5P}Gz7)%9*j0j>uy=XBQ1(3uiw5i8l`oW>ex^lgUFpel@B{%Q1Fm(+>X(&yQrYU}q z;75|Iq2u}yLExha*zRkzI&D%kx}l9T93a9lRtWuLcQ`sbTRKG9(wI7CQO+B@{g-7- zC6NNs6#Ova!<7VgG-fak`FOpDoi&+kO!M!p7TLlymX%O1*%WkfU*&A$cHec+#!*7w z)6k^JmkuB1-0>#2*LT?1?egeck-3^hs~7P1f3@hiA%2vwy6bXA*Aco+Hd`bN6I%N{ zYQ@|?)b`%simkifT9_Y^p~x_RoB;*X<nc%5F_%JK{nbAi`{{uLDM*qOFNzs>5wioI zMr8&eB&$x3a2#USb63@{A6^Tzp}Tdjl|MYIfE-&L*y(|(L+H2pV68*mR2WJkg&;{& z{3yZoBKF>2=i=8Fh?1CYw?jr#Zezy2rgd+;>AiM!eY@>A&)2GDqz+1END+hyIQ=f0 z&X_0>_)*F@Oo&s7u4ky&dFrJC13%_|dx+ca^T^`t#mg^N-~T+_=DvO9dc9J4>cpuj z>}-*oZZTQXc<;tGgR#fh4+x`}Bu$Y*kWmWD$dI>8@`g^o?_*oWyTASZ%vb*>-GW$u zVRPsHnvikv<Wj<PC19ai$9lBM-F=T%->2_|oSHExWfgZ?5q(dPk&2cxAn=2ps_8F0 zFz6dE{;8VLqPI&0_2lf4CbOkE4%PECY>BBuO2TYeP%lbuY$SZN=JL_X4n{_QPi3PE zSHE}Md1y5qefgb>mWnS6lx7(=)k>L?okQ0%WMs%_?6KpF=?(p1v1q=0{YNL?d}uWZ zAuON<)KvM%ynXU(PwB<^Gs<o(=w_uNgcK?n^wZ(a#&Gxc&Gx5%{9Dl7xC`t8TR<;O b(-`1CE1{QKcOYYB00000NkvXXu0mjfy$tXg literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/data/history.png b/examples/widgets/browser/data/history.png new file mode 100644 index 0000000000000000000000000000000000000000..552a1cbd88459475f69668ebd438353d6303ea16 GIT binary patch literal 1527 zcmV<T1qk|yP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00009a7bBm000XU z000XU0RWnu7ytkO2XskIMF-Xb4G$zE*vbjU00006VoOIv0RI600RN!9r;`8x1%ydN zK~zYI#guDI(`OjPmu1OjjM<jB56g^;n^QxgS=2Gb$#g1+fTD3+dgWSZ3kZr6H&H=E z85f~UiU@KmAc#ZcP6t$O<x)^V5NHbnWIBZc)?T5U{rguMoY#HYNlx1I!}*=}d7r-T zyz_s`%VPBh7i_n-WVk!sPxYcaO7)~DQ#~lk6c&9n#f#b<vCHx&mgSm7|2demb=AB4 zJ=;XtT#xah_!zactqu43`!Upa7X!UL7?nvOYnR}9Laa9X=t0@fPCMP17GExStMC9D z^Cg*V#!zl}h_0`_4c%>GGzrU4Q+5q$sqy&Hg@)SlA~aN%psTqNy#%O$7p)HSqFnU1 zwOIMuaG&j8FU{~_j+ZCIpr^SBG*?etzEXgW?g2DQC(u9q5P?UI!2n=lY6=wult!hX zv#}2K`MJml;>f(Mzj--?{@t?o(!FUT#W814C=^4~Btc=R5G-yW+I!__l}};h?+HZw zdJZ#&S!i{7usK|mm6f5Xt_HOQx%eYJ1;KQOl)W21TlfNiZNG_{alp&an45+Cnl6-z zrKo6?<Hp@_w8$n+fWZe-Fc1L!%nZ~TE$PQkl#tZdpe|2<^pin4&OS@dbK!e7E?dBJ zvFp7W5ei#67lD!4C>QsjP;?7ose%}ff&fh7&-)789(V+!W@l$f6D;Vary*)+0M*3_ zrRU=j?d>dfu=rxJ3E*hCYD+T9S$*M1Aj0?su<<yKs9X_h+GVJ0=_AAE+|d0O2{4Xn z4l*=60ydY6it-X<9Y2hV2RyWn->=$l0vy`0E<ZDng(x-~oHLhkI=vPVX+lH_YQZmT zMs0@-!aH)bNXN+&7(<O38X7`)_$lysJn|GuwFZ~Mf{`8M2g(m-=_WuTbMF|j^F(+O z0%Hpilu!)+sC<O*OAwh^jrhwVoG-42ME(>oGi!jaw<nGs4aUumb|@w$P0+$LKJtTn zacrmgJtBGu3E(kyE3-IEuucg;|K%c>A*o;-Gdcy_2m$G^<Wii7OT#uR8_<|em?j#} z5Enf+s&T~xZr|-gV(eLD6IszzJEaK_L)$$m@N|Kly*>8scLIe%0n^nDbOsYlCKFUD zmHf@l&JNaAR;a41cmd$+=MQ%;791TNkr%{8G|gUV8c{scVKmRr9oI6F@G2R{Vw6^^ zg-WG*4nX?Zh{~@JYaSeclh)>=rW1tQT8IUQIQTs=j=U(He%drsqYTe%lwPklg-_2I z$Tgk&<EgYd6eRPIb0h#>HmkEu0Jj}0tuqh%XiAcJsIIOiR?ZJ<qUl_#*?FG^g8};K zr{Qz$H98%9IKB{xL@46NBj+FsHtRq4GXY$;EnjrWpCPYI<&!fnuPA%@n26TKaI(E& z9)M_WEJkB6P?eU9^Z?fpv!x%ceeT~J?6SclEAqs|tvV69ZneJ<ZftA}pKoKZTC0ch zi3&Y;I&rhE7P&E}o-j9=6`0!3$$#I91!-J*Pf1b&I*FI`hPWQrN{Zl3JZLJ7h75c# zHby>h#_uZPuDsb?2&*d5)!l`zwiZ<9q#@LHbLS@?zQ6UQFKQ_L+Z6&o*ZZaXbLj8s z0mIc5mF2}4Qz*z2NF)++eT{cBF*yZcWjQ$(d4C_SrX(VYZYy<J`_<_e+RxXO(}&#z z2beuq<3ct4w>vN}D2Jr270pc|<m6?;$;BDXjUu!*H$X0zqE1+aEMBB4&~9VvzD>)m zUK>8ANcVLMFHyH&&pYJ#s30V0R(4Z@;Q<*&hlemYIEa>pdejk%P>>j_kEiZ>nCfQD zU$g9Evo{9+TW9I!i+^+6ax#NUlV-80lZ4fAbO%iw&EavZv#lc7dOdHm*{AE?s`VSR d<PCnM{sD77$9c@rLfZfU002ovPDHLkV1fxwzES`H literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/data/loading.gif b/examples/widgets/browser/data/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..c1545eb0eddb8d69e9d0cd70f30dd19c872829ec GIT binary patch literal 847 zcmZ?wbhEHb6krfw_`<;O|Nnmn2Zx_Oe`aQ8E?Tt6#l<BiCg#C|2YdJK4G0LBF=Iwg zPtU7YuQqPnc;(8K($dm~h6V;wfZ~5{Ki808XU70nBRvCVMxb8Be^Smxsfi`2DGKG8 zB^e5dS&0=n`H3ldnR#jX42nNlIJtnTbU?O%9Lc~MprFu~k~wdwfk=*`R!hRFwKGJH zO_VZi_`r6vqanJN#fB-fHDIDZD`RKtqP{hnIt`vtKNhJZec0tOX@W(|3R5-NicZsP z1sh?JPnEJq-}gIVvm`@7q09YJPllG%(FZG5IH<VR9yoCD+EFHso+%Bf86rYF2M;JX z2y*!xX!wvE=-$lawB_lx0Mi)?HmgiSQv$bXzfjt@IDLx23bA#I7WO-43!GPWWZ<?5 zFgIYg;bdtr;EQCl(BkE6S60Mkc>>Uf%AY4_+!FE7;yMx_!l}EY-SE9}BbSx)X+!TV z9xY7|**x@SsNCFG$-p77@8TKO8(G|Xjo~g1claV178!VOinZz(^f_JO>dO}JdiItl z)ZVn5nWaW0n>R+2pQjtEr9qHjZBUrPWGHgfVMVjE9NQZ4W7*wJa~Kt!o+Q{aG;U|w zH$mC8$)d_BFrp!OckZGxZXu<G3NH^WPH0SM;|+CNCRubxl5c6jYt7GT{~NGc1M(X% z_EtFXxEcx^Iw_GGD{*|Mgh$VXwi_Dk0$nU7ObX96bR?F!DJFU_L<G;^Xv{TTvXwE( z#bXN_!>XN$NjhA8stpCkV#!C8H>m&MeWS!~VrCworWK*zpwZK-g4NP^sHGl2k8vz> zFkv{f>6oLE=R}STvlb?B*yXjddK##7Nc75in{VRv&|yE=saw*l)zs`NB_goou<pf# q6&(o&n0^Wvc5+B;nC{Te=WL$Ap>N2N#u}s9-J_<+U=2#1U;qGeCkTrG literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/doc/images/browser-demo.png b/examples/widgets/browser/doc/images/browser-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..09d06509538c405422deb3ea417ecaf99b35248f GIT binary patch literal 156342 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 ZV+<LRbfBJylY;{w<fN6QG9~Uk`9DnPhd}@U literal 0 HcmV?d00001 diff --git a/examples/widgets/browser/doc/src/browser.qdoc b/examples/widgets/browser/doc/src/browser.qdoc new file mode 100644 index 000000000..4e53de38f --- /dev/null +++ b/examples/widgets/browser/doc/src/browser.qdoc @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: http://www.gnu.org/copyleft/fdl.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example webkitwidgets/browser + \title Tab Browser + \brief Demonstrates a complete web browsing experience + \ingroup webkit-widgetexamples + + The Tab Browser example shows \l{Qt WebKit} module in action, + providing a little Web browser application with support for tabs. + + \image browser-demo.png + + This browser is the foundation for the \l{Arora Browser}, a simple cross-platform + Web browser. +*/ diff --git a/examples/widgets/browser/downloaditem.ui b/examples/widgets/browser/downloaditem.ui new file mode 100644 index 000000000..4a0a0fd9a --- /dev/null +++ b/examples/widgets/browser/downloaditem.ui @@ -0,0 +1,134 @@ +<ui version="4.0" > + <class>DownloadItem</class> + <widget class="QWidget" name="DownloadItem" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>423</width> + <height>110</height> + </rect> + </property> + <property name="windowTitle" > + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout" > + <property name="margin" > + <number>0</number> + </property> + <item> + <widget class="QLabel" name="fileIcon" > + <property name="sizePolicy" > + <sizepolicy vsizetype="Minimum" hsizetype="Minimum" > + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text" > + <string>Ico</string> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2" > + <item> + <widget class="SqueezeLabel" native="1" name="fileNameLabel" > + <property name="sizePolicy" > + <sizepolicy vsizetype="Preferred" hsizetype="Expanding" > + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text" stdset="0" > + <string>Filename</string> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar" > + <property name="value" > + <number>0</number> + </property> + </widget> + </item> + <item> + <widget class="SqueezeLabel" native="1" name="downloadInfoLabel" > + <property name="sizePolicy" > + <sizepolicy vsizetype="Preferred" hsizetype="Minimum" > + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text" stdset="0" > + <string/> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout" > + <item> + <spacer name="verticalSpacer" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>17</width> + <height>1</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="tryAgainButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>Try Again</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopButton" > + <property name="text" > + <string>Stop</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="openButton" > + <property name="text" > + <string>Open</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>17</width> + <height>5</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SqueezeLabel</class> + <extends>QWidget</extends> + <header>squeezelabel.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/examples/widgets/browser/downloadmanager.cpp b/examples/widgets/browser/downloadmanager.cpp new file mode 100644 index 000000000..04889d9e3 --- /dev/null +++ b/examples/widgets/browser/downloadmanager.cpp @@ -0,0 +1,578 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "downloadmanager.h" + +#include "autosaver.h" +#include "browserapplication.h" +#include "networkaccessmanager.h" + +#include <math.h> + +#include <QtCore/QMetaEnum> +#include <QtCore/QSettings> + +#include <QtGui/QDesktopServices> +#include <QtWidgets/QFileDialog> +#include <QtWidgets/QHeaderView> +#include <QtWidgets/QFileIconProvider> + +#include <QtCore/QDebug> + +#include <QWebSettings> + +/*! + DownloadItem is a widget that is displayed in the download manager list. + It moves the data from the QNetworkReply into the QFile as well + as update the information/progressbar and report errors. + */ +DownloadItem::DownloadItem(QNetworkReply *reply, bool requestFileName, QWidget *parent) + : QWidget(parent) + , m_reply(reply) + , m_requestFileName(requestFileName) + , m_bytesReceived(0) +{ + setupUi(this); + QPalette p = downloadInfoLabel->palette(); + p.setColor(QPalette::Text, Qt::darkGray); + downloadInfoLabel->setPalette(p); + progressBar->setMaximum(0); + tryAgainButton->hide(); + connect(stopButton, SIGNAL(clicked()), this, SLOT(stop())); + connect(openButton, SIGNAL(clicked()), this, SLOT(open())); + connect(tryAgainButton, SIGNAL(clicked()), this, SLOT(tryAgain())); + + init(); +} + +void DownloadItem::init() +{ + if (!m_reply) + return; + + // attach to the m_reply + m_url = m_reply->url(); + m_reply->setParent(this); + connect(m_reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); + connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(error(QNetworkReply::NetworkError))); + connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)), + this, SLOT(downloadProgress(qint64,qint64))); + connect(m_reply, SIGNAL(metaDataChanged()), + this, SLOT(metaDataChanged())); + connect(m_reply, SIGNAL(finished()), + this, SLOT(finished())); + + // reset info + downloadInfoLabel->clear(); + progressBar->setValue(0); + getFileName(); + + // start timer for the download estimation + m_downloadTime.start(); + + if (m_reply->error() != QNetworkReply::NoError) { + error(m_reply->error()); + finished(); + } +} + +void DownloadItem::getFileName() +{ + QSettings settings; + settings.beginGroup(QLatin1String("downloadmanager")); + QString defaultLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + QString downloadDirectory = settings.value(QLatin1String("downloadDirectory"), defaultLocation).toString(); + if (!downloadDirectory.isEmpty()) + downloadDirectory += QLatin1Char('/'); + + QString defaultFileName = saveFileName(downloadDirectory); + QString fileName = defaultFileName; + if (m_requestFileName) { + fileName = QFileDialog::getSaveFileName(this, tr("Save File"), defaultFileName); + if (fileName.isEmpty()) { + m_reply->close(); + fileNameLabel->setText(tr("Download canceled: %1").arg(QFileInfo(defaultFileName).fileName())); + return; + } + } + m_output.setFileName(fileName); + fileNameLabel->setText(QFileInfo(m_output.fileName()).fileName()); + if (m_requestFileName) + downloadReadyRead(); +} + +QString DownloadItem::saveFileName(const QString &directory) const +{ + // Move this function into QNetworkReply to also get file name sent from the server + QString path = m_url.path(); + QFileInfo info(path); + QString baseName = info.completeBaseName(); + QString endName = info.suffix(); + + if (baseName.isEmpty()) { + baseName = QLatin1String("unnamed_download"); + qDebug() << "DownloadManager:: downloading unknown file:" << m_url; + } + QString name = directory + baseName + QLatin1Char('.') + endName; + if (QFile::exists(name)) { + // already exists, don't overwrite + int i = 1; + do { + name = directory + baseName + QLatin1Char('-') + QString::number(i++) + QLatin1Char('.') + endName; + } while (QFile::exists(name)); + } + return name; +} + + +void DownloadItem::stop() +{ + setUpdatesEnabled(false); + stopButton->setEnabled(false); + stopButton->hide(); + tryAgainButton->setEnabled(true); + tryAgainButton->show(); + setUpdatesEnabled(true); + m_reply->abort(); +} + +void DownloadItem::open() +{ + QFileInfo info(m_output); + QUrl url = QUrl::fromLocalFile(info.absolutePath()); + QDesktopServices::openUrl(url); +} + +void DownloadItem::tryAgain() +{ + if (!tryAgainButton->isEnabled()) + return; + + tryAgainButton->setEnabled(false); + tryAgainButton->setVisible(false); + stopButton->setEnabled(true); + stopButton->setVisible(true); + progressBar->setVisible(true); + + QNetworkReply *r = BrowserApplication::networkAccessManager()->get(QNetworkRequest(m_url)); + if (m_reply) + m_reply->deleteLater(); + if (m_output.exists()) + m_output.remove(); + m_reply = r; + init(); + emit statusChanged(); +} + +void DownloadItem::downloadReadyRead() +{ + if (m_requestFileName && m_output.fileName().isEmpty()) + return; + if (!m_output.isOpen()) { + // in case someone else has already put a file there + if (!m_requestFileName) + getFileName(); + if (!m_output.open(QIODevice::WriteOnly)) { + downloadInfoLabel->setText(tr("Error opening save file: %1") + .arg(m_output.errorString())); + stopButton->click(); + emit statusChanged(); + return; + } + emit statusChanged(); + } + if (-1 == m_output.write(m_reply->readAll())) { + downloadInfoLabel->setText(tr("Error saving: %1") + .arg(m_output.errorString())); + stopButton->click(); + } +} + +void DownloadItem::error(QNetworkReply::NetworkError) +{ + qDebug() << "DownloadItem::error" << m_reply->errorString() << m_url; + downloadInfoLabel->setText(tr("Network Error: %1").arg(m_reply->errorString())); + tryAgainButton->setEnabled(true); + tryAgainButton->setVisible(true); +} + +void DownloadItem::metaDataChanged() +{ + qDebug() << "DownloadItem::metaDataChanged: not handled."; +} + +void DownloadItem::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_bytesReceived = bytesReceived; + if (bytesTotal == -1) { + progressBar->setValue(0); + progressBar->setMaximum(0); + } else { + progressBar->setValue(bytesReceived); + progressBar->setMaximum(bytesTotal); + } + updateInfoLabel(); +} + +void DownloadItem::updateInfoLabel() +{ + if (m_reply->error() == QNetworkReply::NoError) + return; + + qint64 bytesTotal = progressBar->maximum(); + bool running = !downloadedSuccessfully(); + + // update info label + double speed = m_bytesReceived * 1000.0 / m_downloadTime.elapsed(); + double timeRemaining = ((double)(bytesTotal - m_bytesReceived)) / speed; + QString timeRemainingString = tr("seconds"); + if (timeRemaining > 60) { + timeRemaining = timeRemaining / 60; + timeRemainingString = tr("minutes"); + } + timeRemaining = floor(timeRemaining); + + // When downloading the eta should never be 0 + if (timeRemaining == 0) + timeRemaining = 1; + + QString info; + if (running) { + QString remaining; + if (bytesTotal != 0) + remaining = tr("- %4 %5 remaining") + .arg(timeRemaining) + .arg(timeRemainingString); + info = tr("%1 of %2 (%3/sec) %4") + .arg(dataString(m_bytesReceived)) + .arg(bytesTotal == 0 ? tr("?") : dataString(bytesTotal)) + .arg(dataString((int)speed)) + .arg(remaining); + } else { + if (m_bytesReceived == bytesTotal) + info = dataString(m_output.size()); + else + info = tr("%1 of %2 - Stopped") + .arg(dataString(m_bytesReceived)) + .arg(dataString(bytesTotal)); + } + downloadInfoLabel->setText(info); +} + +QString DownloadItem::dataString(int size) const +{ + QString unit; + if (size < 1024) { + unit = tr("bytes"); + } else if (size < 1024*1024) { + size /= 1024; + unit = tr("kB"); + } else { + size /= 1024*1024; + unit = tr("MB"); + } + return QString(QLatin1String("%1 %2")).arg(size).arg(unit); +} + +bool DownloadItem::downloading() const +{ + return (progressBar->isVisible()); +} + +bool DownloadItem::downloadedSuccessfully() const +{ + return (stopButton->isHidden() && tryAgainButton->isHidden()); +} + +void DownloadItem::finished() +{ + progressBar->hide(); + stopButton->setEnabled(false); + stopButton->hide(); + m_output.close(); + updateInfoLabel(); + emit statusChanged(); +} + +/*! + DownloadManager is a Dialog that contains a list of DownloadItems + + It is a basic download manager. It only downloads the file, doesn't do BitTorrent, + extract zipped files or anything fancy. + */ +DownloadManager::DownloadManager(QWidget *parent) + : QDialog(parent) + , m_autoSaver(new AutoSaver(this)) + , m_manager(BrowserApplication::networkAccessManager()) + , m_iconProvider(0) + , m_removePolicy(Never) +{ + setupUi(this); + downloadsView->setShowGrid(false); + downloadsView->verticalHeader()->hide(); + downloadsView->horizontalHeader()->hide(); + downloadsView->setAlternatingRowColors(true); + downloadsView->horizontalHeader()->setStretchLastSection(true); + m_model = new DownloadModel(this); + downloadsView->setModel(m_model); + connect(cleanupButton, SIGNAL(clicked()), this, SLOT(cleanup())); + load(); +} + +DownloadManager::~DownloadManager() +{ + m_autoSaver->changeOccurred(); + m_autoSaver->saveIfNeccessary(); + if (m_iconProvider) + delete m_iconProvider; +} + +int DownloadManager::activeDownloads() const +{ + int count = 0; + for (int i = 0; i < m_downloads.count(); ++i) { + if (m_downloads.at(i)->stopButton->isEnabled()) + ++count; + } + return count; +} + +void DownloadManager::download(const QNetworkRequest &request, bool requestFileName) +{ + if (request.url().isEmpty()) + return; + handleUnsupportedContent(m_manager->get(request), requestFileName); +} + +void DownloadManager::handleUnsupportedContent(QNetworkReply *reply, bool requestFileName) +{ + if (!reply || reply->url().isEmpty()) + return; + QVariant header = reply->header(QNetworkRequest::ContentLengthHeader); + bool ok; + int size = header.toInt(&ok); + if (ok && size == 0) + return; + + qDebug() << "DownloadManager::handleUnsupportedContent" << reply->url() << "requestFileName" << requestFileName; + DownloadItem *item = new DownloadItem(reply, requestFileName, this); + addItem(item); +} + +void DownloadManager::addItem(DownloadItem *item) +{ + connect(item, SIGNAL(statusChanged()), this, SLOT(updateRow())); + int row = m_downloads.count(); + m_model->beginInsertRows(QModelIndex(), row, row); + m_downloads.append(item); + m_model->endInsertRows(); + updateItemCount(); + if (row == 0) + show(); + downloadsView->setIndexWidget(m_model->index(row, 0), item); + QIcon icon = style()->standardIcon(QStyle::SP_FileIcon); + item->fileIcon->setPixmap(icon.pixmap(48, 48)); + downloadsView->setRowHeight(row, item->sizeHint().height()); +} + +void DownloadManager::updateRow() +{ + DownloadItem *item = qobject_cast<DownloadItem*>(sender()); + int row = m_downloads.indexOf(item); + if (-1 == row) + return; + if (!m_iconProvider) + m_iconProvider = new QFileIconProvider(); + QIcon icon = m_iconProvider->icon(item->m_output.fileName()); + if (icon.isNull()) + icon = style()->standardIcon(QStyle::SP_FileIcon); + item->fileIcon->setPixmap(icon.pixmap(48, 48)); + downloadsView->setRowHeight(row, item->minimumSizeHint().height()); + + bool remove = false; + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (!item->downloading() + && globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) + remove = true; + + if (item->downloadedSuccessfully() + && removePolicy() == DownloadManager::SuccessFullDownload) { + remove = true; + } + if (remove) + m_model->removeRow(row); + + cleanupButton->setEnabled(m_downloads.count() - activeDownloads() > 0); +} + +DownloadManager::RemovePolicy DownloadManager::removePolicy() const +{ + return m_removePolicy; +} + +void DownloadManager::setRemovePolicy(RemovePolicy policy) +{ + if (policy == m_removePolicy) + return; + m_removePolicy = policy; + m_autoSaver->changeOccurred(); +} + +void DownloadManager::save() const +{ + QSettings settings; + settings.beginGroup(QLatin1String("downloadmanager")); + QMetaEnum removePolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("RemovePolicy")); + settings.setValue(QLatin1String("removeDownloadsPolicy"), QLatin1String(removePolicyEnum.valueToKey(m_removePolicy))); + settings.setValue(QLatin1String("size"), size()); + if (m_removePolicy == Exit) + return; + + for (int i = 0; i < m_downloads.count(); ++i) { + QString key = QString(QLatin1String("download_%1_")).arg(i); + settings.setValue(key + QLatin1String("url"), m_downloads[i]->m_url); + settings.setValue(key + QLatin1String("location"), QFileInfo(m_downloads[i]->m_output).filePath()); + settings.setValue(key + QLatin1String("done"), m_downloads[i]->downloadedSuccessfully()); + } + int i = m_downloads.count(); + QString key = QString(QLatin1String("download_%1_")).arg(i); + while (settings.contains(key + QLatin1String("url"))) { + settings.remove(key + QLatin1String("url")); + settings.remove(key + QLatin1String("location")); + settings.remove(key + QLatin1String("done")); + key = QString(QLatin1String("download_%1_")).arg(++i); + } +} + +void DownloadManager::load() +{ + QSettings settings; + settings.beginGroup(QLatin1String("downloadmanager")); + QSize size = settings.value(QLatin1String("size")).toSize(); + if (size.isValid()) + resize(size); + QByteArray value = settings.value(QLatin1String("removeDownloadsPolicy"), QLatin1String("Never")).toByteArray(); + QMetaEnum removePolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("RemovePolicy")); + m_removePolicy = removePolicyEnum.keyToValue(value) == -1 ? + Never : + static_cast<RemovePolicy>(removePolicyEnum.keyToValue(value)); + + int i = 0; + QString key = QString(QLatin1String("download_%1_")).arg(i); + while (settings.contains(key + QLatin1String("url"))) { + QUrl url = settings.value(key + QLatin1String("url")).toUrl(); + QString fileName = settings.value(key + QLatin1String("location")).toString(); + bool done = settings.value(key + QLatin1String("done"), true).toBool(); + if (!url.isEmpty() && !fileName.isEmpty()) { + DownloadItem *item = new DownloadItem(0, this); + item->m_output.setFileName(fileName); + item->fileNameLabel->setText(QFileInfo(item->m_output.fileName()).fileName()); + item->m_url = url; + item->stopButton->setVisible(false); + item->stopButton->setEnabled(false); + item->tryAgainButton->setVisible(!done); + item->tryAgainButton->setEnabled(!done); + item->progressBar->setVisible(!done); + addItem(item); + } + key = QString(QLatin1String("download_%1_")).arg(++i); + } + cleanupButton->setEnabled(m_downloads.count() - activeDownloads() > 0); +} + +void DownloadManager::cleanup() +{ + if (m_downloads.isEmpty()) + return; + m_model->removeRows(0, m_downloads.count()); + updateItemCount(); + if (m_downloads.isEmpty() && m_iconProvider) { + delete m_iconProvider; + m_iconProvider = 0; + } + m_autoSaver->changeOccurred(); +} + +void DownloadManager::updateItemCount() +{ + int count = m_downloads.count(); + itemCount->setText(count == 1 ? tr("1 Download") : tr("%1 Downloads").arg(count)); +} + +DownloadModel::DownloadModel(DownloadManager *downloadManager, QObject *parent) + : QAbstractListModel(parent) + , m_downloadManager(downloadManager) +{ +} + +QVariant DownloadModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= rowCount(index.parent())) + return QVariant(); + if (role == Qt::ToolTipRole) + if (!m_downloadManager->m_downloads.at(index.row())->downloadedSuccessfully()) + return m_downloadManager->m_downloads.at(index.row())->downloadInfoLabel->text(); + return QVariant(); +} + +int DownloadModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : m_downloadManager->m_downloads.count(); +} + +bool DownloadModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + int lastRow = row + count - 1; + for (int i = lastRow; i >= row; --i) { + if (m_downloadManager->m_downloads.at(i)->downloadedSuccessfully() + || m_downloadManager->m_downloads.at(i)->tryAgainButton->isEnabled()) { + beginRemoveRows(parent, i, i); + m_downloadManager->m_downloads.takeAt(i)->deleteLater(); + endRemoveRows(); + } + } + m_downloadManager->m_autoSaver->changeOccurred(); + return true; +} diff --git a/examples/widgets/browser/downloadmanager.h b/examples/widgets/browser/downloadmanager.h new file mode 100644 index 000000000..072e99efd --- /dev/null +++ b/examples/widgets/browser/downloadmanager.h @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef DOWNLOADMANAGER_H +#define DOWNLOADMANAGER_H + +#include "ui_downloads.h" +#include "ui_downloaditem.h" + +#include <QtNetwork/QNetworkReply> + +#include <QtCore/QFile> +#include <QtCore/QTime> + +class DownloadItem : public QWidget, public Ui_DownloadItem +{ + Q_OBJECT + +signals: + void statusChanged(); + +public: + DownloadItem(QNetworkReply *reply = 0, bool requestFileName = false, QWidget *parent = 0); + bool downloading() const; + bool downloadedSuccessfully() const; + + QUrl m_url; + + QFile m_output; + QNetworkReply *m_reply; + +private slots: + void stop(); + void tryAgain(); + void open(); + + void downloadReadyRead(); + void error(QNetworkReply::NetworkError code); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void metaDataChanged(); + void finished(); + +private: + void getFileName(); + void init(); + void updateInfoLabel(); + QString dataString(int size) const; + + QString saveFileName(const QString &directory) const; + + bool m_requestFileName; + qint64 m_bytesReceived; + QTime m_downloadTime; +}; + +class AutoSaver; +class DownloadModel; +QT_BEGIN_NAMESPACE +class QFileIconProvider; +QT_END_NAMESPACE + +class DownloadManager : public QDialog, public Ui_DownloadDialog +{ + Q_OBJECT + Q_PROPERTY(RemovePolicy removePolicy READ removePolicy WRITE setRemovePolicy) + Q_ENUMS(RemovePolicy) + +public: + enum RemovePolicy { + Never, + Exit, + SuccessFullDownload + }; + + DownloadManager(QWidget *parent = 0); + ~DownloadManager(); + int activeDownloads() const; + + RemovePolicy removePolicy() const; + void setRemovePolicy(RemovePolicy policy); + +public slots: + void download(const QNetworkRequest &request, bool requestFileName = false); + inline void download(const QUrl &url, bool requestFileName = false) + { download(QNetworkRequest(url), requestFileName); } + void handleUnsupportedContent(QNetworkReply *reply, bool requestFileName = false); + void cleanup(); + +private slots: + void save() const; + void updateRow(); + +private: + void addItem(DownloadItem *item); + void updateItemCount(); + void load(); + + AutoSaver *m_autoSaver; + DownloadModel *m_model; + QNetworkAccessManager *m_manager; + QFileIconProvider *m_iconProvider; + QList<DownloadItem*> m_downloads; + RemovePolicy m_removePolicy; + friend class DownloadModel; +}; + +class DownloadModel : public QAbstractListModel +{ + friend class DownloadManager; + Q_OBJECT + +public: + DownloadModel(DownloadManager *downloadManager, QObject *parent = 0); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + +private: + DownloadManager *m_downloadManager; + +}; + +#endif // DOWNLOADMANAGER_H diff --git a/examples/widgets/browser/downloads.ui b/examples/widgets/browser/downloads.ui new file mode 100644 index 000000000..a2e256935 --- /dev/null +++ b/examples/widgets/browser/downloads.ui @@ -0,0 +1,83 @@ +<ui version="4.0" > + <class>DownloadDialog</class> + <widget class="QDialog" name="DownloadDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>332</width> + <height>252</height> + </rect> + </property> + <property name="windowTitle" > + <string>Downloads</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <property name="margin" > + <number>0</number> + </property> + <property name="spacing" > + <number>0</number> + </property> + <item row="0" column="0" colspan="3" > + <widget class="EditTableView" name="downloadsView" /> + </item> + <item row="1" column="0" > + <layout class="QHBoxLayout" name="horizontalLayout" > + <item> + <widget class="QPushButton" name="cleanupButton" > + <property name="enabled" > + <bool>false</bool> + </property> + <property name="text" > + <string>Clean up</string> + </property> + </widget> + </item> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>58</width> + <height>24</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="1" > + <widget class="QLabel" name="itemCount" > + <property name="text" > + <string>0 Items</string> + </property> + </widget> + </item> + <item row="1" column="2" > + <spacer name="horizontalSpacer" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>148</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>EditTableView</class> + <extends>QTableView</extends> + <header>edittableview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/examples/widgets/browser/edittableview.cpp b/examples/widgets/browser/edittableview.cpp new file mode 100644 index 000000000..71d9b5706 --- /dev/null +++ b/examples/widgets/browser/edittableview.cpp @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "edittableview.h" +#include <QtGui/QKeyEvent> + +EditTableView::EditTableView(QWidget *parent) + : QTableView(parent) +{ +} + +void EditTableView::keyPressEvent(QKeyEvent *event) +{ + if ((event->key() == Qt::Key_Delete + || event->key() == Qt::Key_Backspace) + && model()) { + removeOne(); + } else { + QAbstractItemView::keyPressEvent(event); + } +} + +void EditTableView::removeOne() +{ + if (!model() || !selectionModel()) + return; + int row = currentIndex().row(); + model()->removeRow(row, rootIndex()); + QModelIndex idx = model()->index(row, 0, rootIndex()); + if (!idx.isValid()) + idx = model()->index(row - 1, 0, rootIndex()); + selectionModel()->select(idx, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); +} + +void EditTableView::removeAll() +{ + if (model()) + model()->removeRows(0, model()->rowCount(rootIndex()), rootIndex()); +} diff --git a/examples/widgets/browser/edittableview.h b/examples/widgets/browser/edittableview.h new file mode 100644 index 000000000..1c865ad42 --- /dev/null +++ b/examples/widgets/browser/edittableview.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef EDITTABLEVIEW_H +#define EDITTABLEVIEW_H + +#include <QtWidgets/QTableView> + +class EditTableView : public QTableView +{ + Q_OBJECT + +public: + EditTableView(QWidget *parent = 0); + void keyPressEvent(QKeyEvent *event); + +public slots: + void removeOne(); + void removeAll(); +}; + +#endif // EDITTABLEVIEW_H diff --git a/examples/widgets/browser/edittreeview.cpp b/examples/widgets/browser/edittreeview.cpp new file mode 100644 index 000000000..5b8007856 --- /dev/null +++ b/examples/widgets/browser/edittreeview.cpp @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "edittreeview.h" + +#include <QtGui/QKeyEvent> + +EditTreeView::EditTreeView(QWidget *parent) + : QTreeView(parent) +{ +} + +void EditTreeView::keyPressEvent(QKeyEvent *event) +{ + if ((event->key() == Qt::Key_Delete + || event->key() == Qt::Key_Backspace) + && model()) { + removeOne(); + } else { + QAbstractItemView::keyPressEvent(event); + } +} + +void EditTreeView::removeOne() +{ + if (!model()) + return; + QModelIndex ci = currentIndex(); + int row = ci.row(); + model()->removeRow(row, ci.parent()); +} + +void EditTreeView::removeAll() +{ + if (!model()) + return; + model()->removeRows(0, model()->rowCount(rootIndex()), rootIndex()); +} diff --git a/examples/widgets/browser/edittreeview.h b/examples/widgets/browser/edittreeview.h new file mode 100644 index 000000000..4159812a6 --- /dev/null +++ b/examples/widgets/browser/edittreeview.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef EDITTREEVIEW_H +#define EDITTREEVIEW_H + +#include <QtWidgets/QTreeView> + +class EditTreeView : public QTreeView +{ + Q_OBJECT + +public: + EditTreeView(QWidget *parent = 0); + void keyPressEvent(QKeyEvent *event); + +public slots: + void removeOne(); + void removeAll(); +}; + +#endif // EDITTREEVIEW_H diff --git a/examples/widgets/browser/history.cpp b/examples/widgets/browser/history.cpp new file mode 100644 index 000000000..d5b245e34 --- /dev/null +++ b/examples/widgets/browser/history.cpp @@ -0,0 +1,1291 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "history.h" + +#include "autosaver.h" +#include "browserapplication.h" + +#include <QtCore/QBuffer> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QSettings> +#include <QtCore/QTemporaryFile> +#include <QtCore/QTextStream> + +#include <QtCore/QtAlgorithms> + +#include <QtGui/QClipboard> +#include <QtGui/QDesktopServices> +#include <QtWidgets/QHeaderView> +#include <QtWidgets/QStyle> + +#include <QWebHistoryInterface> +#include <QWebSettings> + +#include <QtCore/QDebug> + +static const unsigned int HISTORY_VERSION = 23; + +HistoryManager::HistoryManager(QObject *parent) + : QWebHistoryInterface(parent) + , m_saveTimer(new AutoSaver(this)) + , m_historyLimit(30) + , m_historyModel(0) + , m_historyFilterModel(0) + , m_historyTreeModel(0) +{ + m_expiredTimer.setSingleShot(true); + connect(&m_expiredTimer, SIGNAL(timeout()), + this, SLOT(checkForExpired())); + connect(this, SIGNAL(entryAdded(HistoryItem)), + m_saveTimer, SLOT(changeOccurred())); + connect(this, SIGNAL(entryRemoved(HistoryItem)), + m_saveTimer, SLOT(changeOccurred())); + load(); + + m_historyModel = new HistoryModel(this, this); + m_historyFilterModel = new HistoryFilterModel(m_historyModel, this); + m_historyTreeModel = new HistoryTreeModel(m_historyFilterModel, this); + + // QWebHistoryInterface will delete the history manager + QWebHistoryInterface::setDefaultInterface(this); +} + +HistoryManager::~HistoryManager() +{ + m_saveTimer->saveIfNeccessary(); +} + +QList<HistoryItem> HistoryManager::history() const +{ + return m_history; +} + +bool HistoryManager::historyContains(const QString &url) const +{ + return m_historyFilterModel->historyContains(url); +} + +void HistoryManager::addHistoryEntry(const QString &url) +{ + QUrl cleanUrl(url); + cleanUrl.setPassword(QString()); + cleanUrl.setHost(cleanUrl.host().toLower()); + HistoryItem item(cleanUrl.toString(), QDateTime::currentDateTime()); + addHistoryItem(item); +} + +void HistoryManager::setHistory(const QList<HistoryItem> &history, bool loadedAndSorted) +{ + m_history = history; + + // verify that it is sorted by date + if (!loadedAndSorted) + qSort(m_history.begin(), m_history.end()); + + checkForExpired(); + + if (loadedAndSorted) { + m_lastSavedUrl = m_history.value(0).url; + } else { + m_lastSavedUrl = QString(); + m_saveTimer->changeOccurred(); + } + emit historyReset(); +} + +HistoryModel *HistoryManager::historyModel() const +{ + return m_historyModel; +} + +HistoryFilterModel *HistoryManager::historyFilterModel() const +{ + return m_historyFilterModel; +} + +HistoryTreeModel *HistoryManager::historyTreeModel() const +{ + return m_historyTreeModel; +} + +void HistoryManager::checkForExpired() +{ + if (m_historyLimit < 0 || m_history.isEmpty()) + return; + + QDateTime now = QDateTime::currentDateTime(); + int nextTimeout = 0; + + while (!m_history.isEmpty()) { + QDateTime checkForExpired = m_history.last().dateTime; + checkForExpired.setDate(checkForExpired.date().addDays(m_historyLimit)); + if (now.daysTo(checkForExpired) > 7) { + // check at most in a week to prevent int overflows on the timer + nextTimeout = 7 * 86400; + } else { + nextTimeout = now.secsTo(checkForExpired); + } + if (nextTimeout > 0) + break; + HistoryItem item = m_history.takeLast(); + // remove from saved file also + m_lastSavedUrl = QString(); + emit entryRemoved(item); + } + + if (nextTimeout > 0) + m_expiredTimer.start(nextTimeout * 1000); +} + +void HistoryManager::addHistoryItem(const HistoryItem &item) +{ + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) + return; + + m_history.prepend(item); + emit entryAdded(item); + if (m_history.count() == 1) + checkForExpired(); +} + +void HistoryManager::updateHistoryItem(const QUrl &url, const QString &title) +{ + for (int i = 0; i < m_history.count(); ++i) { + if (url == m_history.at(i).url) { + m_history[i].title = title; + m_saveTimer->changeOccurred(); + if (m_lastSavedUrl.isEmpty()) + m_lastSavedUrl = m_history.at(i).url; + emit entryUpdated(i); + break; + } + } +} + +int HistoryManager::historyLimit() const +{ + return m_historyLimit; +} + +void HistoryManager::setHistoryLimit(int limit) +{ + if (m_historyLimit == limit) + return; + m_historyLimit = limit; + checkForExpired(); + m_saveTimer->changeOccurred(); +} + +void HistoryManager::clear() +{ + m_history.clear(); + m_lastSavedUrl = QString(); + m_saveTimer->changeOccurred(); + m_saveTimer->saveIfNeccessary(); + historyReset(); +} + +void HistoryManager::loadSettings() +{ + // load settings + QSettings settings; + settings.beginGroup(QLatin1String("history")); + m_historyLimit = settings.value(QLatin1String("historyLimit"), 30).toInt(); +} + +void HistoryManager::load() +{ + loadSettings(); + + QFile historyFile(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + + QLatin1String("/history")); + if (!historyFile.exists()) + return; + if (!historyFile.open(QFile::ReadOnly)) { + qWarning() << "Unable to open history file" << historyFile.fileName(); + return; + } + + QList<HistoryItem> list; + QDataStream in(&historyFile); + // Double check that the history file is sorted as it is read in + bool needToSort = false; + HistoryItem lastInsertedItem; + QByteArray data; + QDataStream stream; + QBuffer buffer; + stream.setDevice(&buffer); + while (!historyFile.atEnd()) { + in >> data; + buffer.close(); + buffer.setBuffer(&data); + buffer.open(QIODevice::ReadOnly); + quint32 ver; + stream >> ver; + if (ver != HISTORY_VERSION) + continue; + HistoryItem item; + stream >> item.url; + stream >> item.dateTime; + stream >> item.title; + + if (!item.dateTime.isValid()) + continue; + + if (item == lastInsertedItem) { + if (lastInsertedItem.title.isEmpty() && !list.isEmpty()) + list[0].title = item.title; + continue; + } + + if (!needToSort && !list.isEmpty() && lastInsertedItem < item) + needToSort = true; + + list.prepend(item); + lastInsertedItem = item; + } + if (needToSort) + qSort(list.begin(), list.end()); + + setHistory(list, true); + + // If we had to sort re-write the whole history sorted + if (needToSort) { + m_lastSavedUrl = QString(); + m_saveTimer->changeOccurred(); + } +} + +void HistoryManager::save() +{ + QSettings settings; + settings.beginGroup(QLatin1String("history")); + settings.setValue(QLatin1String("historyLimit"), m_historyLimit); + + bool saveAll = m_lastSavedUrl.isEmpty(); + int first = m_history.count() - 1; + if (!saveAll) { + // find the first one to save + for (int i = 0; i < m_history.count(); ++i) { + if (m_history.at(i).url == m_lastSavedUrl) { + first = i - 1; + break; + } + } + } + if (first == m_history.count() - 1) + saveAll = true; + + QString directory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (directory.isEmpty()) + directory = QDir::homePath() + QLatin1String("/.") + QCoreApplication::applicationName(); + if (!QFile::exists(directory)) { + QDir dir; + dir.mkpath(directory); + } + + QFile historyFile(directory + QLatin1String("/history")); + // When saving everything use a temporary file to prevent possible data loss. + QTemporaryFile tempFile; + tempFile.setAutoRemove(false); + bool open = false; + if (saveAll) { + open = tempFile.open(); + } else { + open = historyFile.open(QFile::Append); + } + + if (!open) { + qWarning() << "Unable to open history file for saving" + << (saveAll ? tempFile.fileName() : historyFile.fileName()); + return; + } + + QDataStream out(saveAll ? &tempFile : &historyFile); + for (int i = first; i >= 0; --i) { + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + HistoryItem item = m_history.at(i); + stream << HISTORY_VERSION << item.url << item.dateTime << item.title; + out << data; + } + tempFile.close(); + + if (saveAll) { + if (historyFile.exists() && !historyFile.remove()) + qWarning() << "History: error removing old history." << historyFile.errorString(); + if (!tempFile.rename(historyFile.fileName())) + qWarning() << "History: error moving new history over old." << tempFile.errorString() << historyFile.fileName(); + } + m_lastSavedUrl = m_history.value(0).url; +} + +HistoryModel::HistoryModel(HistoryManager *history, QObject *parent) + : QAbstractTableModel(parent) + , m_history(history) +{ + Q_ASSERT(m_history); + connect(m_history, SIGNAL(historyReset()), + this, SLOT(historyReset())); + connect(m_history, SIGNAL(entryRemoved(HistoryItem)), + this, SLOT(historyReset())); + + connect(m_history, SIGNAL(entryAdded(HistoryItem)), + this, SLOT(entryAdded())); + connect(m_history, SIGNAL(entryUpdated(int)), + this, SLOT(entryUpdated(int))); +} + +void HistoryModel::historyReset() +{ + beginResetModel(); + endResetModel(); +} + +void HistoryModel::entryAdded() +{ + beginInsertRows(QModelIndex(), 0, 0); + endInsertRows(); +} + +void HistoryModel::entryUpdated(int offset) +{ + QModelIndex idx = index(offset, 0); + emit dataChanged(idx, idx); +} + +QVariant HistoryModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal + && role == Qt::DisplayRole) { + switch (section) { + case 0: return tr("Title"); + case 1: return tr("Address"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +QVariant HistoryModel::data(const QModelIndex &index, int role) const +{ + QList<HistoryItem> lst = m_history->history(); + if (index.row() < 0 || index.row() >= lst.size()) + return QVariant(); + + const HistoryItem &item = lst.at(index.row()); + switch (role) { + case DateTimeRole: + return item.dateTime; + case DateRole: + return item.dateTime.date(); + case UrlRole: + return QUrl(item.url); + case UrlStringRole: + return item.url; + case Qt::DisplayRole: + case Qt::EditRole: { + switch (index.column()) { + case 0: + // when there is no title try to generate one from the url + if (item.title.isEmpty()) { + QString page = QFileInfo(QUrl(item.url).path()).fileName(); + if (!page.isEmpty()) + return page; + return item.url; + } + return item.title; + case 1: + return item.url; + } + } + case Qt::DecorationRole: + if (index.column() == 0) { + return BrowserApplication::instance()->icon(item.url); + } + } + return QVariant(); +} + +int HistoryModel::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 2; +} + +int HistoryModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : m_history->history().count(); +} + +bool HistoryModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + int lastRow = row + count - 1; + beginRemoveRows(parent, row, lastRow); + QList<HistoryItem> lst = m_history->history(); + for (int i = lastRow; i >= row; --i) + lst.removeAt(i); + disconnect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset())); + m_history->setHistory(lst); + connect(m_history, SIGNAL(historyReset()), this, SLOT(historyReset())); + endRemoveRows(); + return true; +} + +#define MOVEDROWS 15 + +/* + Maps the first bunch of items of the source model to the root +*/ +HistoryMenuModel::HistoryMenuModel(HistoryTreeModel *sourceModel, QObject *parent) + : QAbstractProxyModel(parent) + , m_treeModel(sourceModel) +{ + setSourceModel(sourceModel); +} + +int HistoryMenuModel::bumpedRows() const +{ + QModelIndex first = m_treeModel->index(0, 0); + if (!first.isValid()) + return 0; + return qMin(m_treeModel->rowCount(first), MOVEDROWS); +} + +int HistoryMenuModel::columnCount(const QModelIndex &parent) const +{ + return m_treeModel->columnCount(mapToSource(parent)); +} + +int HistoryMenuModel::rowCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) { + int folders = sourceModel()->rowCount(); + int bumpedItems = bumpedRows(); + if (bumpedItems <= MOVEDROWS + && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0))) + --folders; + return bumpedItems + folders; + } + + if (parent.internalId() == quintptr(-1)) { + if (parent.row() < bumpedRows()) + return 0; + } + + QModelIndex idx = mapToSource(parent); + int defaultCount = sourceModel()->rowCount(idx); + if (idx == sourceModel()->index(0, 0)) + return defaultCount - bumpedRows(); + return defaultCount; +} + +QModelIndex HistoryMenuModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + // currently not used or autotested + Q_ASSERT(false); + int sr = m_treeModel->mapToSource(sourceIndex).row(); + return createIndex(sourceIndex.row(), sourceIndex.column(), sr); +} + +QModelIndex HistoryMenuModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!proxyIndex.isValid()) + return QModelIndex(); + + if (proxyIndex.internalId() == quintptr(-1)) { + int bumpedItems = bumpedRows(); + if (proxyIndex.row() < bumpedItems) + return m_treeModel->index(proxyIndex.row(), proxyIndex.column(), m_treeModel->index(0, 0)); + if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(m_treeModel->index(0, 0))) + --bumpedItems; + return m_treeModel->index(proxyIndex.row() - bumpedItems, proxyIndex.column()); + } + + QModelIndex historyIndex = m_treeModel->sourceModel()->index(proxyIndex.internalId(), proxyIndex.column()); + QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex); + return treeIndex; +} + +QModelIndex HistoryMenuModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 + || column < 0 || column >= columnCount(parent) + || parent.column() > 0) + return QModelIndex(); + if (!parent.isValid()) + return createIndex(row, column, quintptr(-1)); + + QModelIndex treeIndexParent = mapToSource(parent); + + int bumpedItems = 0; + if (treeIndexParent == m_treeModel->index(0, 0)) + bumpedItems = bumpedRows(); + QModelIndex treeIndex = m_treeModel->index(row + bumpedItems, column, treeIndexParent); + QModelIndex historyIndex = m_treeModel->mapToSource(treeIndex); + int historyRow = historyIndex.row(); + if (historyRow == -1) + historyRow = treeIndex.row(); + return createIndex(row, column, historyRow); +} + +QModelIndex HistoryMenuModel::parent(const QModelIndex &index) const +{ + int offset = index.internalId(); + if (offset == -1 || !index.isValid()) + return QModelIndex(); + + QModelIndex historyIndex = m_treeModel->sourceModel()->index(index.internalId(), 0); + QModelIndex treeIndex = m_treeModel->mapFromSource(historyIndex); + QModelIndex treeIndexParent = treeIndex.parent(); + + int sr = m_treeModel->mapToSource(treeIndexParent).row(); + int bumpedItems = bumpedRows(); + if (bumpedItems <= MOVEDROWS && bumpedItems == sourceModel()->rowCount(sourceModel()->index(0, 0))) + --bumpedItems; + return createIndex(bumpedItems + treeIndexParent.row(), treeIndexParent.column(), sr); +} + + +HistoryMenu::HistoryMenu(QWidget *parent) + : ModelMenu(parent) + , m_history(0) +{ + connect(this, SIGNAL(activated(QModelIndex)), + this, SLOT(activated(QModelIndex))); + setHoverRole(HistoryModel::UrlStringRole); +} + +void HistoryMenu::activated(const QModelIndex &index) +{ + emit openUrl(index.data(HistoryModel::UrlRole).toUrl()); +} + +bool HistoryMenu::prePopulated() +{ + if (!m_history) { + m_history = BrowserApplication::historyManager(); + m_historyMenuModel = new HistoryMenuModel(m_history->historyTreeModel(), this); + setModel(m_historyMenuModel); + } + // initial actions + for (int i = 0; i < m_initialActions.count(); ++i) + addAction(m_initialActions.at(i)); + if (!m_initialActions.isEmpty()) + addSeparator(); + setFirstSeparator(m_historyMenuModel->bumpedRows()); + + return false; +} + +void HistoryMenu::postPopulated() +{ + if (m_history->history().count() > 0) + addSeparator(); + + QAction *showAllAction = new QAction(tr("Show All History"), this); + connect(showAllAction, SIGNAL(triggered()), this, SLOT(showHistoryDialog())); + addAction(showAllAction); + + QAction *clearAction = new QAction(tr("Clear History"), this); + connect(clearAction, SIGNAL(triggered()), m_history, SLOT(clear())); + addAction(clearAction); +} + +void HistoryMenu::showHistoryDialog() +{ + HistoryDialog *dialog = new HistoryDialog(this); + connect(dialog, SIGNAL(openUrl(QUrl)), + this, SIGNAL(openUrl(QUrl))); + dialog->show(); +} + +void HistoryMenu::setInitialActions(QList<QAction*> actions) +{ + m_initialActions = actions; + for (int i = 0; i < m_initialActions.count(); ++i) + addAction(m_initialActions.at(i)); +} + +TreeProxyModel::TreeProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + setSortRole(HistoryModel::DateTimeRole); + setFilterCaseSensitivity(Qt::CaseInsensitive); +} + +bool TreeProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!source_parent.isValid()) + return true; + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} + +HistoryDialog::HistoryDialog(QWidget *parent, HistoryManager *setHistory) : QDialog(parent) +{ + HistoryManager *history = setHistory; + if (!history) + history = BrowserApplication::historyManager(); + setupUi(this); + tree->setUniformRowHeights(true); + tree->setSelectionBehavior(QAbstractItemView::SelectRows); + tree->setTextElideMode(Qt::ElideMiddle); + QAbstractItemModel *model = history->historyTreeModel(); + TreeProxyModel *proxyModel = new TreeProxyModel(this); + connect(search, SIGNAL(textChanged(QString)), + proxyModel, SLOT(setFilterFixedString(QString))); + connect(removeButton, SIGNAL(clicked()), tree, SLOT(removeOne())); + connect(removeAllButton, SIGNAL(clicked()), history, SLOT(clear())); + proxyModel->setSourceModel(model); + tree->setModel(proxyModel); + tree->setExpanded(proxyModel->index(0, 0), true); + tree->setAlternatingRowColors(true); + QFontMetrics fm(font()); + int header = fm.width(QLatin1Char('m')) * 40; + tree->header()->resizeSection(0, header); + tree->header()->setStretchLastSection(true); + connect(tree, SIGNAL(activated(QModelIndex)), + this, SLOT(open())); + tree->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tree, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(customContextMenuRequested(QPoint))); +} + +void HistoryDialog::customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + QModelIndex index = tree->indexAt(pos); + index = index.sibling(index.row(), 0); + if (index.isValid() && !tree->model()->hasChildren(index)) { + menu.addAction(tr("Open"), this, SLOT(open())); + menu.addSeparator(); + menu.addAction(tr("Copy"), this, SLOT(copy())); + } + menu.addAction(tr("Delete"), tree, SLOT(removeOne())); + menu.exec(QCursor::pos()); +} + +void HistoryDialog::open() +{ + QModelIndex index = tree->currentIndex(); + if (!index.parent().isValid()) + return; + emit openUrl(index.data(HistoryModel::UrlRole).toUrl()); +} + +void HistoryDialog::copy() +{ + QModelIndex index = tree->currentIndex(); + if (!index.parent().isValid()) + return; + QString url = index.data(HistoryModel::UrlStringRole).toString(); + + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(url); +} + +HistoryFilterModel::HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent) + : QAbstractProxyModel(parent), + m_loaded(false) +{ + setSourceModel(sourceModel); +} + +int HistoryFilterModel::historyLocation(const QString &url) const +{ + load(); + if (!m_historyHash.contains(url)) + return 0; + return sourceModel()->rowCount() - m_historyHash.value(url); +} + +QVariant HistoryFilterModel::data(const QModelIndex &index, int role) const +{ + return QAbstractProxyModel::data(index, role); +} + +void HistoryFilterModel::setSourceModel(QAbstractItemModel *newSourceModel) +{ + if (sourceModel()) { + disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); + disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex))); + disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsInserted(QModelIndex,int,int))); + disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + } + + QAbstractProxyModel::setSourceModel(newSourceModel); + + if (sourceModel()) { + m_loaded = false; + connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); + connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(sourceDataChanged(QModelIndex,QModelIndex))); + connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsInserted(QModelIndex,int,int))); + connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + } +} + +void HistoryFilterModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight)); +} + +QVariant HistoryFilterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return sourceModel()->headerData(section, orientation, role); +} + +void HistoryFilterModel::sourceReset() +{ + m_loaded = false; + beginResetModel(); + endResetModel(); +} + +int HistoryFilterModel::rowCount(const QModelIndex &parent) const +{ + load(); + if (parent.isValid()) + return 0; + return m_historyHash.count(); +} + +int HistoryFilterModel::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 2; +} + +QModelIndex HistoryFilterModel::mapToSource(const QModelIndex &proxyIndex) const +{ + load(); + int sourceRow = sourceModel()->rowCount() - proxyIndex.internalId(); + return sourceModel()->index(sourceRow, proxyIndex.column()); +} + +QModelIndex HistoryFilterModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + load(); + QString url = sourceIndex.data(HistoryModel::UrlStringRole).toString(); + if (!m_historyHash.contains(url)) + return QModelIndex(); + + // This can be done in a binary search, but we can't use qBinary find + // because it can't take: qBinaryFind(m_sourceRow.end(), m_sourceRow.begin(), v); + // so if this is a performance bottlneck then convert to binary search, until then + // the cleaner/easier to read code wins the day. + int realRow = -1; + int sourceModelRow = sourceModel()->rowCount() - sourceIndex.row(); + + for (int i = 0; i < m_sourceRow.count(); ++i) { + if (m_sourceRow.at(i) == sourceModelRow) { + realRow = i; + break; + } + } + if (realRow == -1) + return QModelIndex(); + + return createIndex(realRow, sourceIndex.column(), sourceModel()->rowCount() - sourceIndex.row()); +} + +QModelIndex HistoryFilterModel::index(int row, int column, const QModelIndex &parent) const +{ + load(); + if (row < 0 || row >= rowCount(parent) + || column < 0 || column >= columnCount(parent)) + return QModelIndex(); + + return createIndex(row, column, m_sourceRow[row]); +} + +QModelIndex HistoryFilterModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +void HistoryFilterModel::load() const +{ + if (m_loaded) + return; + m_sourceRow.clear(); + m_historyHash.clear(); + m_historyHash.reserve(sourceModel()->rowCount()); + for (int i = 0; i < sourceModel()->rowCount(); ++i) { + QModelIndex idx = sourceModel()->index(i, 0); + QString url = idx.data(HistoryModel::UrlStringRole).toString(); + if (!m_historyHash.contains(url)) { + m_sourceRow.append(sourceModel()->rowCount() - i); + m_historyHash[url] = sourceModel()->rowCount() - i; + } + } + m_loaded = true; +} + +void HistoryFilterModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + Q_ASSERT(start == end && start == 0); + Q_UNUSED(end); + if (!m_loaded) + return; + QModelIndex idx = sourceModel()->index(start, 0, parent); + QString url = idx.data(HistoryModel::UrlStringRole).toString(); + if (m_historyHash.contains(url)) { + int sourceRow = sourceModel()->rowCount() - m_historyHash[url]; + int realRow = mapFromSource(sourceModel()->index(sourceRow, 0)).row(); + beginRemoveRows(QModelIndex(), realRow, realRow); + m_sourceRow.removeAt(realRow); + m_historyHash.remove(url); + endRemoveRows(); + } + beginInsertRows(QModelIndex(), 0, 0); + m_historyHash.insert(url, sourceModel()->rowCount() - start); + m_sourceRow.insert(0, sourceModel()->rowCount()); + endInsertRows(); +} + +void HistoryFilterModel::sourceRowsRemoved(const QModelIndex &, int start, int end) +{ + Q_UNUSED(start); + Q_UNUSED(end); + sourceReset(); +} + +/* + Removing a continuous block of rows will remove filtered rows too as this is + the users intention. +*/ +bool HistoryFilterModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || count <= 0 || row + count > rowCount(parent) || parent.isValid()) + return false; + int lastRow = row + count - 1; + disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + beginRemoveRows(parent, row, lastRow); + int oldCount = rowCount(); + int start = sourceModel()->rowCount() - m_sourceRow.value(row); + int end = sourceModel()->rowCount() - m_sourceRow.value(lastRow); + sourceModel()->removeRows(start, end - start + 1); + endRemoveRows(); + connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + m_loaded = false; + if (oldCount - count != rowCount()) { + beginResetModel(); + endResetModel(); + } + return true; +} + +HistoryCompletionModel::HistoryCompletionModel(QObject *parent) + : QAbstractProxyModel(parent) +{ +} + +QVariant HistoryCompletionModel::data(const QModelIndex &index, int role) const +{ + if (sourceModel() + && (role == Qt::EditRole || role == Qt::DisplayRole) + && index.isValid()) { + QModelIndex idx = mapToSource(index); + idx = idx.sibling(idx.row(), 1); + QString urlString = idx.data(HistoryModel::UrlStringRole).toString(); + if (index.row() % 2) { + QUrl url = urlString; + QString s = url.toString(QUrl::RemoveScheme + | QUrl::RemoveUserInfo + | QUrl::StripTrailingSlash); + return s.mid(2); // strip // from the front + } + return urlString; + } + return QAbstractProxyModel::data(index, role); +} + +int HistoryCompletionModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid() || !sourceModel()) ? 0 : sourceModel()->rowCount(parent) * 2; +} + +int HistoryCompletionModel::columnCount(const QModelIndex &parent) const +{ + return (parent.isValid()) ? 0 : 1; +} + +QModelIndex HistoryCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + int row = sourceIndex.row() * 2; + return index(row, sourceIndex.column()); +} + +QModelIndex HistoryCompletionModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!sourceModel()) + return QModelIndex(); + int row = proxyIndex.row() / 2; + return sourceModel()->index(row, proxyIndex.column()); +} + +QModelIndex HistoryCompletionModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || row >= rowCount(parent) + || column < 0 || column >= columnCount(parent)) + return QModelIndex(); + return createIndex(row, column); +} + +QModelIndex HistoryCompletionModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +void HistoryCompletionModel::setSourceModel(QAbstractItemModel *newSourceModel) +{ + if (sourceModel()) { + disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); + disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceReset())); + disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceReset())); + } + + QAbstractProxyModel::setSourceModel(newSourceModel); + + if (newSourceModel) { + connect(newSourceModel, SIGNAL(modelReset()), this, SLOT(sourceReset())); + connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceReset())); + connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceReset())); + } + + beginResetModel(); + endResetModel(); +} + +void HistoryCompletionModel::sourceReset() +{ + beginResetModel(); + endResetModel(); +} + +HistoryTreeModel::HistoryTreeModel(QAbstractItemModel *sourceModel, QObject *parent) + : QAbstractProxyModel(parent) +{ + setSourceModel(sourceModel); +} + +QVariant HistoryTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return sourceModel()->headerData(section, orientation, role); +} + +QVariant HistoryTreeModel::data(const QModelIndex &index, int role) const +{ + if ((role == Qt::EditRole || role == Qt::DisplayRole)) { + int start = index.internalId(); + if (start == 0) { + int offset = sourceDateRow(index.row()); + if (index.column() == 0) { + QModelIndex idx = sourceModel()->index(offset, 0); + QDate date = idx.data(HistoryModel::DateRole).toDate(); + if (date == QDate::currentDate()) + return tr("Earlier Today"); + return date.toString(QLatin1String("dddd, MMMM d, yyyy")); + } + if (index.column() == 1) { + return tr("%1 items").arg(rowCount(index.sibling(index.row(), 0))); + } + } + } + if (role == Qt::DecorationRole && index.column() == 0 && !index.parent().isValid()) + return QIcon(QLatin1String(":history.png")); + if (role == HistoryModel::DateRole && index.column() == 0 && index.internalId() == 0) { + int offset = sourceDateRow(index.row()); + QModelIndex idx = sourceModel()->index(offset, 0); + return idx.data(HistoryModel::DateRole); + } + + return QAbstractProxyModel::data(index, role); +} + +int HistoryTreeModel::columnCount(const QModelIndex &parent) const +{ + return sourceModel()->columnCount(mapToSource(parent)); +} + +int HistoryTreeModel::rowCount(const QModelIndex &parent) const +{ + if ( parent.internalId() != 0 + || parent.column() > 0 + || !sourceModel()) + return 0; + + // row count OF dates + if (!parent.isValid()) { + if (!m_sourceRowCache.isEmpty()) + return m_sourceRowCache.count(); + QDate currentDate; + int rows = 0; + int totalRows = sourceModel()->rowCount(); + + for (int i = 0; i < totalRows; ++i) { + QDate rowDate = sourceModel()->index(i, 0).data(HistoryModel::DateRole).toDate(); + if (rowDate != currentDate) { + m_sourceRowCache.append(i); + currentDate = rowDate; + ++rows; + } + } + Q_ASSERT(m_sourceRowCache.count() == rows); + return rows; + } + + // row count FOR a date + int start = sourceDateRow(parent.row()); + int end = sourceDateRow(parent.row() + 1); + return (end - start); +} + +// Translate the top level date row into the offset where that date starts +int HistoryTreeModel::sourceDateRow(int row) const +{ + if (row <= 0) + return 0; + + if (m_sourceRowCache.isEmpty()) + rowCount(QModelIndex()); + + if (row >= m_sourceRowCache.count()) { + if (!sourceModel()) + return 0; + return sourceModel()->rowCount(); + } + return m_sourceRowCache.at(row); +} + +QModelIndex HistoryTreeModel::mapToSource(const QModelIndex &proxyIndex) const +{ + int offset = proxyIndex.internalId(); + if (offset == 0) + return QModelIndex(); + int startDateRow = sourceDateRow(offset - 1); + return sourceModel()->index(startDateRow + proxyIndex.row(), proxyIndex.column()); +} + +QModelIndex HistoryTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 + || column < 0 || column >= columnCount(parent) + || parent.column() > 0) + return QModelIndex(); + + if (!parent.isValid()) + return createIndex(row, column); + return createIndex(row, column, parent.row() + 1); +} + +QModelIndex HistoryTreeModel::parent(const QModelIndex &index) const +{ + int offset = index.internalId(); + if (offset == 0 || !index.isValid()) + return QModelIndex(); + return createIndex(offset - 1, 0); +} + +bool HistoryTreeModel::hasChildren(const QModelIndex &parent) const +{ + QModelIndex grandparent = parent.parent(); + if (!grandparent.isValid()) + return true; + return false; +} + +Qt::ItemFlags HistoryTreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; +} + +bool HistoryTreeModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || count <= 0 || row + count > rowCount(parent)) + return false; + + if (parent.isValid()) { + // removing pages + int offset = sourceDateRow(parent.row()); + return sourceModel()->removeRows(offset + row, count); + } else { + // removing whole dates + for (int i = row + count - 1; i >= row; --i) { + QModelIndex dateParent = index(i, 0); + int offset = sourceDateRow(dateParent.row()); + if (!sourceModel()->removeRows(offset, rowCount(dateParent))) + return false; + } + } + return true; +} + +void HistoryTreeModel::setSourceModel(QAbstractItemModel *newSourceModel) +{ + if (sourceModel()) { + disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); + disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); + disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsInserted(QModelIndex,int,int))); + disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + } + + QAbstractProxyModel::setSourceModel(newSourceModel); + + if (newSourceModel) { + connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(sourceReset())); + connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(sourceReset())); + connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(sourceRowsInserted(QModelIndex,int,int))); + connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); + } + + beginResetModel(); + endResetModel(); +} + +void HistoryTreeModel::sourceReset() +{ + beginResetModel(); + m_sourceRowCache.clear(); + endResetModel(); +} + +void HistoryTreeModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(parent); // Avoid warnings when compiling release + Q_ASSERT(!parent.isValid()); + if (start != 0 || start != end) { + beginResetModel(); + m_sourceRowCache.clear(); + endResetModel(); + return; + } + + m_sourceRowCache.clear(); + QModelIndex treeIndex = mapFromSource(sourceModel()->index(start, 0)); + QModelIndex treeParent = treeIndex.parent(); + if (rowCount(treeParent) == 1) { + beginInsertRows(QModelIndex(), 0, 0); + endInsertRows(); + } else { + beginInsertRows(treeParent, treeIndex.row(), treeIndex.row()); + endInsertRows(); + } +} + +QModelIndex HistoryTreeModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if (!sourceIndex.isValid()) + return QModelIndex(); + + if (m_sourceRowCache.isEmpty()) + rowCount(QModelIndex()); + + QList<int>::iterator it; + it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), sourceIndex.row()); + if (*it != sourceIndex.row()) + --it; + int dateRow = qMax(0, it - m_sourceRowCache.begin()); + int row = sourceIndex.row() - m_sourceRowCache.at(dateRow); + return createIndex(row, sourceIndex.column(), dateRow + 1); +} + +void HistoryTreeModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end) +{ + Q_UNUSED(parent); // Avoid warnings when compiling release + Q_ASSERT(!parent.isValid()); + if (m_sourceRowCache.isEmpty()) + return; + for (int i = end; i >= start;) { + QList<int>::iterator it; + it = qLowerBound(m_sourceRowCache.begin(), m_sourceRowCache.end(), i); + // playing it safe + if (it == m_sourceRowCache.end()) { + beginResetModel(); + m_sourceRowCache.clear(); + endResetModel(); + return; + } + + if (*it != i) + --it; + int row = qMax(0, it - m_sourceRowCache.begin()); + int offset = m_sourceRowCache[row]; + QModelIndex dateParent = index(row, 0); + // If we can remove all the rows in the date do that and skip over them + int rc = rowCount(dateParent); + if (i - rc + 1 == offset && start <= i - rc + 1) { + beginRemoveRows(QModelIndex(), row, row); + m_sourceRowCache.removeAt(row); + i -= rc + 1; + } else { + beginRemoveRows(dateParent, i - offset, i - offset); + ++row; + --i; + } + for (int j = row; j < m_sourceRowCache.count(); ++j) + --m_sourceRowCache[j]; + endRemoveRows(); + } +} diff --git a/examples/widgets/browser/history.h b/examples/widgets/browser/history.h new file mode 100644 index 000000000..e8764f2ff --- /dev/null +++ b/examples/widgets/browser/history.h @@ -0,0 +1,349 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef HISTORY_H +#define HISTORY_H + +#include "modelmenu.h" + +#include <QtCore/QDateTime> +#include <QtCore/QHash> +#include <QtCore/QObject> +#include <QtCore/QTimer> +#include <QtCore/QUrl> + +#include <QtCore/QSortFilterProxyModel> + +#include <QWebHistoryInterface> + +class HistoryItem +{ +public: + HistoryItem() {} + HistoryItem(const QString &u, + const QDateTime &d = QDateTime(), const QString &t = QString()) + : title(t), url(u), dateTime(d) {} + + inline bool operator==(const HistoryItem &other) const + { return other.title == title + && other.url == url && other.dateTime == dateTime; } + + // history is sorted in reverse + inline bool operator <(const HistoryItem &other) const + { return dateTime > other.dateTime; } + + QString title; + QString url; + QDateTime dateTime; +}; + +class AutoSaver; +class HistoryModel; +class HistoryFilterModel; +class HistoryTreeModel; +class HistoryManager : public QWebHistoryInterface +{ + Q_OBJECT + Q_PROPERTY(int historyLimit READ historyLimit WRITE setHistoryLimit) + +signals: + void historyReset(); + void entryAdded(const HistoryItem &item); + void entryRemoved(const HistoryItem &item); + void entryUpdated(int offset); + +public: + HistoryManager(QObject *parent = 0); + ~HistoryManager(); + + bool historyContains(const QString &url) const; + void addHistoryEntry(const QString &url); + + void updateHistoryItem(const QUrl &url, const QString &title); + + int historyLimit() const; + void setHistoryLimit(int limit); + + QList<HistoryItem> history() const; + void setHistory(const QList<HistoryItem> &history, bool loadedAndSorted = false); + + // History manager keeps around these models for use by the completer and other classes + HistoryModel *historyModel() const; + HistoryFilterModel *historyFilterModel() const; + HistoryTreeModel *historyTreeModel() const; + +public slots: + void clear(); + void loadSettings(); + +private slots: + void save(); + void checkForExpired(); + +protected: + void addHistoryItem(const HistoryItem &item); + +private: + void load(); + + AutoSaver *m_saveTimer; + int m_historyLimit; + QTimer m_expiredTimer; + QList<HistoryItem> m_history; + QString m_lastSavedUrl; + + HistoryModel *m_historyModel; + HistoryFilterModel *m_historyFilterModel; + HistoryTreeModel *m_historyTreeModel; +}; + +class HistoryModel : public QAbstractTableModel +{ + Q_OBJECT + +public slots: + void historyReset(); + void entryAdded(); + void entryUpdated(int offset); + +public: + enum Roles { + DateRole = Qt::UserRole + 1, + DateTimeRole = Qt::UserRole + 2, + UrlRole = Qt::UserRole + 3, + UrlStringRole = Qt::UserRole + 4 + }; + + HistoryModel(HistoryManager *history, QObject *parent = 0); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + +private: + HistoryManager *m_history; +}; + +/*! + Proxy model that will remove any duplicate entries. + Both m_sourceRow and m_historyHash store their offsets not from + the front of the list, but as offsets from the back. + */ +class HistoryFilterModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + HistoryFilterModel(QAbstractItemModel *sourceModel, QObject *parent = 0); + + inline bool historyContains(const QString &url) const + { load(); return m_historyHash.contains(url); } + int historyLocation(const QString &url) const; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const; + void setSourceModel(QAbstractItemModel *sourceModel); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex index(int, int, const QModelIndex& = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index= QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + +private slots: + void sourceReset(); + void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsRemoved(const QModelIndex &, int, int); + +private: + void load() const; + + mutable QList<int> m_sourceRow; + mutable QHash<QString, int> m_historyHash; + mutable bool m_loaded; +}; + +/* + The history menu + - Removes the first twenty entries and puts them as children of the top level. + - If there are less then twenty entries then the first folder is also removed. + + The mapping is done by knowing that HistoryTreeModel is over a table + We store that row offset in our index's private data. +*/ +class HistoryMenuModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + HistoryMenuModel(HistoryTreeModel *sourceModel, QObject *parent = 0); + int columnCount(const QModelIndex &parent) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex mapFromSource(const QModelIndex & sourceIndex) const; + QModelIndex mapToSource(const QModelIndex & proxyIndex) const; + QModelIndex index(int, int, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index = QModelIndex()) const; + + int bumpedRows() const; + +private: + HistoryTreeModel *m_treeModel; +}; + +// Menu that is dynamically populated from the history +class HistoryMenu : public ModelMenu +{ + Q_OBJECT + +signals: + void openUrl(const QUrl &url); + +public: + HistoryMenu(QWidget *parent = 0); + void setInitialActions(QList<QAction*> actions); + +protected: + bool prePopulated(); + void postPopulated(); + +private slots: + void activated(const QModelIndex &index); + void showHistoryDialog(); + +private: + HistoryManager *m_history; + HistoryMenuModel *m_historyMenuModel; + QList<QAction*> m_initialActions; +}; + +// proxy model for the history model that +// exposes each url http://www.foo.com and it url starting at the host www.foo.com +class HistoryCompletionModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + HistoryCompletionModel(QObject *parent = 0); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const; + QModelIndex index(int, int, const QModelIndex& = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index= QModelIndex()) const; + void setSourceModel(QAbstractItemModel *sourceModel); + +private slots: + void sourceReset(); + +}; + +// proxy model for the history model that converts the list +// into a tree, one top level node per day. +// Used in the HistoryDialog. +class HistoryTreeModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + HistoryTreeModel(QAbstractItemModel *sourceModel, QObject *parent = 0); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int columnCount(const QModelIndex &parent) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index= QModelIndex()) const; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + + void setSourceModel(QAbstractItemModel *sourceModel); + +private slots: + void sourceReset(); + void sourceRowsInserted(const QModelIndex &parent, int start, int end); + void sourceRowsRemoved(const QModelIndex &parent, int start, int end); + +private: + int sourceDateRow(int row) const; + mutable QList<int> m_sourceRowCache; + +}; + +// A modified QSortFilterProxyModel that always accepts the root nodes in the tree +// so filtering is only done on the children. +// Used in the HistoryDialog +class TreeProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + TreeProxyModel(QObject *parent = 0); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; +}; + +#include "ui_history.h" + +class HistoryDialog : public QDialog, public Ui_HistoryDialog +{ + Q_OBJECT + +signals: + void openUrl(const QUrl &url); + +public: + HistoryDialog(QWidget *parent = 0, HistoryManager *history = 0); + +private slots: + void customContextMenuRequested(const QPoint &pos); + void open(); + void copy(); + +}; + +#endif // HISTORY_H diff --git a/examples/widgets/browser/history.ui b/examples/widgets/browser/history.ui new file mode 100644 index 000000000..0944940e7 --- /dev/null +++ b/examples/widgets/browser/history.ui @@ -0,0 +1,106 @@ +<ui version="4.0" > + <class>HistoryDialog</class> + <widget class="QDialog" name="HistoryDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>758</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle" > + <string>History</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <item row="0" column="0" > + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>252</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1" > + <widget class="SearchLineEdit" name="search" /> + </item> + <item row="1" column="0" colspan="2" > + <widget class="EditTreeView" name="tree" /> + </item> + <item row="2" column="0" colspan="2" > + <layout class="QHBoxLayout" > + <item> + <widget class="QPushButton" name="removeButton" > + <property name="text" > + <string>&Remove</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeAllButton" > + <property name="text" > + <string>Remove &All</string> + </property> + </widget> + </item> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="standardButtons" > + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>SearchLineEdit</class> + <extends>QLineEdit</extends> + <header>searchlineedit.h</header> + </customwidget> + <customwidget> + <class>EditTreeView</class> + <extends>QTreeView</extends> + <header>edittreeview.h</header> + </customwidget> + </customwidgets> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>HistoryDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>472</x> + <y>329</y> + </hint> + <hint type="destinationlabel" > + <x>461</x> + <y>356</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/htmls/htmls.qrc b/examples/widgets/browser/htmls/htmls.qrc new file mode 100644 index 000000000..03b256ccb --- /dev/null +++ b/examples/widgets/browser/htmls/htmls.qrc @@ -0,0 +1,5 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>notfound.html</file> +</qresource> +</RCC> diff --git a/examples/widgets/browser/htmls/notfound.html b/examples/widgets/browser/htmls/notfound.html new file mode 100644 index 000000000..e89845aa6 --- /dev/null +++ b/examples/widgets/browser/htmls/notfound.html @@ -0,0 +1,63 @@ +<html> +<head> +<title>%1</title> +<style> +body { + padding: 3em 0em; + background: #eeeeee; +} +hr { + color: lightgray; + width: 100%; +} +img { + float: left; + opacity: .8; +} +#box { + background: white; + border: 1px solid lightgray; + width: 600px; + padding: 60px; + margin: auto; +} +h1 { + font-size: 130%; + font-weight: bold; + border-bottom: 1px solid lightgray; + margin-left: 48px; +} +h2 { + font-size: 100%; + font-weight: normal; + border-bottom: 1px solid lightgray; + margin-left: 48px; +} +ul { + font-size: 80%; + padding-left: 48px; + margin: 0; +} +#reloadButton { + padding-left: 48px; +} +</style> +</head> +<body> + <div id="box"> + <img src="data:image/png;base64,IMAGE_BINARY_DATA_HERE" width="32" height="32"/> + <h1>%2</h1> + <h2>When connecting to: %3.</h2> + <ul> + <li>Check the address for errors such as <b>ww</b>.example.com + instead of <b>www</b>.example.com</li> + <li>If the address is correct, try checking the network + connection.</li> + <li>If your computer or network is protected by a firewall or + proxy, make sure that the browser demo is permitted to access + the network.</li> + </ul> + <br/><br/> + </div> +</body> +</html> diff --git a/examples/widgets/browser/main.cpp b/examples/widgets/browser/main.cpp new file mode 100644 index 000000000..9baf01f42 --- /dev/null +++ b/examples/widgets/browser/main.cpp @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "browserapplication.h" + +int main(int argc, char **argv) +{ + Q_INIT_RESOURCE(data); + BrowserApplication application(argc, argv); + if (!application.isTheOnlyBrowser()) + return 0; + application.newMainWindow(); + return application.exec(); +} diff --git a/examples/widgets/browser/modelmenu.cpp b/examples/widgets/browser/modelmenu.cpp new file mode 100644 index 000000000..56453b080 --- /dev/null +++ b/examples/widgets/browser/modelmenu.cpp @@ -0,0 +1,226 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modelmenu.h" + +#include <QtCore/QAbstractItemModel> +#include <qdebug.h> + +ModelMenu::ModelMenu(QWidget * parent) + : QMenu(parent) + , m_maxRows(7) + , m_firstSeparator(-1) + , m_maxWidth(-1) + , m_hoverRole(0) + , m_separatorRole(0) + , m_model(0) +{ + connect(this, SIGNAL(aboutToShow()), this, SLOT(aboutToShow())); +} + +bool ModelMenu::prePopulated() +{ + return false; +} + +void ModelMenu::postPopulated() +{ +} + +void ModelMenu::setModel(QAbstractItemModel *model) +{ + m_model = model; +} + +QAbstractItemModel *ModelMenu::model() const +{ + return m_model; +} + +void ModelMenu::setMaxRows(int max) +{ + m_maxRows = max; +} + +int ModelMenu::maxRows() const +{ + return m_maxRows; +} + +void ModelMenu::setFirstSeparator(int offset) +{ + m_firstSeparator = offset; +} + +int ModelMenu::firstSeparator() const +{ + return m_firstSeparator; +} + +void ModelMenu::setRootIndex(const QModelIndex &index) +{ + m_root = index; +} + +QModelIndex ModelMenu::rootIndex() const +{ + return m_root; +} + +void ModelMenu::setHoverRole(int role) +{ + m_hoverRole = role; +} + +int ModelMenu::hoverRole() const +{ + return m_hoverRole; +} + +void ModelMenu::setSeparatorRole(int role) +{ + m_separatorRole = role; +} + +int ModelMenu::separatorRole() const +{ + return m_separatorRole; +} + +Q_DECLARE_METATYPE(QModelIndex) +void ModelMenu::aboutToShow() +{ + if (QMenu *menu = qobject_cast<QMenu*>(sender())) { + QVariant v = menu->menuAction()->data(); + if (v.canConvert<QModelIndex>()) { + QModelIndex idx = qvariant_cast<QModelIndex>(v); + createMenu(idx, -1, menu, menu); + disconnect(menu, SIGNAL(aboutToShow()), this, SLOT(aboutToShow())); + return; + } + } + + clear(); + if (prePopulated()) + addSeparator(); + int max = m_maxRows; + if (max != -1) + max += m_firstSeparator; + createMenu(m_root, max, this, this); + postPopulated(); +} + +void ModelMenu::createMenu(const QModelIndex &parent, int max, QMenu *parentMenu, QMenu *menu) +{ + if (!menu) { + QString title = parent.data().toString(); + menu = new QMenu(title, this); + QIcon icon = qvariant_cast<QIcon>(parent.data(Qt::DecorationRole)); + menu->setIcon(icon); + parentMenu->addMenu(menu); + QVariant v; + v.setValue(parent); + menu->menuAction()->setData(v); + connect(menu, SIGNAL(aboutToShow()), this, SLOT(aboutToShow())); + return; + } + + int end = m_model->rowCount(parent); + if (max != -1) + end = qMin(max, end); + + connect(menu, SIGNAL(triggered(QAction*)), this, SLOT(triggered(QAction*))); + connect(menu, SIGNAL(hovered(QAction*)), this, SLOT(hovered(QAction*))); + + for (int i = 0; i < end; ++i) { + QModelIndex idx = m_model->index(i, 0, parent); + if (m_model->hasChildren(idx)) { + createMenu(idx, -1, menu); + } else { + if (m_separatorRole != 0 + && idx.data(m_separatorRole).toBool()) + addSeparator(); + else + menu->addAction(makeAction(idx)); + } + if (menu == this && i == m_firstSeparator - 1) + addSeparator(); + } +} + +QAction *ModelMenu::makeAction(const QModelIndex &index) +{ + QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole)); + QAction *action = makeAction(icon, index.data().toString(), this); + QVariant v; + v.setValue(index); + action->setData(v); + return action; +} + +QAction *ModelMenu::makeAction(const QIcon &icon, const QString &text, QObject *parent) +{ + QFontMetrics fm(font()); + if (-1 == m_maxWidth) + m_maxWidth = fm.width(QLatin1Char('m')) * 30; + QString smallText = fm.elidedText(text, Qt::ElideMiddle, m_maxWidth); + return new QAction(icon, smallText, parent); +} + +void ModelMenu::triggered(QAction *action) +{ + QVariant v = action->data(); + if (v.canConvert<QModelIndex>()) { + QModelIndex idx = qvariant_cast<QModelIndex>(v); + emit activated(idx); + } +} + +void ModelMenu::hovered(QAction *action) +{ + QVariant v = action->data(); + if (v.canConvert<QModelIndex>()) { + QModelIndex idx = qvariant_cast<QModelIndex>(v); + QString hoveredString = idx.data(m_hoverRole).toString(); + if (!hoveredString.isEmpty()) + emit hovered(hoveredString); + } +} diff --git a/examples/widgets/browser/modelmenu.h b/examples/widgets/browser/modelmenu.h new file mode 100644 index 000000000..4045a7758 --- /dev/null +++ b/examples/widgets/browser/modelmenu.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MODELMENU_H +#define MODELMENU_H + +#include <QtWidgets/QMenu> +#include <QtCore/QAbstractItemModel> + +// A QMenu that is dynamically populated from a QAbstractItemModel +class ModelMenu : public QMenu +{ + Q_OBJECT + +signals: + void activated(const QModelIndex &index); + void hovered(const QString &text); + +public: + ModelMenu(QWidget *parent = 0); + + void setModel(QAbstractItemModel *model); + QAbstractItemModel *model() const; + + void setMaxRows(int max); + int maxRows() const; + + void setFirstSeparator(int offset); + int firstSeparator() const; + + void setRootIndex(const QModelIndex &index); + QModelIndex rootIndex() const; + + void setHoverRole(int role); + int hoverRole() const; + + void setSeparatorRole(int role); + int separatorRole() const; + + QAction *makeAction(const QIcon &icon, const QString &text, QObject *parent); + +protected: + // add any actions before the tree, return true if any actions are added. + virtual bool prePopulated(); + // add any actions after the tree + virtual void postPopulated(); + // put all of the children of parent into menu up to max + void createMenu(const QModelIndex &parent, int max, QMenu *parentMenu = 0, QMenu *menu = 0); + +private slots: + void aboutToShow(); + void triggered(QAction *action); + void hovered(QAction *action); + +private: + QAction *makeAction(const QModelIndex &index); + int m_maxRows; + int m_firstSeparator; + int m_maxWidth; + int m_hoverRole; + int m_separatorRole; + QAbstractItemModel *m_model; + QPersistentModelIndex m_root; +}; + +#endif // MODELMENU_H diff --git a/examples/widgets/browser/networkaccessmanager.cpp b/examples/widgets/browser/networkaccessmanager.cpp new file mode 100644 index 000000000..ae5dfe4de --- /dev/null +++ b/examples/widgets/browser/networkaccessmanager.cpp @@ -0,0 +1,214 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "networkaccessmanager.h" + +#include "browserapplication.h" +#include "browsermainwindow.h" +#include "ui_passworddialog.h" +#include "ui_proxy.h" + +#include <QtCore/QSettings> + +#include <QtGui/QDesktopServices> +#include <QtWidgets/QDialog> +#include <QtWidgets/QMessageBox> +#include <QtWidgets/QStyle> +#include <QtGui/QTextDocument> + +#include <QtNetwork/QAuthenticator> +#include <QtNetwork/QNetworkDiskCache> +#include <QtNetwork/QNetworkProxy> +#include <QtNetwork/QNetworkRequest> +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QSslError> + +NetworkAccessManager::NetworkAccessManager(QObject *parent) + : QNetworkAccessManager(parent), + requestFinishedCount(0), requestFinishedFromCacheCount(0), requestFinishedPipelinedCount(0), + requestFinishedSecureCount(0), requestFinishedDownloadBufferCount(0) +{ + connect(this, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), + SLOT(authenticationRequired(QNetworkReply*,QAuthenticator*))); + connect(this, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), + SLOT(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*))); + connect(this, SIGNAL(finished(QNetworkReply*)), + SLOT(requestFinished(QNetworkReply*))); +#ifndef QT_NO_OPENSSL + connect(this, SIGNAL(sslErrors(QNetworkReply*,QList<QSslError>)), + SLOT(sslErrors(QNetworkReply*,QList<QSslError>))); +#endif + loadSettings(); + + QNetworkDiskCache *diskCache = new QNetworkDiskCache(this); + QString location = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + diskCache->setCacheDirectory(location); + setCache(diskCache); +} + +QNetworkReply* NetworkAccessManager::createRequest(Operation op, const QNetworkRequest & req, QIODevice * outgoingData) +{ + QNetworkRequest request = req; // copy so we can modify + // this is a temporary hack until we properly use the pipelining flags from QtWebkit + // pipeline everything! :) + request.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); + return QNetworkAccessManager::createRequest(op, request, outgoingData); +} + +void NetworkAccessManager::requestFinished(QNetworkReply *reply) +{ + requestFinishedCount++; + + if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool() == true) + requestFinishedFromCacheCount++; + + if (reply->attribute(QNetworkRequest::HttpPipeliningWasUsedAttribute).toBool() == true) + requestFinishedPipelinedCount++; + + if (reply->attribute(QNetworkRequest::ConnectionEncryptedAttribute).toBool() == true) + requestFinishedSecureCount++; + + if (reply->attribute(QNetworkRequest::DownloadBufferAttribute).isValid() == true) + requestFinishedDownloadBufferCount++; + + if (requestFinishedCount % 10) + return; + +#ifdef QT_DEBUG + double pctCached = (double(requestFinishedFromCacheCount) * 100.0/ double(requestFinishedCount)); + double pctPipelined = (double(requestFinishedPipelinedCount) * 100.0/ double(requestFinishedCount)); + double pctSecure = (double(requestFinishedSecureCount) * 100.0/ double(requestFinishedCount)); + double pctDownloadBuffer = (double(requestFinishedDownloadBufferCount) * 100.0/ double(requestFinishedCount)); + + qDebug("STATS [%lli requests total] [%3.2f%% from cache] [%3.2f%% pipelined] [%3.2f%% SSL/TLS] [%3.2f%% Zerocopy]", requestFinishedCount, pctCached, pctPipelined, pctSecure, pctDownloadBuffer); +#endif +} + +void NetworkAccessManager::loadSettings() +{ + QSettings settings; + settings.beginGroup(QLatin1String("proxy")); + QNetworkProxy proxy; + if (settings.value(QLatin1String("enabled"), false).toBool()) { + if (settings.value(QLatin1String("type"), 0).toInt() == 0) + proxy = QNetworkProxy::Socks5Proxy; + else + proxy = QNetworkProxy::HttpProxy; + proxy.setHostName(settings.value(QLatin1String("hostName")).toString()); + proxy.setPort(settings.value(QLatin1String("port"), 1080).toInt()); + proxy.setUser(settings.value(QLatin1String("userName")).toString()); + proxy.setPassword(settings.value(QLatin1String("password")).toString()); + } + setProxy(proxy); +} + +void NetworkAccessManager::authenticationRequired(QNetworkReply *reply, QAuthenticator *auth) +{ + BrowserMainWindow *mainWindow = BrowserApplication::instance()->mainWindow(); + + QDialog dialog(mainWindow); + dialog.setWindowFlags(Qt::Sheet); + + Ui::PasswordDialog passwordDialog; + passwordDialog.setupUi(&dialog); + + passwordDialog.iconLabel->setText(QString()); + passwordDialog.iconLabel->setPixmap(mainWindow->style()->standardIcon(QStyle::SP_MessageBoxQuestion, 0, mainWindow).pixmap(32, 32)); + + QString introMessage = tr("<qt>Enter username and password for \"%1\" at %2</qt>"); + introMessage = introMessage.arg(reply->url().toString().toHtmlEscaped()).arg(reply->url().toString().toHtmlEscaped()); + passwordDialog.introLabel->setText(introMessage); + passwordDialog.introLabel->setWordWrap(true); + + if (dialog.exec() == QDialog::Accepted) { + auth->setUser(passwordDialog.userNameLineEdit->text()); + auth->setPassword(passwordDialog.passwordLineEdit->text()); + } +} + +void NetworkAccessManager::proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *auth) +{ + BrowserMainWindow *mainWindow = BrowserApplication::instance()->mainWindow(); + + QDialog dialog(mainWindow); + dialog.setWindowFlags(Qt::Sheet); + + Ui::ProxyDialog proxyDialog; + proxyDialog.setupUi(&dialog); + + proxyDialog.iconLabel->setText(QString()); + proxyDialog.iconLabel->setPixmap(mainWindow->style()->standardIcon(QStyle::SP_MessageBoxQuestion, 0, mainWindow).pixmap(32, 32)); + + QString introMessage = tr("<qt>Connect to proxy \"%1\" using:</qt>"); + introMessage = introMessage.arg(proxy.hostName().toHtmlEscaped()); + proxyDialog.introLabel->setText(introMessage); + proxyDialog.introLabel->setWordWrap(true); + + if (dialog.exec() == QDialog::Accepted) { + auth->setUser(proxyDialog.userNameLineEdit->text()); + auth->setPassword(proxyDialog.passwordLineEdit->text()); + } +} + +#ifndef QT_NO_OPENSSL +void NetworkAccessManager::sslErrors(QNetworkReply *reply, const QList<QSslError> &error) +{ + // check if SSL certificate has been trusted already + QString replyHost = reply->url().host() + QString(":%1").arg(reply->url().port()); + if (! sslTrustedHostList.contains(replyHost)) { + BrowserMainWindow *mainWindow = BrowserApplication::instance()->mainWindow(); + + QStringList errorStrings; + for (int i = 0; i < error.count(); ++i) + errorStrings += error.at(i).errorString(); + QString errors = errorStrings.join(QLatin1String("\n")); + int ret = QMessageBox::warning(mainWindow, QCoreApplication::applicationName(), + tr("SSL Errors:\n\n%1\n\n%2\n\n" + "Do you want to ignore these errors for this host?").arg(reply->url().toString()).arg(errors), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (ret == QMessageBox::Yes) { + reply->ignoreSslErrors(); + sslTrustedHostList.append(replyHost); + } + } +} +#endif diff --git a/examples/widgets/browser/networkaccessmanager.h b/examples/widgets/browser/networkaccessmanager.h new file mode 100644 index 000000000..8a7aaf808 --- /dev/null +++ b/examples/widgets/browser/networkaccessmanager.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef NETWORKACCESSMANAGER_H +#define NETWORKACCESSMANAGER_H + +#include <QtNetwork/QNetworkAccessManager> +#include <QtNetwork/QNetworkRequest> + +class NetworkAccessManager : public QNetworkAccessManager +{ + Q_OBJECT + +public: + NetworkAccessManager(QObject *parent = 0); + + virtual QNetworkReply* createRequest ( Operation op, const QNetworkRequest & req, QIODevice * outgoingData = 0 ); + +private: + QList<QString> sslTrustedHostList; + qint64 requestFinishedCount; + qint64 requestFinishedFromCacheCount; + qint64 requestFinishedPipelinedCount; + qint64 requestFinishedSecureCount; + qint64 requestFinishedDownloadBufferCount; + +public slots: + void loadSettings(); + void requestFinished(QNetworkReply *reply); + +private slots: + void authenticationRequired(QNetworkReply *reply, QAuthenticator *auth); + void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *auth); +#ifndef QT_NO_OPENSSL + void sslErrors(QNetworkReply *reply, const QList<QSslError> &error); +#endif +}; + +#endif // NETWORKACCESSMANAGER_H diff --git a/examples/widgets/browser/passworddialog.ui b/examples/widgets/browser/passworddialog.ui new file mode 100644 index 000000000..7c1665867 --- /dev/null +++ b/examples/widgets/browser/passworddialog.ui @@ -0,0 +1,111 @@ +<ui version="4.0" > + <class>PasswordDialog</class> + <widget class="QDialog" name="PasswordDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>399</width> + <height>148</height> + </rect> + </property> + <property name="windowTitle" > + <string>Authentication Required</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <item row="0" column="0" colspan="2" > + <layout class="QHBoxLayout" > + <item> + <widget class="QLabel" name="iconLabel" > + <property name="text" > + <string>DUMMY ICON</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="introLabel" > + <property name="sizePolicy" > + <sizepolicy vsizetype="MinimumExpanding" hsizetype="MinimumExpanding" > + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text" > + <string>INTRO TEXT DUMMY</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label" > + <property name="text" > + <string>Username:</string> + </property> + </widget> + </item> + <item row="1" column="1" > + <widget class="QLineEdit" name="userNameLineEdit" /> + </item> + <item row="2" column="0" > + <widget class="QLabel" name="lblPassword" > + <property name="text" > + <string>Password:</string> + </property> + </widget> + </item> + <item row="2" column="1" > + <widget class="QLineEdit" name="passwordLineEdit" > + <property name="echoMode" > + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2" > + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PasswordDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PasswordDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/proxy.ui b/examples/widgets/browser/proxy.ui new file mode 100644 index 000000000..62a8be627 --- /dev/null +++ b/examples/widgets/browser/proxy.ui @@ -0,0 +1,104 @@ +<ui version="4.0" > + <class>ProxyDialog</class> + <widget class="QDialog" name="ProxyDialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>369</width> + <height>144</height> + </rect> + </property> + <property name="windowTitle" > + <string>Proxy Authentication</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <item row="0" column="0" > + <widget class="QLabel" name="iconLabel" > + <property name="text" > + <string>ICON</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2" > + <widget class="QLabel" name="introLabel" > + <property name="text" > + <string>Connect to proxy</string> + </property> + <property name="wordWrap" > + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2" > + <widget class="QLabel" name="usernameLabel" > + <property name="text" > + <string>Username:</string> + </property> + </widget> + </item> + <item row="1" column="2" > + <widget class="QLineEdit" name="userNameLineEdit" /> + </item> + <item row="2" column="0" colspan="2" > + <widget class="QLabel" name="passwordLabel" > + <property name="text" > + <string>Password:</string> + </property> + </widget> + </item> + <item row="2" column="2" > + <widget class="QLineEdit" name="passwordLineEdit" > + <property name="echoMode" > + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="3" column="0" colspan="3" > + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ProxyDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ProxyDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/searchlineedit.cpp b/examples/widgets/browser/searchlineedit.cpp new file mode 100644 index 000000000..0d387da4b --- /dev/null +++ b/examples/widgets/browser/searchlineedit.cpp @@ -0,0 +1,240 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "searchlineedit.h" + +#include <QtGui/QPainter> +#include <QtGui/QMouseEvent> +#include <QtWidgets/QMenu> +#include <QtWidgets/QStyle> +#include <QtWidgets/QStyleOptionFrameV2> + +ClearButton::ClearButton(QWidget *parent) + : QAbstractButton(parent) +{ +#ifndef QT_NO_CURSOR + setCursor(Qt::ArrowCursor); +#endif // QT_NO_CURSOR + setToolTip(tr("Clear")); + setVisible(false); + setFocusPolicy(Qt::NoFocus); +} + +void ClearButton::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + int height = this->height(); + + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setBrush(isDown() + ? palette().color(QPalette::Dark) + : palette().color(QPalette::Mid)); + painter.setPen(painter.brush().color()); + int size = width(); + int offset = size / 5; + int radius = size - offset * 2; + painter.drawEllipse(offset, offset, radius, radius); + + painter.setPen(palette().color(QPalette::Base)); + int border = offset * 2; + painter.drawLine(border, border, width() - border, height - border); + painter.drawLine(border, height - border, width() - border, border); +} + +void ClearButton::textChanged(const QString &text) +{ + setVisible(!text.isEmpty()); +} + +/* + Search icon on the left hand side of the search widget + When a menu is set a down arrow appears + */ +class SearchButton : public QAbstractButton { +public: + SearchButton(QWidget *parent = 0); + void paintEvent(QPaintEvent *event); + QMenu *m_menu; + +protected: + void mousePressEvent(QMouseEvent *event); +}; + +SearchButton::SearchButton(QWidget *parent) + : QAbstractButton(parent), + m_menu(0) +{ + setObjectName(QLatin1String("SearchButton")); +#ifndef QT_NO_CURSOR + setCursor(Qt::ArrowCursor); +#endif //QT_NO_CURSOR + setFocusPolicy(Qt::NoFocus); +} + +void SearchButton::mousePressEvent(QMouseEvent *event) +{ + if (m_menu && event->button() == Qt::LeftButton) { + QWidget *p = parentWidget(); + if (p) { + QPoint r = p->mapToGlobal(QPoint(0, p->height())); + m_menu->exec(QPoint(r.x() + height() / 2, r.y())); + } + event->accept(); + } + QAbstractButton::mousePressEvent(event); +} + +void SearchButton::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainterPath myPath; + + int radius = (height() / 5) * 2; + QRect circle(height() / 3 - 1, height() / 4, radius, radius); + myPath.addEllipse(circle); + + myPath.arcMoveTo(circle, 300); + QPointF c = myPath.currentPosition(); + int diff = height() / 7; + myPath.lineTo(qMin(width() - 2, (int)c.x() + diff), c.y() + diff); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(QPen(Qt::darkGray, 2)); + painter.drawPath(myPath); + + if (m_menu) { + QPainterPath dropPath; + dropPath.arcMoveTo(circle, 320); + QPointF c = dropPath.currentPosition(); + c = QPointF(c.x() + 3.5, c.y() + 0.5); + dropPath.moveTo(c); + dropPath.lineTo(c.x() + 4, c.y()); + dropPath.lineTo(c.x() + 2, c.y() + 2); + dropPath.closeSubpath(); + painter.setPen(Qt::darkGray); + painter.setBrush(Qt::darkGray); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawPath(dropPath); + } + painter.end(); +} + +/* + SearchLineEdit is an enhanced QLineEdit + - A Search icon on the left with optional menu + - When there is no text and doesn't have focus an "inactive text" is displayed + - When there is text a clear button is displayed on the right hand side + */ +SearchLineEdit::SearchLineEdit(QWidget *parent) : ExLineEdit(parent), + m_searchButton(new SearchButton(this)) +{ + connect(lineEdit(), SIGNAL(textChanged(QString)), + this, SIGNAL(textChanged(QString))); + setLeftWidget(m_searchButton); + m_inactiveText = tr("Search"); + + QSizePolicy policy = sizePolicy(); + setSizePolicy(QSizePolicy::Preferred, policy.verticalPolicy()); +} + +void SearchLineEdit::paintEvent(QPaintEvent *event) +{ + if (lineEdit()->text().isEmpty() && !hasFocus() && !m_inactiveText.isEmpty()) { + ExLineEdit::paintEvent(event); + QStyleOptionFrameV2 panel; + initStyleOption(&panel); + QRect r = style()->subElementRect(QStyle::SE_LineEditContents, &panel, this); + QFontMetrics fm = fontMetrics(); + int horizontalMargin = lineEdit()->x(); + QRect lineRect(horizontalMargin + r.x(), r.y() + (r.height() - fm.height() + 1) / 2, + r.width() - 2 * horizontalMargin, fm.height()); + QPainter painter(this); + painter.setPen(palette().brush(QPalette::Disabled, QPalette::Text).color()); + painter.drawText(lineRect, Qt::AlignLeft|Qt::AlignVCenter, m_inactiveText); + } else { + ExLineEdit::paintEvent(event); + } +} + +void SearchLineEdit::resizeEvent(QResizeEvent *event) +{ + updateGeometries(); + ExLineEdit::resizeEvent(event); +} + +void SearchLineEdit::updateGeometries() +{ + int menuHeight = height(); + int menuWidth = menuHeight + 1; + if (!m_searchButton->m_menu) + menuWidth = (menuHeight / 5) * 4; + m_searchButton->resize(QSize(menuWidth, menuHeight)); +} + +QString SearchLineEdit::inactiveText() const +{ + return m_inactiveText; +} + +void SearchLineEdit::setInactiveText(const QString &text) +{ + m_inactiveText = text; +} + +void SearchLineEdit::setMenu(QMenu *menu) +{ + if (m_searchButton->m_menu) + m_searchButton->m_menu->deleteLater(); + m_searchButton->m_menu = menu; + updateGeometries(); +} + +QMenu *SearchLineEdit::menu() const +{ + if (!m_searchButton->m_menu) { + m_searchButton->m_menu = new QMenu(m_searchButton); + if (isVisible()) + (const_cast<SearchLineEdit*>(this))->updateGeometries(); + } + return m_searchButton->m_menu; +} diff --git a/examples/widgets/browser/searchlineedit.h b/examples/widgets/browser/searchlineedit.h new file mode 100644 index 000000000..e01fcbff2 --- /dev/null +++ b/examples/widgets/browser/searchlineedit.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SEARCHLINEEDIT_H +#define SEARCHLINEEDIT_H + +#include "urllineedit.h" + +#include <QtWidgets/QLineEdit> +#include <QtWidgets/QAbstractButton> + +QT_BEGIN_NAMESPACE +class QMenu; +QT_END_NAMESPACE + +class SearchButton; + +/* + Clear button on the right hand side of the search widget. + Hidden by default + "A circle with an X in it" + */ +class ClearButton : public QAbstractButton +{ + Q_OBJECT + +public: + ClearButton(QWidget *parent = 0); + void paintEvent(QPaintEvent *event); + +public slots: + void textChanged(const QString &text); +}; + + +class SearchLineEdit : public ExLineEdit +{ + Q_OBJECT + Q_PROPERTY(QString inactiveText READ inactiveText WRITE setInactiveText) + +signals: + void textChanged(const QString &text); + +public: + SearchLineEdit(QWidget *parent = 0); + + QString inactiveText() const; + void setInactiveText(const QString &text); + + QMenu *menu() const; + void setMenu(QMenu *menu); + +protected: + void resizeEvent(QResizeEvent *event); + void paintEvent(QPaintEvent *event); + +private: + void updateGeometries(); + + SearchButton *m_searchButton; + QString m_inactiveText; +}; + +#endif // SEARCHLINEEDIT_H diff --git a/examples/widgets/browser/settings.cpp b/examples/widgets/browser/settings.cpp new file mode 100644 index 000000000..3521ce977 --- /dev/null +++ b/examples/widgets/browser/settings.cpp @@ -0,0 +1,321 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "settings.h" + +#include "browserapplication.h" +#include "browsermainwindow.h" +#include "cookiejar.h" +#include "history.h" +#include "networkaccessmanager.h" +#include "webview.h" + +#include <QtCore/QSettings> +#include <QtWidgets/QtWidgets> +#include <QtWebKitWidgets> + +SettingsDialog::SettingsDialog(QWidget *parent) + : QDialog(parent) +{ + setupUi(this); + connect(exceptionsButton, SIGNAL(clicked()), this, SLOT(showExceptions())); + connect(setHomeToCurrentPageButton, SIGNAL(clicked()), this, SLOT(setHomeToCurrentPage())); + connect(cookiesButton, SIGNAL(clicked()), this, SLOT(showCookies())); + connect(standardFontButton, SIGNAL(clicked()), this, SLOT(chooseFont())); + connect(fixedFontButton, SIGNAL(clicked()), this, SLOT(chooseFixedFont())); + + loadDefaults(); + loadFromSettings(); +} + +void SettingsDialog::loadDefaults() +{ + QWebSettings *defaultSettings = QWebSettings::globalSettings(); + QString standardFontFamily = defaultSettings->fontFamily(QWebSettings::StandardFont); + int standardFontSize = defaultSettings->fontSize(QWebSettings::DefaultFontSize); + standardFont = QFont(standardFontFamily, standardFontSize); + standardLabel->setText(QString(QLatin1String("%1 %2")).arg(standardFont.family()).arg(standardFont.pointSize())); + + QString fixedFontFamily = defaultSettings->fontFamily(QWebSettings::FixedFont); + int fixedFontSize = defaultSettings->fontSize(QWebSettings::DefaultFixedFontSize); + fixedFont = QFont(fixedFontFamily, fixedFontSize); + fixedLabel->setText(QString(QLatin1String("%1 %2")).arg(fixedFont.family()).arg(fixedFont.pointSize())); + + downloadsLocation->setText(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); + + enableJavascript->setChecked(defaultSettings->testAttribute(QWebSettings::JavascriptEnabled)); + enablePlugins->setChecked(defaultSettings->testAttribute(QWebSettings::PluginsEnabled)); +} + +void SettingsDialog::loadFromSettings() +{ + QSettings settings; + settings.beginGroup(QLatin1String("MainWindow")); + QString defaultHome = QLatin1String("http://qt-project.org/"); + homeLineEdit->setText(settings.value(QLatin1String("home"), defaultHome).toString()); + settings.endGroup(); + + settings.beginGroup(QLatin1String("history")); + int historyExpire = settings.value(QLatin1String("historyExpire")).toInt(); + int idx = 0; + switch (historyExpire) { + case 1: idx = 0; break; + case 7: idx = 1; break; + case 14: idx = 2; break; + case 30: idx = 3; break; + case 365: idx = 4; break; + case -1: idx = 5; break; + default: + idx = 5; + } + expireHistory->setCurrentIndex(idx); + settings.endGroup(); + + settings.beginGroup(QLatin1String("downloadmanager")); + QString downloadDirectory = settings.value(QLatin1String("downloadDirectory"), downloadsLocation->text()).toString(); + downloadsLocation->setText(downloadDirectory); + settings.endGroup(); + + settings.beginGroup(QLatin1String("general")); + openLinksIn->setCurrentIndex(settings.value(QLatin1String("openLinksIn"), openLinksIn->currentIndex()).toInt()); + + settings.endGroup(); + + // Appearance + settings.beginGroup(QLatin1String("websettings")); + fixedFont = qvariant_cast<QFont>(settings.value(QLatin1String("fixedFont"), fixedFont)); + standardFont = qvariant_cast<QFont>(settings.value(QLatin1String("standardFont"), standardFont)); + + standardLabel->setText(QString(QLatin1String("%1 %2")).arg(standardFont.family()).arg(standardFont.pointSize())); + fixedLabel->setText(QString(QLatin1String("%1 %2")).arg(fixedFont.family()).arg(fixedFont.pointSize())); + + enableJavascript->setChecked(settings.value(QLatin1String("enableJavascript"), enableJavascript->isChecked()).toBool()); + enablePlugins->setChecked(settings.value(QLatin1String("enablePlugins"), enablePlugins->isChecked()).toBool()); + userStyleSheet->setText(settings.value(QLatin1String("userStyleSheet")).toUrl().toString()); + settings.endGroup(); + + // Privacy + settings.beginGroup(QLatin1String("cookies")); + + QByteArray value = settings.value(QLatin1String("acceptCookies"), QLatin1String("AcceptOnlyFromSitesNavigatedTo")).toByteArray(); + QMetaEnum acceptPolicyEnum = CookieJar::staticMetaObject.enumerator(CookieJar::staticMetaObject.indexOfEnumerator("AcceptPolicy")); + CookieJar::AcceptPolicy acceptCookies = acceptPolicyEnum.keyToValue(value) == -1 ? + CookieJar::AcceptOnlyFromSitesNavigatedTo : + static_cast<CookieJar::AcceptPolicy>(acceptPolicyEnum.keyToValue(value)); + switch (acceptCookies) { + case CookieJar::AcceptAlways: + acceptCombo->setCurrentIndex(0); + break; + case CookieJar::AcceptNever: + acceptCombo->setCurrentIndex(1); + break; + case CookieJar::AcceptOnlyFromSitesNavigatedTo: + acceptCombo->setCurrentIndex(2); + break; + } + + value = settings.value(QLatin1String("keepCookiesUntil"), QLatin1String("Expire")).toByteArray(); + QMetaEnum keepPolicyEnum = CookieJar::staticMetaObject.enumerator(CookieJar::staticMetaObject.indexOfEnumerator("KeepPolicy")); + CookieJar::KeepPolicy keepCookies = keepPolicyEnum.keyToValue(value) == -1 ? + CookieJar::KeepUntilExpire : + static_cast<CookieJar::KeepPolicy>(keepPolicyEnum.keyToValue(value)); + switch (keepCookies) { + case CookieJar::KeepUntilExpire: + keepUntilCombo->setCurrentIndex(0); + break; + case CookieJar::KeepUntilExit: + keepUntilCombo->setCurrentIndex(1); + break; + case CookieJar::KeepUntilTimeLimit: + keepUntilCombo->setCurrentIndex(2); + break; + } + settings.endGroup(); + + + // Proxy + settings.beginGroup(QLatin1String("proxy")); + proxySupport->setChecked(settings.value(QLatin1String("enabled"), false).toBool()); + proxyType->setCurrentIndex(settings.value(QLatin1String("type"), 0).toInt()); + proxyHostName->setText(settings.value(QLatin1String("hostName")).toString()); + proxyPort->setValue(settings.value(QLatin1String("port"), 1080).toInt()); + proxyUserName->setText(settings.value(QLatin1String("userName")).toString()); + proxyPassword->setText(settings.value(QLatin1String("password")).toString()); + settings.endGroup(); +} + +void SettingsDialog::saveToSettings() +{ + QSettings settings; + settings.beginGroup(QLatin1String("MainWindow")); + settings.setValue(QLatin1String("home"), homeLineEdit->text()); + settings.endGroup(); + + settings.beginGroup(QLatin1String("general")); + settings.setValue(QLatin1String("openLinksIn"), openLinksIn->currentIndex()); + settings.endGroup(); + + settings.beginGroup(QLatin1String("history")); + int historyExpire = expireHistory->currentIndex(); + int idx = -1; + switch (historyExpire) { + case 0: idx = 1; break; + case 1: idx = 7; break; + case 2: idx = 14; break; + case 3: idx = 30; break; + case 4: idx = 365; break; + case 5: idx = -1; break; + } + settings.setValue(QLatin1String("historyExpire"), idx); + settings.endGroup(); + + // Appearance + settings.beginGroup(QLatin1String("websettings")); + settings.setValue(QLatin1String("fixedFont"), fixedFont); + settings.setValue(QLatin1String("standardFont"), standardFont); + settings.setValue(QLatin1String("enableJavascript"), enableJavascript->isChecked()); + settings.setValue(QLatin1String("enablePlugins"), enablePlugins->isChecked()); + QString userStyleSheetString = userStyleSheet->text(); + if (QFile::exists(userStyleSheetString)) + settings.setValue(QLatin1String("userStyleSheet"), QUrl::fromLocalFile(userStyleSheetString)); + else + settings.setValue(QLatin1String("userStyleSheet"), QUrl(userStyleSheetString)); + settings.endGroup(); + + //Privacy + settings.beginGroup(QLatin1String("cookies")); + + CookieJar::KeepPolicy keepCookies; + switch (acceptCombo->currentIndex()) { + default: + case 0: + keepCookies = CookieJar::KeepUntilExpire; + break; + case 1: + keepCookies = CookieJar::KeepUntilExit; + break; + case 2: + keepCookies = CookieJar::KeepUntilTimeLimit; + break; + } + QMetaEnum acceptPolicyEnum = CookieJar::staticMetaObject.enumerator(CookieJar::staticMetaObject.indexOfEnumerator("AcceptPolicy")); + settings.setValue(QLatin1String("acceptCookies"), QLatin1String(acceptPolicyEnum.valueToKey(keepCookies))); + + CookieJar::KeepPolicy keepPolicy; + switch (keepUntilCombo->currentIndex()) { + default: + case 0: + keepPolicy = CookieJar::KeepUntilExpire; + break; + case 1: + keepPolicy = CookieJar::KeepUntilExit; + break; + case 2: + keepPolicy = CookieJar::KeepUntilTimeLimit; + break; + } + + QMetaEnum keepPolicyEnum = CookieJar::staticMetaObject.enumerator(CookieJar::staticMetaObject.indexOfEnumerator("KeepPolicy")); + settings.setValue(QLatin1String("keepCookiesUntil"), QLatin1String(keepPolicyEnum.valueToKey(keepPolicy))); + + settings.endGroup(); + + // proxy + settings.beginGroup(QLatin1String("proxy")); + settings.setValue(QLatin1String("enabled"), proxySupport->isChecked()); + settings.setValue(QLatin1String("type"), proxyType->currentIndex()); + settings.setValue(QLatin1String("hostName"), proxyHostName->text()); + settings.setValue(QLatin1String("port"), proxyPort->text()); + settings.setValue(QLatin1String("userName"), proxyUserName->text()); + settings.setValue(QLatin1String("password"), proxyPassword->text()); + settings.endGroup(); + + BrowserApplication::instance()->loadSettings(); + BrowserApplication::networkAccessManager()->loadSettings(); + BrowserApplication::cookieJar()->loadSettings(); + BrowserApplication::historyManager()->loadSettings(); +} + +void SettingsDialog::accept() +{ + saveToSettings(); + QDialog::accept(); +} + +void SettingsDialog::showCookies() +{ + CookiesDialog *dialog = new CookiesDialog(BrowserApplication::cookieJar(), this); + dialog->exec(); +} + +void SettingsDialog::showExceptions() +{ + CookiesExceptionsDialog *dialog = new CookiesExceptionsDialog(BrowserApplication::cookieJar(), this); + dialog->exec(); +} + +void SettingsDialog::chooseFont() +{ + bool ok; + QFont font = QFontDialog::getFont(&ok, standardFont, this); + if ( ok ) { + standardFont = font; + standardLabel->setText(QString(QLatin1String("%1 %2")).arg(font.family()).arg(font.pointSize())); + } +} + +void SettingsDialog::chooseFixedFont() +{ + bool ok; + QFont font = QFontDialog::getFont(&ok, fixedFont, this); + if ( ok ) { + fixedFont = font; + fixedLabel->setText(QString(QLatin1String("%1 %2")).arg(font.family()).arg(font.pointSize())); + } +} + +void SettingsDialog::setHomeToCurrentPage() +{ + BrowserMainWindow *mw = static_cast<BrowserMainWindow*>(parent()); + WebView *webView = mw->currentTab(); + if (webView) + homeLineEdit->setText(webView->url().toString()); +} diff --git a/examples/widgets/browser/settings.h b/examples/widgets/browser/settings.h new file mode 100644 index 000000000..0858e0023 --- /dev/null +++ b/examples/widgets/browser/settings.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SETTINGS_H +#define SETTINGS_H + +#include <QtWidgets/QDialog> +#include "ui_settings.h" + +class SettingsDialog : public QDialog, public Ui_Settings +{ + Q_OBJECT + +public: + SettingsDialog(QWidget *parent = 0); + void accept(); + +private slots: + void loadDefaults(); + void loadFromSettings(); + void saveToSettings(); + + void setHomeToCurrentPage(); + void showCookies(); + void showExceptions(); + + void chooseFont(); + void chooseFixedFont(); + +private: + QFont standardFont; + QFont fixedFont; +}; + +#endif // SETTINGS_H diff --git a/examples/widgets/browser/settings.ui b/examples/widgets/browser/settings.ui new file mode 100644 index 000000000..3491ce0b0 --- /dev/null +++ b/examples/widgets/browser/settings.ui @@ -0,0 +1,614 @@ +<ui version="4.0" > + <class>Settings</class> + <widget class="QDialog" name="Settings" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>657</width> + <height>322</height> + </rect> + </property> + <property name="windowTitle" > + <string>Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout" > + <item row="2" column="0" > + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="1" column="0" > + <widget class="QTabWidget" name="tabWidget" > + <property name="currentIndex" > + <number>0</number> + </property> + <widget class="QWidget" name="tab" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>243</height> + </rect> + </property> + <attribute name="title" > + <string>General</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_4" > + <item row="0" column="0" > + <widget class="QLabel" name="label_3" > + <property name="text" > + <string>Home:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2" > + <widget class="QLineEdit" name="homeLineEdit" /> + </item> + <item row="1" column="1" > + <widget class="QPushButton" name="setHomeToCurrentPageButton" > + <property name="text" > + <string>Set to current page</string> + </property> + </widget> + </item> + <item row="1" column="2" > + <spacer name="horizontalSpacer" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>280</width> + <height>18</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0" > + <widget class="QLabel" name="label_4" > + <property name="text" > + <string>Remove history items:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2" > + <widget class="QComboBox" name="expireHistory" > + <item> + <property name="text" > + <string>After one day</string> + </property> + </item> + <item> + <property name="text" > + <string>After one week</string> + </property> + </item> + <item> + <property name="text" > + <string>After two weeks</string> + </property> + </item> + <item> + <property name="text" > + <string>After one month</string> + </property> + </item> + <item> + <property name="text" > + <string>After one year</string> + </property> + </item> + <item> + <property name="text" > + <string>Manually</string> + </property> + </item> + </widget> + </item> + <item row="3" column="0" > + <widget class="QLabel" name="label_7" > + <property name="text" > + <string>Save downloads to:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2" > + <widget class="QLineEdit" name="downloadsLocation" /> + </item> + <item row="4" column="0" > + <widget class="QLabel" name="label_8" > + <property name="text" > + <string>Open links from applications:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="1" colspan="2" > + <widget class="QComboBox" name="openLinksIn" > + <item> + <property name="text" > + <string>In a tab in the current window</string> + </property> + </item> + <item> + <property name="text" > + <string>In a new window</string> + </property> + </item> + </widget> + </item> + <item row="5" column="1" colspan="2" > + <spacer> + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>391</width> + <height>262</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_3" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>243</height> + </rect> + </property> + <attribute name="title" > + <string>Appearance</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3" > + <item row="0" column="0" > + <widget class="QLabel" name="label_5" > + <property name="text" > + <string>Standard font:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1" > + <widget class="QLabel" name="standardLabel" > + <property name="sizePolicy" > + <sizepolicy vsizetype="Preferred" hsizetype="Expanding" > + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape" > + <enum>QFrame::StyledPanel</enum> + </property> + <property name="text" > + <string>Times 16</string> + </property> + <property name="alignment" > + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="0" column="2" > + <widget class="QPushButton" name="standardFontButton" > + <property name="text" > + <string>Select...</string> + </property> + </widget> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label_6" > + <property name="text" > + <string>Fixed-width font:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1" > + <widget class="QLabel" name="fixedLabel" > + <property name="frameShape" > + <enum>QFrame::StyledPanel</enum> + </property> + <property name="text" > + <string>Courier 13</string> + </property> + <property name="alignment" > + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="1" column="2" > + <widget class="QPushButton" name="fixedFontButton" > + <property name="text" > + <string>Select...</string> + </property> + </widget> + </item> + <item row="2" column="1" > + <spacer name="verticalSpacer" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>20</width> + <height>93</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>243</height> + </rect> + </property> + <attribute name="title" > + <string>Privacy</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3" > + <item> + <widget class="QGroupBox" name="groupBox" > + <property name="title" > + <string>Web Content</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2" > + <item> + <widget class="QCheckBox" name="enablePlugins" > + <property name="text" > + <string>Enable Plugins</string> + </property> + <property name="checked" > + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="enableJavascript" > + <property name="text" > + <string>Enable Javascript</string> + </property> + <property name="checked" > + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="cookiesGroupBox" > + <property name="title" > + <string>Cookies</string> + </property> + <layout class="QGridLayout" > + <item row="0" column="0" > + <widget class="QLabel" name="label_2" > + <property name="text" > + <string>Accept Cookies:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1" > + <widget class="QComboBox" name="acceptCombo" > + <item> + <property name="text" > + <string>Always</string> + </property> + </item> + <item> + <property name="text" > + <string>Never</string> + </property> + </item> + <item> + <property name="text" > + <string>Only from sites you navigate to</string> + </property> + </item> + </widget> + </item> + <item row="0" column="2" > + <widget class="QPushButton" name="exceptionsButton" > + <property name="text" > + <string>Exceptions...</string> + </property> + </widget> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label" > + <property name="text" > + <string>Keep until:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1" > + <widget class="QComboBox" name="keepUntilCombo" > + <item> + <property name="text" > + <string>They expire</string> + </property> + </item> + <item> + <property name="text" > + <string>I exit the application</string> + </property> + </item> + <item> + <property name="text" > + <string>At most 90 days</string> + </property> + </item> + </widget> + </item> + <item row="1" column="2" > + <widget class="QPushButton" name="cookiesButton" > + <property name="text" > + <string>Cookies...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer> + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>371</width> + <height>177</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_4" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>243</height> + </rect> + </property> + <attribute name="title" > + <string>Proxy</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout" > + <item> + <widget class="QGroupBox" name="proxySupport" > + <property name="title" > + <string>Enable proxy</string> + </property> + <property name="checkable" > + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout_6" > + <item row="0" column="0" > + <widget class="QLabel" name="label_9" > + <property name="text" > + <string>Type:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2" > + <widget class="QComboBox" name="proxyType" > + <item> + <property name="text" > + <string>Socks5</string> + </property> + </item> + <item> + <property name="text" > + <string>Http</string> + </property> + </item> + </widget> + </item> + <item row="1" column="0" > + <widget class="QLabel" name="label_10" > + <property name="text" > + <string>Host:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2" > + <widget class="QLineEdit" name="proxyHostName" /> + </item> + <item row="2" column="0" > + <widget class="QLabel" name="label_11" > + <property name="text" > + <string>Port:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="1" > + <widget class="QSpinBox" name="proxyPort" > + <property name="maximum" > + <number>10000</number> + </property> + <property name="value" > + <number>1080</number> + </property> + </widget> + </item> + <item row="2" column="2" > + <spacer name="horizontalSpacer_2" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>293</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="0" > + <widget class="QLabel" name="label_12" > + <property name="text" > + <string>User Name:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2" > + <widget class="QLineEdit" name="proxyUserName" /> + </item> + <item row="4" column="0" > + <widget class="QLabel" name="label_13" > + <property name="text" > + <string>Password:</string> + </property> + <property name="alignment" > + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="1" colspan="2" > + <widget class="QLineEdit" name="proxyPassword" > + <property name="echoMode" > + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="5" column="0" > + <spacer name="verticalSpacer_2" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>20</width> + <height>8</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_5" > + <attribute name="title" > + <string>Advanced</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2" > + <item row="0" column="0" > + <widget class="QLabel" name="label_14" > + <property name="text" > + <string>Style Sheet:</string> + </property> + </widget> + </item> + <item row="0" column="1" > + <widget class="QLineEdit" name="userStyleSheet" /> + </item> + <item row="1" column="1" > + <spacer name="verticalSpacer_3" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0" > + <size> + <width>20</width> + <height>176</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>Settings</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Settings</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/widgets/browser/squeezelabel.cpp b/examples/widgets/browser/squeezelabel.cpp new file mode 100644 index 000000000..c40fe97da --- /dev/null +++ b/examples/widgets/browser/squeezelabel.cpp @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "squeezelabel.h" + +SqueezeLabel::SqueezeLabel(QWidget *parent) : QLabel(parent) +{ +} + +void SqueezeLabel::paintEvent(QPaintEvent *event) +{ + QFontMetrics fm = fontMetrics(); + if (fm.width(text()) > contentsRect().width()) { + QString elided = fm.elidedText(text(), Qt::ElideMiddle, width()); + QString oldText = text(); + setText(elided); + QLabel::paintEvent(event); + setText(oldText); + } else { + QLabel::paintEvent(event); + } +} diff --git a/examples/widgets/browser/squeezelabel.h b/examples/widgets/browser/squeezelabel.h new file mode 100644 index 000000000..6df84179a --- /dev/null +++ b/examples/widgets/browser/squeezelabel.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SQUEEZELABEL_H +#define SQUEEZELABEL_H + +#include <QtWidgets/QLabel> + +class SqueezeLabel : public QLabel +{ + Q_OBJECT + +public: + SqueezeLabel(QWidget *parent = 0); + +protected: + void paintEvent(QPaintEvent *event); + +}; + +#endif // SQUEEZELABEL_H diff --git a/examples/widgets/browser/tabwidget.cpp b/examples/widgets/browser/tabwidget.cpp new file mode 100644 index 000000000..e451b39d5 --- /dev/null +++ b/examples/widgets/browser/tabwidget.cpp @@ -0,0 +1,834 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "tabwidget.h" + +#include "browserapplication.h" +#include "browsermainwindow.h" +#include "history.h" +#include "urllineedit.h" +#include "webview.h" + +#include <QtCore/QMimeData> +#include <QtGui/QClipboard> +#include <QtWidgets/QCompleter> +#include <QtWidgets/QListView> +#include <QtWidgets/QMenu> +#include <QtWidgets/QMessageBox> +#include <QtGui/QDrag> +#include <QtGui/QMouseEvent> +#include <QtWidgets/QStackedWidget> +#include <QtWidgets/QStyle> +#include <QtWidgets/QToolButton> + +#include <QtCore/QDebug> + +TabBar::TabBar(QWidget *parent) + : QTabBar(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + setAcceptDrops(true); + connect(this, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(contextMenuRequested(QPoint))); + + QString ctrl = QLatin1String("Ctrl+%1"); + for (int i = 1; i <= 10; ++i) { + int key = i; + if (key == 10) + key = 0; + QShortcut *shortCut = new QShortcut(ctrl.arg(key), this); + m_tabShortcuts.append(shortCut); + connect(shortCut, SIGNAL(activated()), this, SLOT(selectTabAction())); + } + setTabsClosable(true); + connect(this, SIGNAL(tabCloseRequested(int)), + this, SIGNAL(closeTab(int))); + setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); + setMovable(true); +} + +void TabBar::selectTabAction() +{ + if (QShortcut *shortCut = qobject_cast<QShortcut*>(sender())) { + int index = m_tabShortcuts.indexOf(shortCut); + if (index == 0) + index = 10; + setCurrentIndex(index); + } +} + +void TabBar::contextMenuRequested(const QPoint &position) +{ + QMenu menu; + menu.addAction(tr("New &Tab"), this, SIGNAL(newTab()), QKeySequence::AddTab); + int index = tabAt(position); + if (-1 != index) { + QAction *action = menu.addAction(tr("Clone Tab"), + this, SLOT(cloneTab())); + action->setData(index); + + menu.addSeparator(); + + action = menu.addAction(tr("&Close Tab"), + this, SLOT(closeTab()), QKeySequence::Close); + action->setData(index); + + action = menu.addAction(tr("Close &Other Tabs"), + this, SLOT(closeOtherTabs())); + action->setData(index); + + menu.addSeparator(); + + action = menu.addAction(tr("Reload Tab"), + this, SLOT(reloadTab()), QKeySequence::Refresh); + action->setData(index); + } else { + menu.addSeparator(); + } + menu.addAction(tr("Reload All Tabs"), this, SIGNAL(reloadAllTabs())); + menu.exec(QCursor::pos()); +} + +void TabBar::cloneTab() +{ + if (QAction *action = qobject_cast<QAction*>(sender())) { + int index = action->data().toInt(); + emit cloneTab(index); + } +} + +void TabBar::closeTab() +{ + if (QAction *action = qobject_cast<QAction*>(sender())) { + int index = action->data().toInt(); + emit closeTab(index); + } +} + +void TabBar::closeOtherTabs() +{ + if (QAction *action = qobject_cast<QAction*>(sender())) { + int index = action->data().toInt(); + emit closeOtherTabs(index); + } +} + +void TabBar::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + m_dragStartPos = event->pos(); + QTabBar::mousePressEvent(event); +} + +void TabBar::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::LeftButton) { + int diffX = event->pos().x() - m_dragStartPos.x(); + int diffY = event->pos().y() - m_dragStartPos.y(); + if ((event->pos() - m_dragStartPos).manhattanLength() > QApplication::startDragDistance() + && diffX < 3 && diffX > -3 + && diffY < -10) { + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + QList<QUrl> urls; + int index = tabAt(event->pos()); + QUrl url = tabData(index).toUrl(); + urls.append(url); + mimeData->setUrls(urls); + mimeData->setText(tabText(index)); + mimeData->setData(QLatin1String("action"), "tab-reordering"); + drag->setMimeData(mimeData); + drag->exec(); + } + } + QTabBar::mouseMoveEvent(event); +} + +// When index is -1 index chooses the current tab +void TabWidget::reloadTab(int index) +{ + if (index < 0) + index = currentIndex(); + if (index < 0 || index >= count()) + return; + + QWidget *widget = this->widget(index); + if (WebView *tab = qobject_cast<WebView*>(widget)) + tab->reload(); +} + +void TabBar::reloadTab() +{ + if (QAction *action = qobject_cast<QAction*>(sender())) { + int index = action->data().toInt(); + emit reloadTab(index); + } +} + +TabWidget::TabWidget(QWidget *parent) + : QTabWidget(parent) + , m_recentlyClosedTabsAction(0) + , m_newTabAction(0) + , m_closeTabAction(0) + , m_nextTabAction(0) + , m_previousTabAction(0) + , m_recentlyClosedTabsMenu(0) + , m_lineEditCompleter(0) + , m_lineEdits(0) + , m_tabBar(new TabBar(this)) +{ + setElideMode(Qt::ElideRight); + + connect(m_tabBar, SIGNAL(newTab()), this, SLOT(newTab())); + connect(m_tabBar, SIGNAL(closeTab(int)), this, SLOT(closeTab(int))); + connect(m_tabBar, SIGNAL(cloneTab(int)), this, SLOT(cloneTab(int))); + connect(m_tabBar, SIGNAL(closeOtherTabs(int)), this, SLOT(closeOtherTabs(int))); + connect(m_tabBar, SIGNAL(reloadTab(int)), this, SLOT(reloadTab(int))); + connect(m_tabBar, SIGNAL(reloadAllTabs()), this, SLOT(reloadAllTabs())); + connect(m_tabBar, SIGNAL(tabMoved(int,int)), this, SLOT(moveTab(int,int))); + setTabBar(m_tabBar); + setDocumentMode(true); + + // Actions + m_newTabAction = new QAction(QIcon(QLatin1String(":addtab.png")), tr("New &Tab"), this); + m_newTabAction->setShortcuts(QKeySequence::AddTab); + m_newTabAction->setIconVisibleInMenu(false); + connect(m_newTabAction, SIGNAL(triggered()), this, SLOT(newTab())); + + m_closeTabAction = new QAction(QIcon(QLatin1String(":closetab.png")), tr("&Close Tab"), this); + m_closeTabAction->setShortcuts(QKeySequence::Close); + m_closeTabAction->setIconVisibleInMenu(false); + connect(m_closeTabAction, SIGNAL(triggered()), this, SLOT(closeTab())); + + m_nextTabAction = new QAction(tr("Show Next Tab"), this); + QList<QKeySequence> shortcuts; + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_BraceRight)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_PageDown)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_BracketRight)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_Less)); + m_nextTabAction->setShortcuts(shortcuts); + connect(m_nextTabAction, SIGNAL(triggered()), this, SLOT(nextTab())); + + m_previousTabAction = new QAction(tr("Show Previous Tab"), this); + shortcuts.clear(); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_BraceLeft)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_PageUp)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_BracketLeft)); + shortcuts.append(QKeySequence(Qt::CTRL | Qt::Key_Greater)); + m_previousTabAction->setShortcuts(shortcuts); + connect(m_previousTabAction, SIGNAL(triggered()), this, SLOT(previousTab())); + + m_recentlyClosedTabsMenu = new QMenu(this); + connect(m_recentlyClosedTabsMenu, SIGNAL(aboutToShow()), + this, SLOT(aboutToShowRecentTabsMenu())); + connect(m_recentlyClosedTabsMenu, SIGNAL(triggered(QAction*)), + this, SLOT(aboutToShowRecentTriggeredAction(QAction*))); + m_recentlyClosedTabsAction = new QAction(tr("Recently Closed Tabs"), this); + m_recentlyClosedTabsAction->setMenu(m_recentlyClosedTabsMenu); + m_recentlyClosedTabsAction->setEnabled(false); + + connect(this, SIGNAL(currentChanged(int)), + this, SLOT(currentChanged(int))); + + m_lineEdits = new QStackedWidget(this); +} + +void TabWidget::clear() +{ + // clear the recently closed tabs + m_recentlyClosedTabs.clear(); + // clear the line edit history + for (int i = 0; i < m_lineEdits->count(); ++i) { + QLineEdit *qLineEdit = lineEdit(i); + qLineEdit->setText(qLineEdit->text()); + } +} + +void TabWidget::moveTab(int fromIndex, int toIndex) +{ + QWidget *lineEdit = m_lineEdits->widget(fromIndex); + m_lineEdits->removeWidget(lineEdit); + m_lineEdits->insertWidget(toIndex, lineEdit); +} + +void TabWidget::addWebAction(QAction *action, QWebPage::WebAction webAction) +{ + if (!action) + return; + m_actions.append(new WebActionMapper(action, webAction, this)); +} + +void TabWidget::currentChanged(int index) +{ + WebView *webView = this->webView(index); + if (!webView) + return; + + Q_ASSERT(m_lineEdits->count() == count()); + + WebView *oldWebView = this->webView(m_lineEdits->currentIndex()); + if (oldWebView) { + disconnect(oldWebView, SIGNAL(statusBarMessage(QString)), + this, SIGNAL(showStatusBarMessage(QString))); + disconnect(oldWebView->page(), SIGNAL(linkHovered(QString,QString,QString)), + this, SIGNAL(linkHovered(QString))); + disconnect(oldWebView, SIGNAL(loadProgress(int)), + this, SIGNAL(loadProgress(int))); + } + + connect(webView, SIGNAL(statusBarMessage(QString)), + this, SIGNAL(showStatusBarMessage(QString))); + connect(webView->page(), SIGNAL(linkHovered(QString,QString,QString)), + this, SIGNAL(linkHovered(QString))); + connect(webView, SIGNAL(loadProgress(int)), + this, SIGNAL(loadProgress(int))); + + for (int i = 0; i < m_actions.count(); ++i) { + WebActionMapper *mapper = m_actions[i]; + mapper->updateCurrent(webView->page()); + } + emit setCurrentTitle(webView->title()); + m_lineEdits->setCurrentIndex(index); + emit loadProgress(webView->progress()); + emit showStatusBarMessage(webView->lastStatusBarText()); + if (webView->url().isEmpty()) + m_lineEdits->currentWidget()->setFocus(); + else + webView->setFocus(); +} + +QAction *TabWidget::newTabAction() const +{ + return m_newTabAction; +} + +QAction *TabWidget::closeTabAction() const +{ + return m_closeTabAction; +} + +QAction *TabWidget::recentlyClosedTabsAction() const +{ + return m_recentlyClosedTabsAction; +} + +QAction *TabWidget::nextTabAction() const +{ + return m_nextTabAction; +} + +QAction *TabWidget::previousTabAction() const +{ + return m_previousTabAction; +} + +QWidget *TabWidget::lineEditStack() const +{ + return m_lineEdits; +} + +QLineEdit *TabWidget::currentLineEdit() const +{ + return lineEdit(m_lineEdits->currentIndex()); +} + +WebView *TabWidget::currentWebView() const +{ + return webView(currentIndex()); +} + +QLineEdit *TabWidget::lineEdit(int index) const +{ + UrlLineEdit *urlLineEdit = qobject_cast<UrlLineEdit*>(m_lineEdits->widget(index)); + if (urlLineEdit) + return urlLineEdit->lineEdit(); + return 0; +} + +WebView *TabWidget::webView(int index) const +{ + QWidget *widget = this->widget(index); + if (WebView *webView = qobject_cast<WebView*>(widget)) { + return webView; + } else { + // optimization to delay creating the first webview + if (count() == 1) { + TabWidget *that = const_cast<TabWidget*>(this); + that->setUpdatesEnabled(false); + that->newTab(); + that->closeTab(0); + that->setUpdatesEnabled(true); + return currentWebView(); + } + } + return 0; +} + +int TabWidget::webViewIndex(WebView *webView) const +{ + int index = indexOf(webView); + return index; +} + +WebView *TabWidget::newTab(bool makeCurrent) +{ + // line edit + UrlLineEdit *urlLineEdit = new UrlLineEdit; + QLineEdit *lineEdit = urlLineEdit->lineEdit(); + if (!m_lineEditCompleter && count() > 0) { + HistoryCompletionModel *completionModel = new HistoryCompletionModel(this); + completionModel->setSourceModel(BrowserApplication::historyManager()->historyFilterModel()); + m_lineEditCompleter = new QCompleter(completionModel, this); + // Should this be in Qt by default? + QAbstractItemView *popup = m_lineEditCompleter->popup(); + QListView *listView = qobject_cast<QListView*>(popup); + if (listView) + listView->setUniformItemSizes(true); + } + lineEdit->setCompleter(m_lineEditCompleter); + connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(lineEditReturnPressed())); + m_lineEdits->addWidget(urlLineEdit); + m_lineEdits->setSizePolicy(lineEdit->sizePolicy()); + + // optimization to delay creating the more expensive WebView, history, etc + if (count() == 0) { + QWidget *emptyWidget = new QWidget; + QPalette p = emptyWidget->palette(); + p.setColor(QPalette::Window, palette().color(QPalette::Base)); + emptyWidget->setPalette(p); + emptyWidget->setAutoFillBackground(true); + disconnect(this, SIGNAL(currentChanged(int)), + this, SLOT(currentChanged(int))); + addTab(emptyWidget, tr("(Untitled)")); + connect(this, SIGNAL(currentChanged(int)), + this, SLOT(currentChanged(int))); + return 0; + } + + // webview + WebView *webView = new WebView; + urlLineEdit->setWebView(webView); + connect(webView, SIGNAL(loadStarted()), + this, SLOT(webViewLoadStarted())); + connect(webView, SIGNAL(loadFinished(bool)), + this, SLOT(webViewIconChanged())); + connect(webView, SIGNAL(iconChanged()), + this, SLOT(webViewIconChanged())); + connect(webView, SIGNAL(titleChanged(QString)), + this, SLOT(webViewTitleChanged(QString))); + connect(webView, SIGNAL(urlChanged(QUrl)), + this, SLOT(webViewUrlChanged(QUrl))); + connect(webView->page(), SIGNAL(windowCloseRequested()), + this, SLOT(windowCloseRequested())); + connect(webView->page(), SIGNAL(geometryChangeRequested(QRect)), + this, SIGNAL(geometryChangeRequested(QRect))); + connect(webView->page(), SIGNAL(printRequested(QWebFrame*)), + this, SIGNAL(printRequested(QWebFrame*))); + connect(webView->page(), SIGNAL(menuBarVisibilityChangeRequested(bool)), + this, SIGNAL(menuBarVisibilityChangeRequested(bool))); + connect(webView->page(), SIGNAL(statusBarVisibilityChangeRequested(bool)), + this, SIGNAL(statusBarVisibilityChangeRequested(bool))); + connect(webView->page(), SIGNAL(toolBarVisibilityChangeRequested(bool)), + this, SIGNAL(toolBarVisibilityChangeRequested(bool))); + addTab(webView, tr("(Untitled)")); + if (makeCurrent) + setCurrentWidget(webView); + + // webview actions + for (int i = 0; i < m_actions.count(); ++i) { + WebActionMapper *mapper = m_actions[i]; + mapper->addChild(webView->page()->action(mapper->webAction())); + } + + if (count() == 1) + currentChanged(currentIndex()); + emit tabsChanged(); + return webView; +} + +void TabWidget::reloadAllTabs() +{ + for (int i = 0; i < count(); ++i) { + QWidget *tabWidget = widget(i); + if (WebView *tab = qobject_cast<WebView*>(tabWidget)) { + tab->reload(); + } + } +} + +void TabWidget::lineEditReturnPressed() +{ + if (QLineEdit *lineEdit = qobject_cast<QLineEdit*>(sender())) { + emit loadPage(lineEdit->text()); + if (m_lineEdits->currentWidget() == lineEdit) + currentWebView()->setFocus(); + } +} + +void TabWidget::windowCloseRequested() +{ + WebPage *webPage = qobject_cast<WebPage*>(sender()); + WebView *webView = qobject_cast<WebView*>(webPage->view()); + int index = webViewIndex(webView); + if (index >= 0) { + if (count() == 1) + webView->webPage()->mainWindow()->close(); + else + closeTab(index); + } +} + +void TabWidget::closeOtherTabs(int index) +{ + if (-1 == index) + return; + for (int i = count() - 1; i > index; --i) + closeTab(i); + for (int i = index - 1; i >= 0; --i) + closeTab(i); +} + +// When index is -1 index chooses the current tab +void TabWidget::cloneTab(int index) +{ + if (index < 0) + index = currentIndex(); + if (index < 0 || index >= count()) + return; + WebView *tab = newTab(false); + tab->setUrl(webView(index)->url()); +} + +// When index is -1 index chooses the current tab +void TabWidget::closeTab(int index) +{ + if (index < 0) + index = currentIndex(); + if (index < 0 || index >= count()) + return; + + bool hasFocus = false; + if (WebView *tab = webView(index)) { + if (tab->isModified()) { + QMessageBox closeConfirmation(tab); + closeConfirmation.setWindowFlags(Qt::Sheet); + closeConfirmation.setWindowTitle(tr("Do you really want to close this page?")); + closeConfirmation.setInformativeText(tr("You have modified this page and when closing it you would lose the modification.\n" + "Do you really want to close this page?\n")); + closeConfirmation.setIcon(QMessageBox::Question); + closeConfirmation.addButton(QMessageBox::Yes); + closeConfirmation.addButton(QMessageBox::No); + closeConfirmation.setEscapeButton(QMessageBox::No); + if (closeConfirmation.exec() == QMessageBox::No) + return; + } + hasFocus = tab->hasFocus(); + + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (!globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) { + m_recentlyClosedTabsAction->setEnabled(true); + m_recentlyClosedTabs.prepend(tab->url()); + if (m_recentlyClosedTabs.size() >= TabWidget::m_recentlyClosedTabsSize) + m_recentlyClosedTabs.removeLast(); + } + } + QWidget *lineEdit = m_lineEdits->widget(index); + m_lineEdits->removeWidget(lineEdit); + lineEdit->deleteLater(); + QWidget *webView = widget(index); + removeTab(index); + webView->deleteLater(); + emit tabsChanged(); + if (hasFocus && count() > 0) + currentWebView()->setFocus(); + if (count() == 0) + emit lastTabClosed(); +} + +void TabWidget::webViewLoadStarted() +{ + WebView *webView = qobject_cast<WebView*>(sender()); + int index = webViewIndex(webView); + if (-1 != index) { + QIcon icon(QLatin1String(":loading.gif")); + setTabIcon(index, icon); + } +} + +void TabWidget::webViewIconChanged() +{ + WebView *webView = qobject_cast<WebView*>(sender()); + int index = webViewIndex(webView); + if (-1 != index) { + QIcon icon = BrowserApplication::instance()->icon(webView->url()); + setTabIcon(index, icon); + } +} + +void TabWidget::webViewTitleChanged(const QString &title) +{ + WebView *webView = qobject_cast<WebView*>(sender()); + int index = webViewIndex(webView); + if (-1 != index) { + setTabText(index, title); + } + if (currentIndex() == index) + emit setCurrentTitle(title); + BrowserApplication::historyManager()->updateHistoryItem(webView->url(), title); +} + +void TabWidget::webViewUrlChanged(const QUrl &url) +{ + WebView *webView = qobject_cast<WebView*>(sender()); + int index = webViewIndex(webView); + if (-1 != index) { + m_tabBar->setTabData(index, url); + } + emit tabsChanged(); +} + +void TabWidget::aboutToShowRecentTabsMenu() +{ + m_recentlyClosedTabsMenu->clear(); + for (int i = 0; i < m_recentlyClosedTabs.count(); ++i) { + QAction *action = new QAction(m_recentlyClosedTabsMenu); + action->setData(m_recentlyClosedTabs.at(i)); + QIcon icon = BrowserApplication::instance()->icon(m_recentlyClosedTabs.at(i)); + action->setIcon(icon); + action->setText(m_recentlyClosedTabs.at(i).toString()); + m_recentlyClosedTabsMenu->addAction(action); + } +} + +void TabWidget::aboutToShowRecentTriggeredAction(QAction *action) +{ + QUrl url = action->data().toUrl(); + loadUrlInCurrentTab(url); +} + +void TabWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (!childAt(event->pos()) + // Remove the line below when QTabWidget does not have a one pixel frame + && event->pos().y() < (tabBar()->y() + tabBar()->height())) { + newTab(); + return; + } + QTabWidget::mouseDoubleClickEvent(event); +} + +void TabWidget::contextMenuEvent(QContextMenuEvent *event) +{ + if (!childAt(event->pos())) { + m_tabBar->contextMenuRequested(event->pos()); + return; + } + QTabWidget::contextMenuEvent(event); +} + +void TabWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::MidButton && !childAt(event->pos()) + // Remove the line below when QTabWidget does not have a one pixel frame + && event->pos().y() < (tabBar()->y() + tabBar()->height())) { + QUrl url(QApplication::clipboard()->text(QClipboard::Selection)); + if (!url.isEmpty() && url.isValid() && !url.scheme().isEmpty()) { + WebView *webView = newTab(); + webView->setUrl(url); + } + } +} + +void TabWidget::loadUrlInCurrentTab(const QUrl &url) +{ + WebView *webView = currentWebView(); + if (webView) { + webView->loadUrl(url); + webView->setFocus(); + } +} + +void TabWidget::nextTab() +{ + int next = currentIndex() + 1; + if (next == count()) + next = 0; + setCurrentIndex(next); +} + +void TabWidget::previousTab() +{ + int next = currentIndex() - 1; + if (next < 0) + next = count() - 1; + setCurrentIndex(next); +} + +static const qint32 TabWidgetMagic = 0xaa; + +QByteArray TabWidget::saveState() const +{ + int version = 1; + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + + stream << qint32(TabWidgetMagic); + stream << qint32(version); + + QStringList tabs; + for (int i = 0; i < count(); ++i) { + if (WebView *tab = qobject_cast<WebView*>(widget(i))) { + tabs.append(tab->url().toString()); + } else { + tabs.append(QString::null); + } + } + stream << tabs; + stream << currentIndex(); + return data; +} + +bool TabWidget::restoreState(const QByteArray &state) +{ + int version = 1; + QByteArray sd = state; + QDataStream stream(&sd, QIODevice::ReadOnly); + if (stream.atEnd()) + return false; + + qint32 marker; + qint32 v; + stream >> marker; + stream >> v; + if (marker != TabWidgetMagic || v != version) + return false; + + QStringList openTabs; + stream >> openTabs; + + for (int i = 0; i < openTabs.count(); ++i) { + if (i != 0) + newTab(); + loadPage(openTabs.at(i)); + } + + int currentTab; + stream >> currentTab; + setCurrentIndex(currentTab); + + return true; +} + +WebActionMapper::WebActionMapper(QAction *root, QWebPage::WebAction webAction, QObject *parent) + : QObject(parent) + , m_currentParent(0) + , m_root(root) + , m_webAction(webAction) +{ + if (!m_root) + return; + connect(m_root, SIGNAL(triggered()), this, SLOT(rootTriggered())); + connect(root, SIGNAL(destroyed(QObject*)), this, SLOT(rootDestroyed())); + root->setEnabled(false); +} + +void WebActionMapper::rootDestroyed() +{ + m_root = 0; +} + +void WebActionMapper::currentDestroyed() +{ + updateCurrent(0); +} + +void WebActionMapper::addChild(QAction *action) +{ + if (!action) + return; + connect(action, SIGNAL(changed()), this, SLOT(childChanged())); +} + +QWebPage::WebAction WebActionMapper::webAction() const +{ + return m_webAction; +} + +void WebActionMapper::rootTriggered() +{ + if (m_currentParent) { + QAction *gotoAction = m_currentParent->action(m_webAction); + gotoAction->trigger(); + } +} + +void WebActionMapper::childChanged() +{ + if (QAction *source = qobject_cast<QAction*>(sender())) { + if (m_root + && m_currentParent + && source->parent() == m_currentParent) { + m_root->setChecked(source->isChecked()); + m_root->setEnabled(source->isEnabled()); + } + } +} + +void WebActionMapper::updateCurrent(QWebPage *currentParent) +{ + if (m_currentParent) + disconnect(m_currentParent, SIGNAL(destroyed(QObject*)), + this, SLOT(currentDestroyed())); + + m_currentParent = currentParent; + if (!m_root) + return; + if (!m_currentParent) { + m_root->setEnabled(false); + m_root->setChecked(false); + return; + } + QAction *source = m_currentParent->action(m_webAction); + m_root->setChecked(source->isChecked()); + m_root->setEnabled(source->isEnabled()); + connect(m_currentParent, SIGNAL(destroyed(QObject*)), + this, SLOT(currentDestroyed())); +} diff --git a/examples/widgets/browser/tabwidget.h b/examples/widgets/browser/tabwidget.h new file mode 100644 index 000000000..9a8f10ff1 --- /dev/null +++ b/examples/widgets/browser/tabwidget.h @@ -0,0 +1,223 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TABWIDGET_H +#define TABWIDGET_H + +#include <QtWidgets/QTabBar> + +#include <QtWidgets/QShortcut> +/* + Tab bar with a few more features such as a context menu and shortcuts + */ +class TabBar : public QTabBar +{ + Q_OBJECT + +signals: + void newTab(); + void cloneTab(int index); + void closeTab(int index); + void closeOtherTabs(int index); + void reloadTab(int index); + void reloadAllTabs(); + void tabMoveRequested(int fromIndex, int toIndex); + +public: + TabBar(QWidget *parent = 0); + +protected: + void mousePressEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); + +private slots: + void selectTabAction(); + void cloneTab(); + void closeTab(); + void closeOtherTabs(); + void reloadTab(); + void contextMenuRequested(const QPoint &position); + +private: + QList<QShortcut*> m_tabShortcuts; + friend class TabWidget; + + QPoint m_dragStartPos; + int m_dragCurrentIndex; +}; + +#include <QWebPage> + +QT_BEGIN_NAMESPACE +class QAction; +QT_END_NAMESPACE +class WebView; +/*! + A proxy object that connects a single browser action + to one child webpage action at a time. + + Example usage: used to keep the main window stop action in sync with + the current tabs webview's stop action. + */ +class WebActionMapper : public QObject +{ + Q_OBJECT + +public: + WebActionMapper(QAction *root, QWebPage::WebAction webAction, QObject *parent); + QWebPage::WebAction webAction() const; + void addChild(QAction *action); + void updateCurrent(QWebPage *currentParent); + +private slots: + void rootTriggered(); + void childChanged(); + void rootDestroyed(); + void currentDestroyed(); + +private: + QWebPage *m_currentParent; + QAction *m_root; + QWebPage::WebAction m_webAction; +}; + +#include <QtCore/QUrl> +#include <QtWidgets/QTabWidget> +QT_BEGIN_NAMESPACE +class QCompleter; +class QLineEdit; +class QMenu; +class QStackedWidget; +QT_END_NAMESPACE +/*! + TabWidget that contains WebViews and a stack widget of associated line edits. + + Connects up the current tab's signals to this class's signal and uses WebActionMapper + to proxy the actions. + */ +class TabWidget : public QTabWidget +{ + Q_OBJECT + +signals: + // tab widget signals + void loadPage(const QString &url); + void tabsChanged(); + void lastTabClosed(); + + // current tab signals + void setCurrentTitle(const QString &url); + void showStatusBarMessage(const QString &message); + void linkHovered(const QString &link); + void loadProgress(int progress); + void geometryChangeRequested(const QRect &geometry); + void menuBarVisibilityChangeRequested(bool visible); + void statusBarVisibilityChangeRequested(bool visible); + void toolBarVisibilityChangeRequested(bool visible); + void printRequested(QWebFrame *frame); + +public: + TabWidget(QWidget *parent = 0); + void clear(); + void addWebAction(QAction *action, QWebPage::WebAction webAction); + + QAction *newTabAction() const; + QAction *closeTabAction() const; + QAction *recentlyClosedTabsAction() const; + QAction *nextTabAction() const; + QAction *previousTabAction() const; + + QWidget *lineEditStack() const; + QLineEdit *currentLineEdit() const; + WebView *currentWebView() const; + WebView *webView(int index) const; + QLineEdit *lineEdit(int index) const; + int webViewIndex(WebView *webView) const; + + QByteArray saveState() const; + bool restoreState(const QByteArray &state); + +protected: + void mouseDoubleClickEvent(QMouseEvent *event); + void contextMenuEvent(QContextMenuEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + +public slots: + void loadUrlInCurrentTab(const QUrl &url); + WebView *newTab(bool makeCurrent = true); + void cloneTab(int index = -1); + void closeTab(int index = -1); + void closeOtherTabs(int index); + void reloadTab(int index = -1); + void reloadAllTabs(); + void nextTab(); + void previousTab(); + +private slots: + void currentChanged(int index); + void aboutToShowRecentTabsMenu(); + void aboutToShowRecentTriggeredAction(QAction *action); + void webViewLoadStarted(); + void webViewIconChanged(); + void webViewTitleChanged(const QString &title); + void webViewUrlChanged(const QUrl &url); + void lineEditReturnPressed(); + void windowCloseRequested(); + void moveTab(int fromIndex, int toIndex); + +private: + QAction *m_recentlyClosedTabsAction; + QAction *m_newTabAction; + QAction *m_closeTabAction; + QAction *m_nextTabAction; + QAction *m_previousTabAction; + + QMenu *m_recentlyClosedTabsMenu; + static const int m_recentlyClosedTabsSize = 10; + QList<QUrl> m_recentlyClosedTabs; + QList<WebActionMapper*> m_actions; + + QCompleter *m_lineEditCompleter; + QStackedWidget *m_lineEdits; + TabBar *m_tabBar; +}; + +#endif // TABWIDGET_H diff --git a/examples/widgets/browser/toolbarsearch.cpp b/examples/widgets/browser/toolbarsearch.cpp new file mode 100644 index 000000000..415be66c4 --- /dev/null +++ b/examples/widgets/browser/toolbarsearch.cpp @@ -0,0 +1,163 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "toolbarsearch.h" +#include "autosaver.h" + +#include <QtCore/QSettings> +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> + +#include <QtWidgets/QCompleter> +#include <QtWidgets/QMenu> +#include <QtCore/QStringListModel> + +#include <QWebSettings> + +/* + ToolbarSearch is a very basic search widget that also contains a small history. + Searches are turned into urls that use Google to perform search + */ +ToolbarSearch::ToolbarSearch(QWidget *parent) + : SearchLineEdit(parent) + , m_autosaver(new AutoSaver(this)) + , m_maxSavedSearches(10) + , m_stringListModel(new QStringListModel(this)) +{ + QMenu *m = menu(); + connect(m, SIGNAL(aboutToShow()), this, SLOT(aboutToShowMenu())); + connect(m, SIGNAL(triggered(QAction*)), this, SLOT(triggeredMenuAction(QAction*))); + + QCompleter *completer = new QCompleter(m_stringListModel, this); + completer->setCompletionMode(QCompleter::InlineCompletion); + lineEdit()->setCompleter(completer); + + connect(lineEdit(), SIGNAL(returnPressed()), SLOT(searchNow())); + setInactiveText(tr("Google")); + load(); +} + +ToolbarSearch::~ToolbarSearch() +{ + m_autosaver->saveIfNeccessary(); +} + +void ToolbarSearch::save() +{ + QSettings settings; + settings.beginGroup(QLatin1String("toolbarsearch")); + settings.setValue(QLatin1String("recentSearches"), m_stringListModel->stringList()); + settings.setValue(QLatin1String("maximumSaved"), m_maxSavedSearches); + settings.endGroup(); +} + +void ToolbarSearch::load() +{ + QSettings settings; + settings.beginGroup(QLatin1String("toolbarsearch")); + QStringList list = settings.value(QLatin1String("recentSearches")).toStringList(); + m_maxSavedSearches = settings.value(QLatin1String("maximumSaved"), m_maxSavedSearches).toInt(); + m_stringListModel->setStringList(list); + settings.endGroup(); +} + +void ToolbarSearch::searchNow() +{ + QString searchText = lineEdit()->text(); + QStringList newList = m_stringListModel->stringList(); + if (newList.contains(searchText)) + newList.removeAt(newList.indexOf(searchText)); + newList.prepend(searchText); + if (newList.size() >= m_maxSavedSearches) + newList.removeLast(); + + QWebSettings *globalSettings = QWebSettings::globalSettings(); + if (!globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled)) { + m_stringListModel->setStringList(newList); + m_autosaver->changeOccurred(); + } + + QUrl url(QLatin1String("http://www.google.com/search")); + QUrlQuery urlQuery; + urlQuery.addQueryItem(QLatin1String("q"), searchText); + urlQuery.addQueryItem(QLatin1String("ie"), QLatin1String("UTF-8")); + urlQuery.addQueryItem(QLatin1String("oe"), QLatin1String("UTF-8")); + urlQuery.addQueryItem(QLatin1String("client"), QLatin1String("qtdemobrowser")); + url.setQuery(urlQuery); + emit search(url); +} + +void ToolbarSearch::aboutToShowMenu() +{ + lineEdit()->selectAll(); + QMenu *m = menu(); + m->clear(); + QStringList list = m_stringListModel->stringList(); + if (list.isEmpty()) { + m->addAction(tr("No Recent Searches")); + return; + } + + QAction *recent = m->addAction(tr("Recent Searches")); + recent->setEnabled(false); + for (int i = 0; i < list.count(); ++i) { + QString text = list.at(i); + m->addAction(text)->setData(text); + } + m->addSeparator(); + m->addAction(tr("Clear Recent Searches"), this, SLOT(clear())); +} + +void ToolbarSearch::triggeredMenuAction(QAction *action) +{ + QVariant v = action->data(); + if (v.canConvert<QString>()) { + QString text = v.toString(); + lineEdit()->setText(text); + searchNow(); + } +} + +void ToolbarSearch::clear() +{ + m_stringListModel->setStringList(QStringList()); + m_autosaver->changeOccurred();; +} diff --git a/examples/widgets/browser/toolbarsearch.h b/examples/widgets/browser/toolbarsearch.h new file mode 100644 index 000000000..1189785db --- /dev/null +++ b/examples/widgets/browser/toolbarsearch.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TOOLBARSEARCH_H +#define TOOLBARSEARCH_H + +#include "searchlineedit.h" + +QT_BEGIN_NAMESPACE +class QUrl; +class QAction; +class QStringListModel; +QT_END_NAMESPACE + +class AutoSaver; + +class ToolbarSearch : public SearchLineEdit +{ + Q_OBJECT + +signals: + void search(const QUrl &url); + +public: + ToolbarSearch(QWidget *parent = 0); + ~ToolbarSearch(); + +public slots: + void clear(); + void searchNow(); + +private slots: + void save(); + void aboutToShowMenu(); + void triggeredMenuAction(QAction *action); + +private: + void load(); + + AutoSaver *m_autosaver; + int m_maxSavedSearches; + QStringListModel *m_stringListModel; +}; + +#endif // TOOLBARSEARCH_H diff --git a/examples/widgets/browser/urllineedit.cpp b/examples/widgets/browser/urllineedit.cpp new file mode 100644 index 000000000..47828d95f --- /dev/null +++ b/examples/widgets/browser/urllineedit.cpp @@ -0,0 +1,342 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "urllineedit.h" + +#include "browserapplication.h" +#include "searchlineedit.h" +#include "webview.h" + +#include <QtCore/QEvent> +#include <QtCore/QMimeData> + +#include <QtWidgets/QApplication> +#include <QtWidgets/QCompleter> +#include <QtGui/QFocusEvent> +#include <QtWidgets/QHBoxLayout> +#include <QtWidgets/QLabel> +#include <QtWidgets/QLineEdit> +#include <QtGui/QDrag> +#include <QtGui/QPainter> +#include <QtWidgets/QStyle> +#include <QtWidgets/QStyleOptionFrameV2> + +#include <QtCore/QDebug> + +ExLineEdit::ExLineEdit(QWidget *parent) + : QWidget(parent) + , m_leftWidget(0) + , m_lineEdit(new QLineEdit(this)) + , m_clearButton(0) +{ + setFocusPolicy(m_lineEdit->focusPolicy()); + setAttribute(Qt::WA_InputMethodEnabled); + setSizePolicy(m_lineEdit->sizePolicy()); + setBackgroundRole(m_lineEdit->backgroundRole()); + setMouseTracking(true); + setAcceptDrops(true); + setAttribute(Qt::WA_MacShowFocusRect, true); + QPalette p = m_lineEdit->palette(); + setPalette(p); + + // line edit + m_lineEdit->setFrame(false); + m_lineEdit->setFocusProxy(this); + m_lineEdit->setAttribute(Qt::WA_MacShowFocusRect, false); + QPalette clearPalette = m_lineEdit->palette(); + clearPalette.setBrush(QPalette::Base, QBrush(Qt::transparent)); + m_lineEdit->setPalette(clearPalette); + + // clearButton + m_clearButton = new ClearButton(this); + connect(m_clearButton, SIGNAL(clicked()), + m_lineEdit, SLOT(clear())); + connect(m_lineEdit, SIGNAL(textChanged(QString)), + m_clearButton, SLOT(textChanged(QString))); +} + +void ExLineEdit::setLeftWidget(QWidget *widget) +{ + m_leftWidget = widget; +} + +QWidget *ExLineEdit::leftWidget() const +{ + return m_leftWidget; +} + +void ExLineEdit::resizeEvent(QResizeEvent *event) +{ + Q_ASSERT(m_leftWidget); + updateGeometries(); + QWidget::resizeEvent(event); +} + +void ExLineEdit::updateGeometries() +{ + QStyleOptionFrameV2 panel; + initStyleOption(&panel); + QRect rect = style()->subElementRect(QStyle::SE_LineEditContents, &panel, this); + + int height = rect.height(); + int width = rect.width(); + + int m_leftWidgetHeight = m_leftWidget->height(); + m_leftWidget->setGeometry(rect.x() + 2, rect.y() + (height - m_leftWidgetHeight)/2, + m_leftWidget->width(), m_leftWidget->height()); + + int clearButtonWidth = this->height(); + m_lineEdit->setGeometry(m_leftWidget->x() + m_leftWidget->width(), 0, + width - clearButtonWidth - m_leftWidget->width(), this->height()); + + m_clearButton->setGeometry(this->width() - clearButtonWidth, 0, + clearButtonWidth, this->height()); +} + +void ExLineEdit::initStyleOption(QStyleOptionFrameV2 *option) const +{ + option->initFrom(this); + option->rect = contentsRect(); + option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, option, this); + option->midLineWidth = 0; + option->state |= QStyle::State_Sunken; + if (m_lineEdit->isReadOnly()) + option->state |= QStyle::State_ReadOnly; +#ifdef QT_KEYPAD_NAVIGATION + if (hasEditFocus()) + option->state |= QStyle::State_HasEditFocus; +#endif + option->features = QStyleOptionFrameV2::None; +} + +QSize ExLineEdit::sizeHint() const +{ + m_lineEdit->setFrame(true); + QSize size = m_lineEdit->sizeHint(); + m_lineEdit->setFrame(false); + return size; +} + +void ExLineEdit::focusInEvent(QFocusEvent *event) +{ + m_lineEdit->event(event); + QWidget::focusInEvent(event); +} + +void ExLineEdit::focusOutEvent(QFocusEvent *event) +{ + m_lineEdit->event(event); + + if (m_lineEdit->completer()) { + connect(m_lineEdit->completer(), SIGNAL(activated(QString)), + m_lineEdit, SLOT(setText(QString))); + connect(m_lineEdit->completer(), SIGNAL(highlighted(QString)), + m_lineEdit, SLOT(_q_completionHighlighted(QString))); + } + QWidget::focusOutEvent(event); +} + +void ExLineEdit::keyPressEvent(QKeyEvent *event) +{ + m_lineEdit->event(event); +} + +bool ExLineEdit::event(QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) + return m_lineEdit->event(event); + return QWidget::event(event); +} + +void ExLineEdit::paintEvent(QPaintEvent *) +{ + QPainter p(this); + QStyleOptionFrameV2 panel; + initStyleOption(&panel); + style()->drawPrimitive(QStyle::PE_PanelLineEdit, &panel, &p, this); +} + +QVariant ExLineEdit::inputMethodQuery(Qt::InputMethodQuery property) const +{ + return m_lineEdit->inputMethodQuery(property); +} + +void ExLineEdit::inputMethodEvent(QInputMethodEvent *e) +{ + m_lineEdit->event(e); +} + + +class UrlIconLabel : public QLabel +{ + +public: + UrlIconLabel(QWidget *parent); + + WebView *m_webView; + +protected: + void mousePressEvent(QMouseEvent *event); + void mouseMoveEvent(QMouseEvent *event); + +private: + QPoint m_dragStartPos; + +}; + +UrlIconLabel::UrlIconLabel(QWidget *parent) + : QLabel(parent) + , m_webView(0) +{ + setMinimumWidth(16); + setMinimumHeight(16); +} + +void UrlIconLabel::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) + m_dragStartPos = event->pos(); + QLabel::mousePressEvent(event); +} + +void UrlIconLabel::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::LeftButton + && (event->pos() - m_dragStartPos).manhattanLength() > QApplication::startDragDistance() + && m_webView) { + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setText(QString::fromUtf8(m_webView->url().toEncoded())); + QList<QUrl> urls; + urls.append(m_webView->url()); + mimeData->setUrls(urls); + drag->setMimeData(mimeData); + drag->exec(); + } +} + +UrlLineEdit::UrlLineEdit(QWidget *parent) + : ExLineEdit(parent) + , m_webView(0) + , m_iconLabel(0) +{ + // icon + m_iconLabel = new UrlIconLabel(this); + m_iconLabel->resize(16, 16); + setLeftWidget(m_iconLabel); + m_defaultBaseColor = palette().color(QPalette::Base); + + webViewIconChanged(); +} + +void UrlLineEdit::setWebView(WebView *webView) +{ + Q_ASSERT(!m_webView); + m_webView = webView; + m_iconLabel->m_webView = webView; + connect(webView, SIGNAL(urlChanged(QUrl)), + this, SLOT(webViewUrlChanged(QUrl))); + connect(webView, SIGNAL(loadFinished(bool)), + this, SLOT(webViewIconChanged())); + connect(webView, SIGNAL(iconChanged()), + this, SLOT(webViewIconChanged())); + connect(webView, SIGNAL(loadProgress(int)), + this, SLOT(update())); +} + +void UrlLineEdit::webViewUrlChanged(const QUrl &url) +{ + m_lineEdit->setText(QString::fromUtf8(url.toEncoded())); + m_lineEdit->setCursorPosition(0); +} + +void UrlLineEdit::webViewIconChanged() +{ + QUrl url = (m_webView) ? m_webView->url() : QUrl(); + QIcon icon = BrowserApplication::instance()->icon(url); + QPixmap pixmap(icon.pixmap(16, 16)); + m_iconLabel->setPixmap(pixmap); +} + +QLinearGradient UrlLineEdit::generateGradient(const QColor &color) const +{ + QLinearGradient gradient(0, 0, 0, height()); + gradient.setColorAt(0, m_defaultBaseColor); + gradient.setColorAt(0.15, color.lighter(120)); + gradient.setColorAt(0.5, color); + gradient.setColorAt(0.85, color.lighter(120)); + gradient.setColorAt(1, m_defaultBaseColor); + return gradient; +} + +void UrlLineEdit::focusOutEvent(QFocusEvent *event) +{ + if (m_lineEdit->text().isEmpty() && m_webView) + m_lineEdit->setText(QString::fromUtf8(m_webView->url().toEncoded())); + ExLineEdit::focusOutEvent(event); +} + +void UrlLineEdit::paintEvent(QPaintEvent *event) +{ + QPalette p = palette(); + if (m_webView && m_webView->url().scheme() == QLatin1String("https")) { + QColor lightYellow(248, 248, 210); + p.setBrush(QPalette::Base, generateGradient(lightYellow)); + } else { + p.setBrush(QPalette::Base, m_defaultBaseColor); + } + setPalette(p); + ExLineEdit::paintEvent(event); + + QPainter painter(this); + QStyleOptionFrameV2 panel; + initStyleOption(&panel); + QRect backgroundRect = style()->subElementRect(QStyle::SE_LineEditContents, &panel, this); + if (m_webView && !hasFocus()) { + int progress = m_webView->progress(); + QColor loadingColor = QColor(116, 192, 250); + painter.setBrush(generateGradient(loadingColor)); + painter.setPen(Qt::transparent); + int mid = backgroundRect.width() / 100 * progress; + QRect progressRect(backgroundRect.x(), backgroundRect.y(), mid, backgroundRect.height()); + painter.drawRect(progressRect); + } +} diff --git a/examples/widgets/browser/urllineedit.h b/examples/widgets/browser/urllineedit.h new file mode 100644 index 000000000..c30f40b57 --- /dev/null +++ b/examples/widgets/browser/urllineedit.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef URLLINEEDIT_H +#define URLLINEEDIT_H + +#include <QtCore/QUrl> +#include <QtWidgets/QWidget> +#include <QtWidgets/QStyleOptionFrame> + +QT_BEGIN_NAMESPACE +class QLineEdit; +QT_END_NAMESPACE + +class ClearButton; +class ExLineEdit : public QWidget +{ + Q_OBJECT + +public: + ExLineEdit(QWidget *parent = 0); + + inline QLineEdit *lineEdit() const { return m_lineEdit; } + + void setLeftWidget(QWidget *widget); + QWidget *leftWidget() const; + + QSize sizeHint() const; + + QVariant inputMethodQuery(Qt::InputMethodQuery property) const; +protected: + void focusInEvent(QFocusEvent *event); + void focusOutEvent(QFocusEvent *event); + void keyPressEvent(QKeyEvent *event); + void paintEvent(QPaintEvent *event); + void resizeEvent(QResizeEvent *event); + void inputMethodEvent(QInputMethodEvent *e); + bool event(QEvent *event); + +protected: + void updateGeometries(); + void initStyleOption(QStyleOptionFrameV2 *option) const; + + QWidget *m_leftWidget; + QLineEdit *m_lineEdit; + ClearButton *m_clearButton; +}; + +class UrlIconLabel; +class WebView; +class UrlLineEdit : public ExLineEdit +{ + Q_OBJECT + +public: + UrlLineEdit(QWidget *parent = 0); + void setWebView(WebView *webView); + +protected: + void paintEvent(QPaintEvent *event); + void focusOutEvent(QFocusEvent *event); + +private slots: + void webViewUrlChanged(const QUrl &url); + void webViewIconChanged(); + +private: + QLinearGradient generateGradient(const QColor &color) const; + WebView *m_webView; + UrlIconLabel *m_iconLabel; + QColor m_defaultBaseColor; + +}; + + +#endif // URLLINEEDIT_H diff --git a/examples/widgets/browser/webview.cpp b/examples/widgets/browser/webview.cpp new file mode 100644 index 000000000..be6f7de53 --- /dev/null +++ b/examples/widgets/browser/webview.cpp @@ -0,0 +1,315 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "browserapplication.h" +#include "browsermainwindow.h" +#include "cookiejar.h" +#include "downloadmanager.h" +#include "networkaccessmanager.h" +#include "tabwidget.h" +#include "webview.h" + +#include <QtGui/QClipboard> +#include <QtWidgets/QMenu> +#include <QtWidgets/QMessageBox> +#include <QtGui/QMouseEvent> + +#include <QWebHitTestResult> + +#ifndef QT_NO_UITOOLS +#include <QtUiTools/QUiLoader> +#endif //QT_NO_UITOOLS + +#include <QtCore/QDebug> +#include <QtCore/QBuffer> + +WebPage::WebPage(QObject *parent) + : QWebPage(parent) + , m_keyboardModifiers(Qt::NoModifier) + , m_pressedButtons(Qt::NoButton) + , m_openInNewTab(false) +{ + setNetworkAccessManager(BrowserApplication::networkAccessManager()); + connect(this, SIGNAL(unsupportedContent(QNetworkReply*)), + this, SLOT(handleUnsupportedContent(QNetworkReply*))); +} + +BrowserMainWindow *WebPage::mainWindow() +{ + QObject *w = this->parent(); + while (w) { + if (BrowserMainWindow *mw = qobject_cast<BrowserMainWindow*>(w)) + return mw; + w = w->parent(); + } + return BrowserApplication::instance()->mainWindow(); +} + +bool WebPage::acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type) +{ + // ctrl open in new tab + // ctrl-shift open in new tab and select + // ctrl-alt open in new window + if (type == QWebPage::NavigationTypeLinkClicked + && (m_keyboardModifiers & Qt::ControlModifier + || m_pressedButtons == Qt::MidButton)) { + bool newWindow = (m_keyboardModifiers & Qt::AltModifier); + WebView *webView; + if (newWindow) { + BrowserApplication::instance()->newMainWindow(); + BrowserMainWindow *newMainWindow = BrowserApplication::instance()->mainWindow(); + webView = newMainWindow->currentTab(); + newMainWindow->raise(); + newMainWindow->activateWindow(); + webView->setFocus(); + } else { + bool selectNewTab = (m_keyboardModifiers & Qt::ShiftModifier); + webView = mainWindow()->tabWidget()->newTab(selectNewTab); + } + webView->load(request); + m_keyboardModifiers = Qt::NoModifier; + m_pressedButtons = Qt::NoButton; + return false; + } + if (frame == mainFrame()) { + m_loadingUrl = request.url(); + emit loadingUrl(m_loadingUrl); + } + return QWebPage::acceptNavigationRequest(frame, request, type); +} + +QWebPage *WebPage::createWindow(QWebPage::WebWindowType type) +{ + Q_UNUSED(type); + if (m_keyboardModifiers & Qt::ControlModifier || m_pressedButtons == Qt::MidButton) + m_openInNewTab = true; + if (m_openInNewTab) { + m_openInNewTab = false; + return mainWindow()->tabWidget()->newTab()->page(); + } + BrowserApplication::instance()->newMainWindow(); + BrowserMainWindow *mainWindow = BrowserApplication::instance()->mainWindow(); + return mainWindow->currentTab()->page(); +} + +#if !defined(QT_NO_UITOOLS) +QObject *WebPage::createPlugin(const QString &classId, const QUrl &url, const QStringList ¶mNames, const QStringList ¶mValues) +{ + Q_UNUSED(url); + Q_UNUSED(paramNames); + Q_UNUSED(paramValues); + QUiLoader loader; + return loader.createWidget(classId, view()); +} +#endif // !defined(QT_NO_UITOOLS) + +void WebPage::handleUnsupportedContent(QNetworkReply *reply) +{ + QString errorString = reply->errorString(); + + if (m_loadingUrl != reply->url()) { + // sub resource of this page + qWarning() << "Resource" << reply->url().toEncoded() << "has unknown Content-Type, will be ignored."; + reply->deleteLater(); + return; + } + + if (reply->error() == QNetworkReply::NoError && !reply->header(QNetworkRequest::ContentTypeHeader).isValid()) { + errorString = "Unknown Content-Type"; + } + + QFile file(QLatin1String(":/notfound.html")); + bool isOpened = file.open(QIODevice::ReadOnly); + Q_ASSERT(isOpened); + Q_UNUSED(isOpened) + + QString title = tr("Error loading page: %1").arg(reply->url().toString()); + QString html = QString(QLatin1String(file.readAll())) + .arg(title) + .arg(errorString) + .arg(reply->url().toString()); + + QBuffer imageBuffer; + imageBuffer.open(QBuffer::ReadWrite); + QIcon icon = view()->style()->standardIcon(QStyle::SP_MessageBoxWarning, 0, view()); + QPixmap pixmap = icon.pixmap(QSize(32,32)); + if (pixmap.save(&imageBuffer, "PNG")) { + html.replace(QLatin1String("IMAGE_BINARY_DATA_HERE"), + QString(QLatin1String(imageBuffer.buffer().toBase64()))); + } + + QList<QWebFrame*> frames; + frames.append(mainFrame()); + while (!frames.isEmpty()) { + QWebFrame *frame = frames.takeFirst(); + if (frame->url() == reply->url()) { + frame->setHtml(html, reply->url()); + return; + } + QList<QWebFrame *> children = frame->childFrames(); + foreach (QWebFrame *frame, children) + frames.append(frame); + } + if (m_loadingUrl == reply->url()) { + mainFrame()->setHtml(html, reply->url()); + } +} + + +WebView::WebView(QWidget* parent) + : QWebView(parent) + , m_progress(0) + , m_page(new WebPage(this)) +{ + setPage(m_page); + connect(page(), SIGNAL(statusBarMessage(QString)), + SLOT(setStatusBarText(QString))); + connect(this, SIGNAL(loadProgress(int)), + this, SLOT(setProgress(int))); + connect(this, SIGNAL(loadFinished(bool)), + this, SLOT(loadFinished())); + connect(page(), SIGNAL(loadingUrl(QUrl)), + this, SIGNAL(urlChanged(QUrl))); + connect(page(), SIGNAL(downloadRequested(QNetworkRequest)), + this, SLOT(downloadRequested(QNetworkRequest))); + page()->setForwardUnsupportedContent(true); + +} + +void WebView::contextMenuEvent(QContextMenuEvent *event) +{ + QWebHitTestResult r = page()->mainFrame()->hitTestContent(event->pos()); + if (!r.linkUrl().isEmpty()) { + QMenu menu(this); + menu.addAction(pageAction(QWebPage::OpenLinkInNewWindow)); + menu.addAction(tr("Open in New Tab"), this, SLOT(openLinkInNewTab())); + menu.addSeparator(); + menu.addAction(pageAction(QWebPage::DownloadLinkToDisk)); + // Add link to bookmarks... + menu.addSeparator(); + menu.addAction(pageAction(QWebPage::CopyLinkToClipboard)); + if (page()->settings()->testAttribute(QWebSettings::DeveloperExtrasEnabled)) + menu.addAction(pageAction(QWebPage::InspectElement)); + menu.exec(mapToGlobal(event->pos())); + return; + } + QWebView::contextMenuEvent(event); +} + +void WebView::wheelEvent(QWheelEvent *event) +{ + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + int numDegrees = event->delta() / 8; + int numSteps = numDegrees / 15; + setTextSizeMultiplier(textSizeMultiplier() + numSteps * 0.1); + event->accept(); + return; + } + QWebView::wheelEvent(event); +} + +void WebView::openLinkInNewTab() +{ + m_page->m_openInNewTab = true; + pageAction(QWebPage::OpenLinkInNewWindow)->trigger(); +} + +void WebView::setProgress(int progress) +{ + m_progress = progress; +} + +void WebView::loadFinished() +{ + if (100 != m_progress) { + qWarning() << "Received finished signal while progress is still:" << progress() + << "Url:" << url(); + } + m_progress = 0; +} + +void WebView::loadUrl(const QUrl &url) +{ + m_initialUrl = url; + load(url); +} + +QString WebView::lastStatusBarText() const +{ + return m_statusBarText; +} + +QUrl WebView::url() const +{ + QUrl url = QWebView::url(); + if (!url.isEmpty()) + return url; + + return m_initialUrl; +} + +void WebView::mousePressEvent(QMouseEvent *event) +{ + m_page->m_pressedButtons = event->buttons(); + m_page->m_keyboardModifiers = event->modifiers(); + QWebView::mousePressEvent(event); +} + +void WebView::mouseReleaseEvent(QMouseEvent *event) +{ + QWebView::mouseReleaseEvent(event); + if (!event->isAccepted() && (m_page->m_pressedButtons & Qt::MidButton)) { + QUrl url(QApplication::clipboard()->text(QClipboard::Selection)); + if (!url.isEmpty() && url.isValid() && !url.scheme().isEmpty()) { + setUrl(url); + } + } +} + +void WebView::setStatusBarText(const QString &string) +{ + m_statusBarText = string; +} + +void WebView::downloadRequested(const QNetworkRequest &request) +{ + BrowserApplication::downloadManager()->download(request); +} diff --git a/examples/widgets/browser/webview.h b/examples/widgets/browser/webview.h new file mode 100644 index 000000000..4fd04aca9 --- /dev/null +++ b/examples/widgets/browser/webview.h @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBVIEW_H +#define WEBVIEW_H + +#include <QWebView> + +QT_BEGIN_NAMESPACE +class QAuthenticator; +class QMouseEvent; +class QNetworkProxy; +class QNetworkReply; +class QSslError; +QT_END_NAMESPACE + +class BrowserMainWindow; +class WebPage : public QWebPage { + Q_OBJECT + +signals: + void loadingUrl(const QUrl &url); + +public: + WebPage(QObject *parent = 0); + BrowserMainWindow *mainWindow(); + +protected: + bool acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type); + QWebPage *createWindow(QWebPage::WebWindowType type); +#if !defined(QT_NO_UITOOLS) + QObject *createPlugin(const QString &classId, const QUrl &url, const QStringList ¶mNames, const QStringList ¶mValues); +#endif + +private slots: + void handleUnsupportedContent(QNetworkReply *reply); + +private: + friend class WebView; + + // set the webview mousepressedevent + Qt::KeyboardModifiers m_keyboardModifiers; + Qt::MouseButtons m_pressedButtons; + bool m_openInNewTab; + QUrl m_loadingUrl; +}; + +class WebView : public QWebView { + Q_OBJECT + +public: + WebView(QWidget *parent = 0); + WebPage *webPage() const { return m_page; } + + void loadUrl(const QUrl &url); + QUrl url() const; + + QString lastStatusBarText() const; + inline int progress() const { return m_progress; } + +protected: + void mousePressEvent(QMouseEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + void contextMenuEvent(QContextMenuEvent *event); + void wheelEvent(QWheelEvent *event); + +private slots: + void setProgress(int progress); + void loadFinished(); + void setStatusBarText(const QString &string); + void downloadRequested(const QNetworkRequest &request); + void openLinkInNewTab(); + +private: + QString m_statusBarText; + QUrl m_initialUrl; + int m_progress; + WebPage *m_page; +}; + +#endif diff --git a/examples/widgets/browser/xbel.cpp b/examples/widgets/browser/xbel.cpp new file mode 100644 index 000000000..ae84a6a37 --- /dev/null +++ b/examples/widgets/browser/xbel.cpp @@ -0,0 +1,282 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "xbel.h" + +#include <QtCore/QFile> + +BookmarkNode::BookmarkNode(BookmarkNode::Type type, BookmarkNode *parent) : + expanded(false) + , m_parent(parent) + , m_type(type) +{ + if (parent) + parent->add(this); +} + +BookmarkNode::~BookmarkNode() +{ + if (m_parent) + m_parent->remove(this); + qDeleteAll(m_children); + m_parent = 0; + m_type = BookmarkNode::Root; +} + +bool BookmarkNode::operator==(const BookmarkNode &other) +{ + if (url != other.url + || title != other.title + || desc != other.desc + || expanded != other.expanded + || m_type != other.m_type + || m_children.count() != other.m_children.count()) + return false; + + for (int i = 0; i < m_children.count(); ++i) + if (!((*(m_children[i])) == (*(other.m_children[i])))) + return false; + return true; +} + +BookmarkNode::Type BookmarkNode::type() const +{ + return m_type; +} + +void BookmarkNode::setType(Type type) +{ + m_type = type; +} + +QList<BookmarkNode *> BookmarkNode::children() const +{ + return m_children; +} + +BookmarkNode *BookmarkNode::parent() const +{ + return m_parent; +} + +void BookmarkNode::add(BookmarkNode *child, int offset) +{ + Q_ASSERT(child->m_type != Root); + if (child->m_parent) + child->m_parent->remove(child); + child->m_parent = this; + if (-1 == offset) + offset = m_children.size(); + m_children.insert(offset, child); +} + +void BookmarkNode::remove(BookmarkNode *child) +{ + child->m_parent = 0; + m_children.removeAll(child); +} + + +XbelReader::XbelReader() +{ +} + +BookmarkNode *XbelReader::read(const QString &fileName) +{ + QFile file(fileName); + if (!file.exists()) { + return new BookmarkNode(BookmarkNode::Root); + } + file.open(QFile::ReadOnly); + return read(&file); +} + +BookmarkNode *XbelReader::read(QIODevice *device) +{ + BookmarkNode *root = new BookmarkNode(BookmarkNode::Root); + setDevice(device); + if (readNextStartElement()) { + QString version = attributes().value(QLatin1String("version")).toString(); + if (name() == QLatin1String("xbel") + && (version.isEmpty() || version == QLatin1String("1.0"))) { + readXBEL(root); + } else { + raiseError(QObject::tr("The file is not an XBEL version 1.0 file.")); + } + } + return root; +} + +void XbelReader::readXBEL(BookmarkNode *parent) +{ + Q_ASSERT(isStartElement() && name() == QLatin1String("xbel")); + + while (readNextStartElement()) { + if (name() == QLatin1String("folder")) + readFolder(parent); + else if (name() == QLatin1String("bookmark")) + readBookmarkNode(parent); + else if (name() == QLatin1String("separator")) + readSeparator(parent); + else + skipCurrentElement(); + } +} + +void XbelReader::readFolder(BookmarkNode *parent) +{ + Q_ASSERT(isStartElement() && name() == QLatin1String("folder")); + + BookmarkNode *folder = new BookmarkNode(BookmarkNode::Folder, parent); + folder->expanded = (attributes().value(QLatin1String("folded")) == QLatin1String("no")); + + while (readNextStartElement()) { + if (name() == QLatin1String("title")) + readTitle(folder); + else if (name() == QLatin1String("desc")) + readDescription(folder); + else if (name() == QLatin1String("folder")) + readFolder(folder); + else if (name() == QLatin1String("bookmark")) + readBookmarkNode(folder); + else if (name() == QLatin1String("separator")) + readSeparator(folder); + else + skipCurrentElement(); + } +} + +void XbelReader::readTitle(BookmarkNode *parent) +{ + Q_ASSERT(isStartElement() && name() == QLatin1String("title")); + parent->title = readElementText(); +} + +void XbelReader::readDescription(BookmarkNode *parent) +{ + Q_ASSERT(isStartElement() && name() == QLatin1String("desc")); + parent->desc = readElementText(); +} + +void XbelReader::readSeparator(BookmarkNode *parent) +{ + new BookmarkNode(BookmarkNode::Separator, parent); + // empty elements have a start and end element + readNext(); +} + +void XbelReader::readBookmarkNode(BookmarkNode *parent) +{ + Q_ASSERT(isStartElement() && name() == QLatin1String("bookmark")); + BookmarkNode *bookmark = new BookmarkNode(BookmarkNode::Bookmark, parent); + bookmark->url = attributes().value(QLatin1String("href")).toString(); + while (readNextStartElement()) { + if (name() == QLatin1String("title")) + readTitle(bookmark); + else if (name() == QLatin1String("desc")) + readDescription(bookmark); + else + skipCurrentElement(); + } + if (bookmark->title.isEmpty()) + bookmark->title = QObject::tr("Unknown title"); +} + + +XbelWriter::XbelWriter() +{ + setAutoFormatting(true); +} + +bool XbelWriter::write(const QString &fileName, const BookmarkNode *root) +{ + QFile file(fileName); + if (!root || !file.open(QFile::WriteOnly)) + return false; + return write(&file, root); +} + +bool XbelWriter::write(QIODevice *device, const BookmarkNode *root) +{ + setDevice(device); + + writeStartDocument(); + writeDTD(QLatin1String("<!DOCTYPE xbel>")); + writeStartElement(QLatin1String("xbel")); + writeAttribute(QLatin1String("version"), QLatin1String("1.0")); + if (root->type() == BookmarkNode::Root) { + for (int i = 0; i < root->children().count(); ++i) + writeItem(root->children().at(i)); + } else { + writeItem(root); + } + + writeEndDocument(); + return true; +} + +void XbelWriter::writeItem(const BookmarkNode *parent) +{ + switch (parent->type()) { + case BookmarkNode::Folder: + writeStartElement(QLatin1String("folder")); + writeAttribute(QLatin1String("folded"), parent->expanded ? QLatin1String("no") : QLatin1String("yes")); + writeTextElement(QLatin1String("title"), parent->title); + for (int i = 0; i < parent->children().count(); ++i) + writeItem(parent->children().at(i)); + writeEndElement(); + break; + case BookmarkNode::Bookmark: + writeStartElement(QLatin1String("bookmark")); + if (!parent->url.isEmpty()) + writeAttribute(QLatin1String("href"), parent->url); + writeTextElement(QLatin1String("title"), parent->title); + if (!parent->desc.isEmpty()) + writeAttribute(QLatin1String("desc"), parent->desc); + writeEndElement(); + break; + case BookmarkNode::Separator: + writeEmptyElement(QLatin1String("separator")); + break; + default: + break; + } +} diff --git a/examples/widgets/browser/xbel.h b/examples/widgets/browser/xbel.h new file mode 100644 index 000000000..8ee9558ef --- /dev/null +++ b/examples/widgets/browser/xbel.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the demonstration applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef XBEL_H +#define XBEL_H + +#include <QtCore/QXmlStreamReader> +#include <QtCore/QDateTime> + +class BookmarkNode +{ +public: + enum Type { + Root, + Folder, + Bookmark, + Separator + }; + + BookmarkNode(Type type = Root, BookmarkNode *parent = 0); + ~BookmarkNode(); + bool operator==(const BookmarkNode &other); + + Type type() const; + void setType(Type type); + QList<BookmarkNode *> children() const; + BookmarkNode *parent() const; + + void add(BookmarkNode *child, int offset = -1); + void remove(BookmarkNode *child); + + QString url; + QString title; + QString desc; + bool expanded; + +private: + BookmarkNode *m_parent; + Type m_type; + QList<BookmarkNode *> m_children; + +}; + +class XbelReader : public QXmlStreamReader +{ +public: + XbelReader(); + BookmarkNode *read(const QString &fileName); + BookmarkNode *read(QIODevice *device); + +private: + void readXBEL(BookmarkNode *parent); + void readTitle(BookmarkNode *parent); + void readDescription(BookmarkNode *parent); + void readSeparator(BookmarkNode *parent); + void readFolder(BookmarkNode *parent); + void readBookmarkNode(BookmarkNode *parent); +}; + +#include <QtCore/QXmlStreamWriter> + +class XbelWriter : public QXmlStreamWriter +{ +public: + XbelWriter(); + bool write(const QString &fileName, const BookmarkNode *root); + bool write(QIODevice *device, const BookmarkNode *root); + +private: + void writeItem(const BookmarkNode *parent); +}; + +#endif // XBEL_H -- GitLab