diff options
51 files changed, 1186 insertions, 993 deletions
@@ -11,6 +11,7 @@ tools/src/crashhandler/defaults.go .config .config.old compile_commands.json +*.roff lib/configuration/test/corpus diff --git a/doc/man/genroff.sh b/doc/man/genroff.sh new file mode 100755 index 0000000..1d513f4 --- /dev/null +++ b/doc/man/genroff.sh @@ -0,0 +1,4 @@ +#/usr/bin/env sh + +scdoc < $1 + diff --git a/doc/man/smolbote-profile.5.scd b/doc/man/smolbote-profile.5.scd new file mode 100644 index 0000000..115e6ed --- /dev/null +++ b/doc/man/smolbote-profile.5.scd @@ -0,0 +1,80 @@ +smolbote(5) "smolbote profile configuration" + +# NAME + +smolbote profile format + +# DESCRIPTION + +A profile is a collection of settings, policies, scripts, cookies, cache and +history. They store their data separately, and can be used to isolate pages from +each other. + +Off-the-record profiles only use in-memory cache and don't keep cookies or +history between sessions. They are useful as a private browsing mode. + +Each subwindow has a default profile it uses when opening new tabs. This can be +changed from the _Subwindow_ menu. Additionally, tabs can have their profiles +individually changed from the _Page_ menu. + +Profiles can be either temporary or permanent. Temporary profiles expire when +the application is closed, whereas permanent profiles are kept between runs. + +The browser will save changes made to profiles automatically. If you want to +prevent modifications, set the corresponding .profile to read-only. + +# SECTIONS + +Profiles are defined in an INI format file with a `.profile` extension. The file +name is used as the profile's ID. +If no value is specified, the default value is used instead. + +## General +- `name`: Name (Default: same as the ID) +- `otr`: Off-the-record toggle, true or false (Default: true) +- `search`: The search engine URL, with the following format: + https://engine.url/q=%1, where %1 will be substituted by the search term. +- `homepage`: The homepage URL. (Default: about:blank) +- `newtab`: The URL that should be loaded by default when opening a new tab with + this profile. (Default: about:blank) + +## properties +- `cachePath` +- `persistentStoragePath` +- `persistentCookiesPolicy` +- `httpAcceptLanguage` +- `httpCacheMaximumSize` +- `httpCacheType` +- `httpUserAgent` +- `spellCheckEnabled` + +See the QtWebEngine documentation[0] for more information. + +## attributes +QWebEngineSettings::WebAttribute, see the QtWebEngine documentation[1] for more +information. + +## headers +HTTP headers that should be enforced by the profile. + +# EXAMPLES +To check where profiles are read from, you can use: + $ poi configuration --dump | grep profile.path + +Creating a `otr.profile` file in that location will create a permanent +off-the-record profile with an ID of `otr`. You can customize it further: + +``` +[General] +name=off-the-record + +[headers] +Dnt=1 +``` + +This will name the profile `off-the-record` in menus, and cause it to send out a +Dnt (Do not track) header. + +# SEE ALSO +0: https://doc.qt.io/qt-5/qwebengineprofile.html++ +1: https://doc.qt.io/qt-5/qwebenginesettings.html diff --git a/doc/man/smolbote.1.scd b/doc/man/smolbote.1.scd index 4ca55b7..758fda3 100644 --- a/doc/man/smolbote.1.scd +++ b/doc/man/smolbote.1.scd @@ -10,66 +10,29 @@ smolbote - yet another no-frills web browser # DESCRIPTION -smolbote is a cross-platform keep-it-simple free software web browser that -uses Qt and QtWebEngine. +smolbote is a cross-platform keep-it-simple free software web browser that uses Qt and QtWebEngine. # USAGE ## Command-line options - `-h`, `--help`: Display command-line options list. - `-v`, `--version`: Display version information. -- `--build`: Display build commit. +- `-b`, `--build`: Display build commit. - `-c`, `--config`: Set configuration file. - `--no-remote`: Don't check for other instances when starting. - `-s, --session`: Open the selected session. - `--pick-session`: Open all available sessions and select which one to open. -## Profiles -A Profile is a collection of settings, policies, scripts, cookies, cache and -history. Profiles can be used to isolate pages from each other. - -Off-the-record profiles only use in-memory cache and save no files to disk. - -Profiles can be either temporary or permanent. Temporary profiles expire when -the application is closed, whereas permanent profiles are kept between runs. - -Each window has a default profile it uses when opening new tabs. This can be -changed from the window's menu. Additionally, tabs can have their profiles -individually changed from their context menu. - -Because profiles store all their data separately, you can log in into the same -site with a different account from each profile. However, links opened into new -tabs will still use the subwindow's default profile. For example, you can set a -profile to hold login information for a site, but all new tabs opened from that -site would still be using the default off-the-record profile. - -## Plugins -Plugins a way of extending smolbote's functionality using the Qt plugin system. -They are not to be confused with NPAPI/PPAPI or WebExtension plugins. - -To enable a plugin either copy it or symlink it in the plugins.path location, -or set its absolute path as the plugins.path. - -## Filters -smolbote has a singular URL request filter that is installed onto all profiles. -Any setting applied to it will be applied to all profiles. - -filter.header: A list of header-value pairs, separated by a colon (':'). - -You can specify multiple headers by using --filter.header multiple times: -``` -poi --filter.header "Dnt:1" --filter.header "Accept:text/html" -``` - # SEE ALSO -*smolbote*(5) - configuration file and options +*smolboterc*(5) - configuration file and options++ +*smolbote-profile*(5) - profiles and how to use them # AUTHORS Maintained by <aqua@iserlohn-fortress.net>. -Up-to-date sources can be found at https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote +Up-to-date sources can be found at https://neueland.iserlohn-fortress.net/cgit/smolbote Bug reports and patches can be submitted by email to <aqua@iserlohn-fortress.net>. diff --git a/doc/man/smolbote.5.scd b/doc/man/smolbote.5.scd deleted file mode 100644 index 063a050..0000000 --- a/doc/man/smolbote.5.scd +++ /dev/null @@ -1,106 +0,0 @@ -smolbote(5) "smolbote configuration" - -# NAME - -smolbote - configuration file and options description - -# DESCRIPTION - -The smolbote configuration is loaded at startup, and is not reloaded when -changed. By default (without any plugins), the browser does not change its -configuration or overwrite this file. Thus, it can be made read-only. - -The settings in this file change your preferences and keybindings. - -Lines starting with *#* are considered comments and ignored. - -Options taking multiple values (list options) can have multiple values set by -using the same option name multiple times. - -# SECTIONS - -## Browser Options - -*browser.stylesheet* (arg):: TODO - -*browser.iconTheme* (arg):: -Set arg as icon theme. - -*browser.locale* (arg):: -Set Qt localization. This will translate Qt strings, such as the dialog buttons. -For possible values see `/usr/share/qt/translations`. - -*browser.translation* (arg):: -Set browser localization. This will translate the rest of the browser. For -possible values, see `install-root/share/smolbote/lang`. - -*browser.session.path* (arg=~/.config/smolbote/session.d):: -Location where browser sessions should be saved and loaded from by default. - -*plugins.path* (arg=~/.config/smolbote/plugins.d):: -Location where plugins should be loaded from. - -*bookmarks.path* (arg=~/.config/smolbote/bookmarks.xbel):: -Default bookmarks location. -*bookmarks.shortcut* (arg=Ctrl+B):: -Show/Hide bookmakrs widget shortcut. - -*downloads.path* (arg=~/Downloads):: -Default downloads location. -*downloads.shortcut* (arg=Ctrl+D):: -Show/Hide downloads widget shortcut. - -## UI Options - -*mainwindow.height* (arg=720) -*mainwindow.width* (arg=1280) -*mainwindow.maximized* (arg=1):: Default window size and maximize toggle. - -*mainwindow.title* (arg=smolbote):: Default window title. - -*mainwindow.shortcuts.saveSession* (arg=Ctrl+S,S):: Save Session shortcut -*mainwindow.shortcuts.openSession* (arg=Ctrl+S,O):: Open Session shortcut -*mainwindow.shortcuts.newGroup* (arg=Ctrl+G):: New Group shortcut (subwindow) -*mainwindow.shortcuts.newWindow* (arg=Ctrl+N):: New Window shortcut -*mainwindow.shortcuts.about* (arg=F1):: About dialog shortcut -*mainwindow.shortcuts.quit* (arg=Ctrl+Q):: Quit shortcut -*mainwindow.shortcuts.search* (arg=F3):: Search in page shortcut -*mainwindow.shortcuts.tileWindows* (arg=F9):: Tile subwindows shortcut -*mainwindow.shortcuts.cascadeWindows* (arg=F10):: Cascade subwindows shortcut - -*navigation.movable* (arg=0):: Make navigation bar movable -*navigation.shortcuts.back* (arg=Ctrl+Left):: Back shortcut -*navigation.shortcuts.backMenu* (arg=Ctrl+Down):: Back menu shortcut -*navigation.shortcuts.forward* (arg=Ctrl+Right):: Forward shortcut -*navigation.shortcuts.forwardMenu* (arg=Ctrl+Up):: Forward menu shortcut -*navigation.shortcuts.refresh* (arg=F5):: Refresh shortcut -*navigation.shortcuts.reload* (arg=Ctrl+F5):: Reload shortcut -*navigation.shortcuts.home* (arg=Ctrl+Home):: Home shortcut - -*addressbar.shortcuts.focus* (arg=F4):: Focus on the address -*addressbar.shortcuts.menu* (arg=F2):: Show addressbar menu - -*subwindow.shortcuts.menu* (arg=Ctrl+M):: Show subwindow menu -*subwindow.shortcuts.new* (arg=Ctrl+T):: Create new tab shortcut -*subwindow.shortcuts.close* (arg=Ctrl+X):: Close tab shortcut -*subwindow.shortcuts.restoreTab* (arg=Ctrl+Shift+T):: Restore last closed tab -*subwindow.shortcuts.left* (arg=Ctrl+O):: Move to tab on the left -*subwindow.shortcuts.moveLeft* (arg=Ctrl+Shift+O):: Move tab to the left -*subwindow.shortcuts.right* (arg=Ctrl+P):: Move to tab on the right -*subwindow.shortcuts.moveRight* (arg=Ctrl+Shift+P):: Move tab to the right -*subwindow.shortcuts.fullscreen* (arg=F11):: Show page fullscreen - -## Security Options - -*filter.hosts* (arg=~/.config/smolbote/hosts.d):: Hostlist -*filter.adblock* arg:: TODO -*filter.header* (list):: -A list of HTTP headers to set. Each header should be given as a colon-separated -name:value pair. - -*profile.default* (arg):: Default browser profile -*profile.path* (arg=~/.config/smolbote/profiles.d):: Profile location -*profile.search* (arg=`https://duckduckgo.com/?q=%1&ia=web`):: -Default search engine. %1 is replaced by the search term. -*profile.homepage* (arg=`about:blank`):: Default homepage -*profile.newtab* (arg=`about:blank`):: Default new tab page diff --git a/doc/man/smolboterc.5.scd b/doc/man/smolboterc.5.scd new file mode 100644 index 0000000..f340c01 --- /dev/null +++ b/doc/man/smolboterc.5.scd @@ -0,0 +1,21 @@ +smolbote(5) "smolbote configuration" + +# NAME + +smolbote - configuration file and options description + +# DESCRIPTION + +The smolbote configuration is loaded at startup, and is not reloaded when +changed. By default (without any plugins), the browser does not change its +configuration or overwrite this file. Thus, it can be made read-only. + +The settings in this file change your preferences and keybindings. + +Lines starting with *#* are considered comments and ignored. + +Options taking multiple values (list options) can have multiple values set by +using the same option name multiple times. + +# SECTIONS + diff --git a/doc/meson.build b/doc/meson.build index 26e5d6c..6490969 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,33 +1,16 @@ -scdoc = find_program('scdoc', required: get_option('manpage'), disabler: true) -if scdoc.found() +if not get_option('manpage') + subdir_done() +endif sh = find_program('sh', required: true, native: true, disabler: true) -man_files = ['man/smolbote.1.scd', 'man/smolbote.5.scd'] - -foreach input : man_files - topic = input.split('/')[-1].split('.')[-3] - section = input.split('.')[-2] - output = '@0@.@1@'.format(topic, section) - - message('creating manpage target ' + output) - custom_target(output, - build_by_default: true, - - input: input, - output: output, - - # scdoc takes input from stdin, and prints its output to stdout - # meson uses 'capture' to store stdout to output, but there is no stdin toggle - #command: [scdoc], - #capture: true, - - # workaround using sh - command: [sh, '-c', '@0@ < @INPUT0@ > @OUTPUT0@'.format(scdoc.path())], - - install: true, - install_dir: '@0@/man@1@'.format(get_option('mandir'), section) - ) +foreach f : [ 'smolbote.1', 'smolboterc.5', 'smolbote-profile.5' ] +manpage = custom_target(f, + input: 'man'/f+'.scd', + output: '@BASENAME@', + capture: true, + command: [ sh, meson.current_source_dir()/'man/genroff.sh', '@INPUT@' ], + install: true, + install_dir: get_option('mandir') +) endforeach - -endif # manpage diff --git a/include/meson.build b/include/meson.build index 7601966..1cd9957 100644 --- a/include/meson.build +++ b/include/meson.build @@ -1,4 +1,5 @@ plugininterfaces_include = include_directories('.') +smolbote_include = include_directories('.') install_headers('smolbote/filterinterface.hpp', 'smolbote/plugininterface.hpp', subdir: 'smolbote') diff --git a/include/singleton.hpp b/include/singleton.hpp new file mode 100644 index 0000000..d13e29e --- /dev/null +++ b/include/singleton.hpp @@ -0,0 +1,27 @@ +#ifndef SMOLBOTE_SINGLETON_HPP +#define SMOLBOTE_SINGLETON_HPP + +/* + * Clang consumed semantics + * States can be: unconsumed, consumed, unknown + * Mark classes with consumable(unconsumed) + * Mark constructors with return_typestate(unconsumed) + * Mark invalidating functions with set_typestate(consumed) + */ + +#if defined(__clang__) +#define consumable(X) [[clang::consumable(X)]] +#define return_typestate(X) [[clang::return_typestate(X)]] +#define set_typestate(X) [[clang::set_typestate(X)]] +#define callable_when(X) [[clang::callable_when(X)]] +#define param_typestate(X) [[clang::param_typestate(X)]] + +#else +#define consumable(X) +#define return_typestate(X) +#define set_typestate(X) +#define callable_when(X) +#define param_typestate(X) +#endif + +#endif // SMOLBOTE_SINGLETON_HPP diff --git a/lib/configuration/configuration.h b/lib/configuration/configuration.h index 09c41a0..cd3c244 100644 --- a/lib/configuration/configuration.h +++ b/lib/configuration/configuration.h @@ -42,7 +42,7 @@ public: return_typestate(unconsumed) Configuration(); return_typestate(unconsumed) Configuration(std::initializer_list<std::pair<std::string, conf_value_t>> l) noexcept; - return_typestate(unconsumed) Configuration(const Configuration &other param_typestate(unconsumed)) = default; + Configuration(const Configuration &) = delete; Configuration &operator=(const Configuration &) = delete; return_typestate(unconsumed) Configuration(Configuration && other param_typestate(unconsumed)) = default; diff --git a/lib/configuration/test/main.cpp b/lib/configuration/test/main.cpp index 21a02cf..fa04f10 100644 --- a/lib/configuration/test/main.cpp +++ b/lib/configuration/test/main.cpp @@ -1,5 +1,7 @@ #define CATCH_CONFIG_RUNNER +// clazy:excludeall=non-pod-global-static + #include "configuration.h" #include <QApplication> #include <catch2/catch.hpp> diff --git a/lib/pluginloader/test/pluginloader-sigmatch.cpp b/lib/pluginloader/test/pluginloader-sigmatch.cpp index 991d9bc..0f4789a 100644 --- a/lib/pluginloader/test/pluginloader-sigmatch.cpp +++ b/lib/pluginloader/test/pluginloader-sigmatch.cpp @@ -1,5 +1,7 @@ #define CATCH_CONFIG_MAIN +// clazy:excludeall=non-pod-global-static + #include "pluginloader.h" #include <catch2/catch.hpp> diff --git a/lib/session_formats/test/json.cpp b/lib/session_formats/test/json.cpp index 4c6b683..e8cccb3 100644 --- a/lib/session_formats/test/json.cpp +++ b/lib/session_formats/test/json.cpp @@ -1,4 +1,7 @@ #define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file + +// clazy:excludeall=non-pod-global-static + #include <catch2/catch.hpp> #include <session_json.hpp> diff --git a/meson.build b/meson.build index adee6c2..3cf7105 100644 --- a/meson.build +++ b/meson.build @@ -31,14 +31,18 @@ add_project_arguments(cxx.get_supported_arguments([ '-ffunction-sections', # Place each function into its own section, better ASLR but larger executables '-fstack-protector-all', # Emit code to check for buffer overflows on all functions '-fstack-clash-protection', # Emit code to check for stack clash attacks - '-fconcepts', - '-mspeculative-load-hardening', # Spectre v1 mitigation + # gcc specific + '-fconcepts', # gcc9 c++20 compat - '-Wconsumed', # (clang) use-after-move warnings - '-Wdate-time', # Warn when using __TIME__ and __DATE__ macros - '-Wimplicit-fallthrough', - '-Wold-style-cast' + # clang specific + '-mspeculative-load-hardening', # Spectre v1 mitigation + '-Wconsumed', # use-after-move warnings + '-Xclang -plugin-arg-clazy -Xclang level0,level1', # clazy default warning level + + '-Wdate-time', # __TIME__ and __DATE__ macros + '-Wimplicit-fallthrough', # switch implicit fallthrough + '-Wold-style-cast' # c-style casts ]), language: 'cpp') if get_option('buildtype') == 'release' @@ -49,7 +53,7 @@ endif mod_qt5 = import('qt5') dep_qt5 = dependency('qt5', - modules: [ 'Core', 'Network', 'Widgets', 'Svg', 'WebEngineWidgets', 'Concurrent', 'Test' ], + modules: [ 'Core', 'Network', 'Widgets', 'Svg', 'WebEngine', 'WebEngineWidgets', 'Concurrent' ], include_type: 'system' ) diff --git a/meson_options.txt b/meson_options.txt index 1bbda86..eb39c03 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,7 +1,7 @@ option('poi', description: 'Executable name', type: 'string', value: 'poi') option('firejail', description: 'firejail executable name', type: 'string', value: '/usr/bin/firejail') -option('manpage', description: 'Generate and install manpage', type: 'feature', value: 'auto') +option('manpage', description: 'Generate and install manpage', type: 'boolean', value: 'true') option('translations', description: 'Generate and install translations', type: 'feature', value: 'auto') option('crashhandler', description: 'Enable breakpad crash reporting', type: 'feature', value: 'auto') option('updater', description: 'Build updater component', type: 'feature', value: 'auto') diff --git a/src/about/aboutplugin.cpp b/src/about/aboutplugin.cpp index 99c04ec..92e55bb 100644 --- a/src/about/aboutplugin.cpp +++ b/src/about/aboutplugin.cpp @@ -10,6 +10,7 @@ #include "ui_aboutplugin.h" #include <QJsonArray> #include <QPluginLoader> +#include <QToolButton> QTreeWidgetItem *createItem(const QString &key, const QJsonValue &json, QTreeWidgetItem *parent) { @@ -54,58 +55,50 @@ QTreeWidgetItem *createItem(const QString &key, const QJsonValue &json, QTreeWid return item; } -AboutPluginDialog::AboutPluginDialog(QPluginLoader *loader, QWidget *parent) +AboutPluginDialog::AboutPluginDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::AboutPluginDialog) { setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); - // load button icon - { - QIcon load_icon; - load_icon.addPixmap(style()->standardPixmap(QStyle::SP_MediaPlay), QIcon::Normal, QIcon::On); - load_icon.addPixmap(style()->standardPixmap(QStyle::SP_MediaStop), QIcon::Normal, QIcon::Off); - ui->load->setIcon(load_icon); - } - - auto metaData = loader->metaData()["MetaData"].toObject(); - - this->setWindowTitle(metaData["name"].toString()); - - ui->path->setText(loader->fileName()); - ui->load->setChecked(loader->isLoaded()); - - connect(ui->load, &QToolButton::clicked, this, [this, loader](bool checked) { - if(checked) { - // load plugin - if(!loader->load()) { - ui->load->setChecked(false); - ui->error->setText(loader->errorString()); - } - } else { - // unload plugin - if(!loader->unload()) { - ui->load->setChecked(true); - ui->error->setText(loader->errorString()); - } - } - }); - - ui->name->setText(metaData[QLatin1String("name")].toString()); - ui->author->setText(metaData[QLatin1String("author")].toString()); - ui->license->setText(metaData[QLatin1String("license")].toString()); - ui->shortcut->setText(metaData[QLatin1String("shortcut")].toString()); - for(const QString &key : loader->metaData().keys()) { - auto *i = createItem(key, loader->metaData()[key], nullptr); - - if(i != nullptr) - ui->details_treeWidget->insertTopLevelItem(0, i); - } } AboutPluginDialog::~AboutPluginDialog() { delete ui; } + +void AboutPluginDialog::add(QPluginLoader *loader) +{ + const auto index = ui->tableWidget->rowCount(); + ui->tableWidget->setRowCount(index + 1); + + const auto metadata = loader->metaData()["MetaData"].toObject(); + ui->tableWidget->setItem(index, 0, new QTableWidgetItem(metadata.value("name").toString())); + ui->tableWidget->setItem(index, 1, new QTableWidgetItem(metadata.value("author").toString())); + ui->tableWidget->setItem(index, 2, new QTableWidgetItem(metadata.value("license").toString())); + ui->tableWidget->setItem(index, 3, new QTableWidgetItem(metadata.value("shortcut").toString())); + ui->tableWidget->setItem(index, 5, new QTableWidgetItem(loader->fileName())); + + auto *enable_btn = new QToolButton(this); + enable_btn->setCheckable(true); + enable_btn->setChecked(loader->isLoaded()); + ui->tableWidget->setCellWidget(index, 4, enable_btn); + + connect(enable_btn, &QToolButton::clicked, this, [ loader, enable_btn ](bool checked) { + const bool success = checked ? loader->load() : loader->unload(); + if(!success) { + enable_btn->setChecked(!checked); + //ui->error->setText(loader->errorString()); + } + }); + + for(const QString &key : metadata.keys()) { + auto *i = createItem(key, metadata.value(key), nullptr); + if(i != nullptr) { + ui->details_treeWidget->insertTopLevelItem(0, i); + } + } +} diff --git a/src/about/aboutplugin.h b/src/about/aboutplugin.h index 9651060..b01cc78 100644 --- a/src/about/aboutplugin.h +++ b/src/about/aboutplugin.h @@ -21,9 +21,12 @@ class AboutPluginDialog : public QDialog Q_OBJECT public: - explicit AboutPluginDialog(QPluginLoader *loader, QWidget *parent = nullptr); + explicit AboutPluginDialog(QWidget *parent = nullptr); ~AboutPluginDialog() override; +public slots: + void add(QPluginLoader *loader); + private: Ui::AboutPluginDialog *ui; }; diff --git a/src/about/aboutplugin.ui b/src/about/aboutplugin.ui index 5df1c0d..d4291e5 100644 --- a/src/about/aboutplugin.ui +++ b/src/about/aboutplugin.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>500</width> - <height>400</height> + <width>600</width> + <height>420</height> </rect> </property> <property name="windowTitle"> @@ -15,135 +15,54 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QTabWidget" name="tabWidget"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="general_tab"> - <attribute name="title"> - <string>General</string> - </attribute> - <layout class="QFormLayout" name="formLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="name_label"> - <property name="text"> - <string>Name</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="name"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="author_label"> - <property name="text"> - <string>Author</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QLabel" name="author"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QLabel" name="shortcut_label"> - <property name="text"> - <string>Shortcut</string> - </property> - </widget> - </item> - <item row="3" column="1"> - <widget class="QLabel" name="shortcut"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="4" column="1"> - <layout class="QHBoxLayout" name="controls_layout"/> - </item> - <item row="2" column="1"> - <widget class="QLabel" name="license"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QLabel" name="license_label"> - <property name="text"> - <string>License</string> - </property> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="details_tab"> - <attribute name="title"> - <string>Details</string> - </attribute> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QLabel" name="path"> - <property name="text"> - <string>TextLabel</string> - </property> - </widget> - </item> - <item> - <widget class="QTreeWidget" name="details_treeWidget"> - <column> - <property name="text"> - <string>Key</string> - </property> - </column> - <column> - <property name="text"> - <string>Value</string> - </property> - </column> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="controls_tab"> - <attribute name="title"> - <string>Controls</string> - </attribute> - <layout class="QFormLayout" name="formLayout_2"> - <item row="0" column="1"> - <widget class="QToolButton" name="load"> - <property name="text"> - <string>Load</string> - </property> - <property name="checkable"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QLabel" name="error"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="error_label"> - <property name="text"> - <string>Status</string> - </property> - </widget> - </item> - </layout> - </widget> + <widget class="QTableWidget" name="tableWidget"> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Author</string> + </property> + </column> + <column> + <property name="text"> + <string>License</string> + </property> + </column> + <column> + <property name="text"> + <string>Shortcut</string> + </property> + </column> + <column> + <property name="text"> + <string>Enable</string> + </property> + </column> + <column> + <property name="text"> + <string>Path</string> + </property> + </column> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="details_treeWidget"> + <column> + <property name="text"> + <string>Key</string> + </property> + </column> + <column> + <property name="text"> + <string>Value</string> + </property> + </column> </widget> </item> <item> diff --git a/src/applicationmenu.cpp b/src/applicationmenu.cpp new file mode 100644 index 0000000..b7acd57 --- /dev/null +++ b/src/applicationmenu.cpp @@ -0,0 +1,68 @@ +/* + * This file is part of smolbote. It's copyrighted by the contributors recorded + * in the version control history of the file, available from its original + * location: https://neueland.iserlohn-fortress.net/cgit/smolbote + * + * SPDX-License-Identifier: GPL-3.0 + */ + +#include "applicationmenu.h" +#include "browser.h" +#include "configuration.h" +#include "session/savesessiondialog.h" +#include "session/sessiondialog.h" +#include "smolbote/plugininterface.hpp" +#include <QPluginLoader> + +ApplicationMenu::ApplicationMenu(Browser *app, QWidget *parent) + : QMenu(parent) +{ + setTitle(qApp->applicationName()); + Configuration conf; + + const auto sessionPath = conf.value<QString>("session.path").value(); + auto *actionSaveSession = addAction(tr("Save Session"), this, [sessionPath]() { + auto *sessionDialog = new SaveSessionDialog(nullptr); + if(sessionDialog->exec() == QDialog::Accepted) { + sessionDialog->save(sessionPath); + } + }); + conf.shortcut<QAction>(*actionSaveSession, "shortcuts.session.save"); + + auto *actionOpenSession = addAction(tr("Open Session"), this, []() { + auto *sessionDialog = new SessionDialog(nullptr); + sessionDialog->exec(); + }); + conf.shortcut<QAction>(*actionOpenSession, "shortcuts.session.open"); + + bottom_pluginSeparator = addSeparator(); + + auto *actionAbout = addAction(tr("About"), app, &Browser::about); + conf.shortcut<QAction>(*actionAbout, "shortcuts.window.about"); + + auto *action_aboutPlugins = addAction(tr("About Plugins"), app, &Browser::aboutPlugins); + conf.shortcut(*action_aboutPlugins, "app.shortcuts.about.plugins"); + + auto *actionQuit = addAction(tr("Quit"), app, &Browser::quit); + conf.shortcut<QAction>(*actionQuit, "shortcuts.window.quit"); +} + +void ApplicationMenu::addPlugin(QPluginLoader *plugin) +{ + if(top_pluginSeparator == nullptr) { + top_pluginSeparator = insertSeparator(bottom_pluginSeparator); + } + + const auto metadata = plugin->metaData().value("MetaData").toObject(); + auto *action = addAction(metadata.value("name").toString()); + action->setShortcut(QKeySequence::fromString(metadata.value("shortcut").toString())); + + connect(action, &QAction::triggered, this, [this, plugin]() { + if(plugin->isLoaded()) { + if(const auto *interface = qobject_cast<PluginInterface *>(plugin->instance())) { + interface->createWidget(this->parentWidget())->exec(); + } + } + }); + insertAction(bottom_pluginSeparator, action); +} diff --git a/src/applicationmenu.h b/src/applicationmenu.h new file mode 100644 index 0000000..7e1fe47 --- /dev/null +++ b/src/applicationmenu.h @@ -0,0 +1,31 @@ +/* + * This file is part of smolbote. It's copyrighted by the contributors recorded + * in the version control history of the file, available from its original + * location: https://neueland.iserlohn-fortress.net/cgit/smolbote + * + * SPDX-License-Identifier: GPL-3.0 + */ + +#ifndef SMOLBOTE_APPLICATION_MENU_H +#define SMOLBOTE_APPLICATION_MENU_H + +#include <QMenu> + +class Browser; +class QPluginLoader; +class ApplicationMenu : public QMenu +{ + Q_OBJECT + +public: + ApplicationMenu(Browser *app, QWidget *parent = nullptr); + ~ApplicationMenu() = default; + +public slots: + void addPlugin(QPluginLoader *plugin); + +private: + QAction *top_pluginSeparator = nullptr, *bottom_pluginSeparator = nullptr; +}; + +#endif // SMOLBOTE_APPLICATION_MENU_H diff --git a/src/browser.cpp b/src/browser.cpp index 61be3b0..2277e54 100644 --- a/src/browser.cpp +++ b/src/browser.cpp @@ -10,6 +10,7 @@ #include "aboutdialog.h" #include "aboutplugin.h" #include "adblock/adblocklist.h" +#include "applicationmenu.h" #include "bookmarks/bookmarkswidget.h" #include "configuration.h" #include "downloadswidget.h" @@ -19,19 +20,12 @@ #include "mainwindow/menubar.h" #include "smolbote/plugininterface.hpp" #include "subwindow/subwindow.h" -#include "urlfilter.h" #include "util.h" #include "webengine/urlinterceptor.h" #include "webengine/webprofile.h" #include "webengine/webprofilemanager.h" #include "webengine/webview.h" #include <QAction> -#include <QDir> -#include <QFileDialog> -#include <QFileInfo> -#include <QFileInfoList> -#include <QJsonArray> -#include <QJsonDocument> #include <QLibraryInfo> #include <QPluginLoader> #include <QTimer> @@ -89,31 +83,13 @@ void Browser::about() dlg->exec(); } -void Browser::loadProfiles(const QStringList &profilePaths) +void Browser::aboutPlugins() { - Configuration conf; - - for(const QString &path : profilePaths) { - const QString id = QFileInfo(path).baseName(); - - auto *profile = m_profileManager->add(id, path); - connect(profile, &WebProfile::downloadRequested, m_downloads.get(), &DownloadsWidget::addDownload); - - auto *interceptor = new UrlRequestInterceptor(profile, profile); - for(UrlFilter *filter : qAsConst(m_filters)) { - interceptor->addFilter(filter); - } - - const auto headers = conf.value<QStringList>("filter.header").value_or(QStringList()); - for(const QString &header : headers) { - const auto h = header.split(QLatin1String(":")); - if(h.length() == 2) - interceptor->addHttpHeader(h.at(0).toLatin1(), h.at(1).toLatin1()); - } - profile->setUrlRequestInterceptor(interceptor); - - spdlog::info("Added profile\t{}{}\t{}", qUtf8Printable(id), profile->isOffTheRecord() ? "*" : "", qUtf8Printable(profile->name())); + auto *dlg = new AboutPluginDialog; + for(auto *info : m_plugins) { + dlg->add(info->loader); } + dlg->exec(); } void Browser::loadPlugins(const QStringList &paths, const std::function<void(const QPluginLoader *)> &callback) @@ -161,38 +137,23 @@ void Browser::setup() // downloads m_downloads = std::make_unique<DownloadsWidget>(conf.value<QString>("downloads.path").value()); - // url request filter - for(const QString &hostlist : Util::files(conf.value<QString>("filter.hosts").value_or(QString()))) { - QFile f(hostlist); - if(f.open(QIODevice::ReadOnly | QIODevice::Text)) { - m_filters.append(new HostList(&f)); - f.close(); - } - } - for(const QString &adblock : Util::files(conf.value<QString>("filter.adblock").value_or(QString()))) { - QFile f(adblock); - if(f.open(QIODevice::ReadOnly | QIODevice::Text)) { - m_filters.append(new AdBlockList(&f)); - f.close(); - } - } - // cookie request filter - // load profiles - m_profileManager = new WebProfileManager(this); - WebProfileManager::setInstance(m_profileManager); - loadProfiles(Util::files(conf.value<QString>("profile.path").value(), { "*.profile" })); - - // set default profile { - const QString id = conf.value<QString>("profile.default").value(); - auto *profile = m_profileManager->profile(id); - if(profile == nullptr) { - spdlog::error("Unknown default profile, conjuring one up..."); - profile = m_profileManager->add("default", QString(), true); + const auto profiles = Util::files(conf.value<QString>("profile.path").value(), { "*.profile" }); + const auto search = conf.value<QString>("profile.search").value(); + const auto homepage = QUrl::fromUserInput(conf.value<QString>("profile.homepage").value()); + const auto newtab = QUrl::fromUserInput(conf.value<QString>("profile.newtab").value()); + const auto default_id = conf.value<QString>("profile.default").value(); + m_profileManager = std::make_unique<WebProfileManager<false>>(profiles, default_id, search, homepage, newtab); + m_profileManager->make_global(); + + for(const auto &id : m_profileManager->idList()) { + spdlog::info("Added profile\t{}", qUtf8Printable(id)); } - spdlog::info("Default profile\t{}{}\t{}", qUtf8Printable(id), profile->isOffTheRecord() ? "*" : "", qUtf8Printable(profile->name())); - WebProfile::setDefaultProfile(profile); + + // set default profile + auto *profile = m_profileManager->profile(default_id); + spdlog::info("Default profile\t{}{}\t{}", qUtf8Printable(default_id), profile->isOffTheRecord() ? "*" : "", qUtf8Printable(profile->name())); } // bookmarks @@ -243,15 +204,16 @@ void Browser::open(const QVector<Session::MainWindow> &data, bool merge) } for(const auto &windowData : data) { - // the window will delete itself when it closes, so we don't need to delete it - auto *window = new MainWindow(windowData); - connect(window->addressBar, &AddressBar::complete, m_bookmarks.get(), &BookmarksWidget::search); - + auto *menu = new ApplicationMenu(this); for(auto *info : qAsConst(m_plugins)) { - addPluginTo(info, window); + menu->addPlugin(info->loader); } + auto *window = new MainWindow(windowData, menu); + connect(window->addressBar, &AddressBar::complete, m_bookmarks.get(), &BookmarksWidget::search); + m_windows.append(window); + // the window will delete itself when it closes, so we don't need to delete it connect(window, &MainWindow::destroyed, this, [this, window]() { m_windows.removeOne(window); }); @@ -262,15 +224,9 @@ void Browser::addPluginTo(PluginInfo *info, MainWindow *window) { QPluginLoader *loader = info->loader; auto *pluginMenu = new QMenu(loader->metaData().value("MetaData").toObject().value("name").toString()); - window->m_menuBar->insertPlugin(pluginMenu); + //window->m_menuBar->insertPlugin(pluginMenu); info->menus.append(pluginMenu); - auto *aboutAction = pluginMenu->addAction(tr("About")); - connect(aboutAction, &QAction::triggered, this, [loader, window]() { - auto *dlg = new AboutPluginDialog(loader, window); - dlg->exec(); - }); - auto *runAction = pluginMenu->addAction(tr("Run")); runAction->setShortcut(QKeySequence::fromString(loader->metaData().value("MetaData").toObject().value("shortcut").toString())); @@ -284,7 +240,6 @@ void Browser::addPluginTo(PluginInfo *info, MainWindow *window) auto *removeAction = pluginMenu->addAction(tr("Remove")); connect(removeAction, &QAction::triggered, this, [this, info]() { - ; m_plugins.removeOne(info); delete info; }); diff --git a/src/browser.h b/src/browser.h index 95ce936..74136f1 100644 --- a/src/browser.h +++ b/src/browser.h @@ -10,6 +10,7 @@ #define SMOLBOTE_BROWSER_H #include "session.hpp" +#include "webengine/webprofilemanager.h" #include <QJsonObject> #include <QMenu> #include <QPluginLoader> @@ -18,13 +19,10 @@ #include <memory> #include <singleapplication.h> -class UrlFilter; class Configuration; class BookmarksWidget; class DownloadsWidget; class MainWindow; -class WebProfile; -class WebProfileManager; class Browser final : public SingleApplication { Q_OBJECT @@ -33,15 +31,8 @@ public: explicit Browser(int &argc, char *argv[], bool allowSecondary = true); ~Browser() final; -public slots: - void about(); - -public: - // interface - void loadProfiles(const QStringList &profilePaths); void loadPlugins( - const QStringList &paths, - const std::function<void(const QPluginLoader *)> &callback = [](const auto) {}); + const QStringList &paths, const std::function<void(const QPluginLoader *)> &callback = [](const auto) {}); void setup(); const QVector<MainWindow *> windows() const @@ -59,6 +50,8 @@ public: } public slots: + void about(); + void aboutPlugins(); void showWidget(QWidget *widget, MainWindow *where) const; void open(const QVector<Session::MainWindow> &data, bool merge = true); @@ -84,8 +77,7 @@ private: std::shared_ptr<BookmarksWidget> m_bookmarks; std::unique_ptr<DownloadsWidget> m_downloads; - WebProfileManager *m_profileManager = nullptr; - QVector<UrlFilter *> m_filters; + std::unique_ptr<WebProfileManager<false>> m_profileManager{ nullptr }; QVector<MainWindow *> m_windows; QVector<PluginInfo *> m_plugins; diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index e16f34f..4ec79b1 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -24,7 +24,7 @@ #include <QMessageBox> #include <QStatusBar> -MainWindow::MainWindow(const Session::MainWindow &data, QWidget *parent) +MainWindow::MainWindow(const Session::MainWindow &data, QMenu *appMenu, QWidget *parent) : QMainWindow(parent) { Configuration config; @@ -50,8 +50,8 @@ MainWindow::MainWindow(const Session::MainWindow &data, QWidget *parent) this->addToolBarBreak(); this->addToolBar(new BookmarksToolbar(app->bookmarks()->model(), this)); - m_menuBar = new MenuBar(this); - this->setMenuBar(m_menuBar); + m_menuBar = new MenuBar(appMenu, this); + setMenuBar(m_menuBar); // status bar searchBox = new SearchForm(this); @@ -125,10 +125,9 @@ void MainWindow::createTab(const Session::WebView &data) SubWindow *MainWindow::createSubWindow(const Session::SubWindow &data) { - const auto *profileManager = WebProfileManager::instance(); - Q_CHECK_PTR(profileManager); + WebProfileManager profileManager; - auto *profile = profileManager->profile(data.profile); + auto *profile = profileManager.profile(data.profile); if(profile == nullptr) { profile = WebProfile::defaultProfile(); } diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index 2b9f82b..31f5658 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -34,9 +34,11 @@ public: ToolsMenu }; - explicit MainWindow(const Session::MainWindow &data, QWidget *parent = nullptr); + explicit MainWindow(const Session::MainWindow &data, QMenu *appMenu, QWidget *parent = nullptr); + MainWindow(const MainWindow &) = delete; ~MainWindow() = default; + [[nodiscard]] Session::MainWindow serialize() const { QVector<Session::SubWindow> subwindows(m_subwindows.size()); diff --git a/src/mainwindow/menubar.cpp b/src/mainwindow/menubar.cpp index 83cafee..f8979de 100644 --- a/src/mainwindow/menubar.cpp +++ b/src/mainwindow/menubar.cpp @@ -12,23 +12,17 @@ #include "configuration.h" #include "downloadswidget.h" #include "mainwindow.h" -#include "session/savesessiondialog.h" -#include "session/sessiondialog.h" #include "subwindow/subwindow.h" #include "webengine/webprofilemanager.h" #include "webengine/webview.h" #include "widgets/menusearch.h" -#include <QApplication> #include <QDir> #include <QFileDialog> -#include <QLineEdit> -#include <QMdiArea> #include <QPrintDialog> #include <QPrinter> #include <QPrinterInfo> #include <QToolBar> #include <QVBoxLayout> -#include <QWidgetAction> #include <functional> inline void run_if(SubWindow *_subwindow, const std::function<void(SubWindow *, int)> &f) @@ -70,7 +64,7 @@ inline QDialog *createDevToolsDialog(QWebEnginePage *page) return popup; } -MenuBar::MenuBar(MainWindow *parent) +MenuBar::MenuBar(QMenu *appMenu, MainWindow *parent) : QMenuBar(parent) { m_parent = parent; @@ -78,86 +72,21 @@ MenuBar::MenuBar(MainWindow *parent) Q_CHECK_PTR(browser); Configuration conf; - smolbote = this->addMenu(qApp->applicationName()); - { - auto *findMenu = smolbote->addMenu(tr("Find in menus")); - - auto *findWidget = new QWidgetAction(this); - auto *find_lineEdit = new MenuSearch(this); - findWidget->setDefaultWidget(find_lineEdit); - findMenu->addAction(findWidget); - - connect(findMenu, &QMenu::aboutToShow, [findMenu, find_lineEdit]() { - find_lineEdit->clear(); - const auto actions = findMenu->actions(); - for(int i = 1; i < actions.length(); i++) { - findMenu->removeAction(actions.at(i)); - } - find_lineEdit->setFocus(); - }); - - connect(find_lineEdit, &QLineEdit::textEdited, [this, findMenu](const QString &text) { - // clear menu - const auto actions = findMenu->actions(); - for(int i = 1; i < actions.length(); i++) - findMenu->removeAction(actions.at(i)); - - if(text.isEmpty()) - return; - - // findChildren - for(QAction *a : findChildren<QAction *>()) { - if(a->text().contains(text)) - findMenu->addAction(a); - } - }); - - smolbote->addSeparator(); - - const QString sessionPath = conf.value<QString>("session.path").value(); - auto *actionSaveSession = smolbote->addAction(tr("Save Session"), parent, [sessionPath]() { - auto *sessionDialog = new SaveSessionDialog(nullptr); - if(sessionDialog->exec() == QDialog::Accepted) - sessionDialog->save(sessionPath); - }); - conf.shortcut<QAction>(*actionSaveSession, "shortcuts.session.save"); - - auto *actionOpenSession = smolbote->addAction(tr("Open Session"), parent, [parent]() { - auto *sessionDialog = new SessionDialog(parent); - sessionDialog->exec(); - }); - conf.shortcut<QAction>(*actionOpenSession, "shortcuts.session.open"); - - smolbote->addSeparator(); - auto *actionBookmarks = smolbote->addAction(tr("Bookmarks"), browser, [browser, parent]() { - browser->showWidget(browser->bookmarks(), parent); - }); - conf.shortcut<QAction>(*actionBookmarks, "shortcuts.window.bookmarks.show"); - - auto *actionDownloads = smolbote->addAction(tr("Downloads"), browser, [browser, parent]() { - browser->showWidget(browser->downloads(), parent); - }); - conf.shortcut<QAction>(*actionDownloads, "shortcuts.window.downloads.show"); - - smolbote->addSeparator(); - smolbote->addAction(tr("Load Plugin"), browser, [browser]() { - const QString path = QFileDialog::getOpenFileName(nullptr, tr("Select Plugin"), QDir::homePath(), tr("Plugins (*.so)")); - browser->loadPlugins(QStringList(path)); - }); - - pluginInsertLocation = smolbote->addSeparator(); - - auto *actionAbout = smolbote->addAction(tr("About"), browser, &Browser::about); - conf.shortcut<QAction>(*actionAbout, "shortcuts.window.about"); - - smolbote->addSeparator(); - - auto *actionQuit = smolbote->addAction(tr("Quit"), qApp, &QApplication::quit); - conf.shortcut<QAction>(*actionQuit, "shortcuts.window.quit"); - } + addMenu(appMenu); window = this->addMenu(tr("&Window")); { + auto *actionBookmarks = window->addAction(tr("Bookmarks"), browser, [browser, parent]() { + browser->showWidget(browser->bookmarks(), parent); + }); + conf.shortcut<QAction>(*actionBookmarks, "shortcuts.window.bookmarks.show"); + + auto *actionDownloads = window->addAction(tr("Downloads"), browser, [browser, parent]() { + browser->showWidget(browser->downloads(), parent); + }); + conf.shortcut<QAction>(*actionDownloads, "shortcuts.window.downloads.show"); + window->addSeparator(); + auto *actionNewWindow = window->addAction(tr("New Window"), browser, [browser]() { const Session::MainWindow window; browser->open({ window }, false); @@ -532,11 +461,6 @@ MenuBar::MenuBar(MainWindow *parent) } } -QAction *MenuBar::insertPlugin(QMenu *menu) -{ - return smolbote->insertMenu(pluginInsertLocation, menu); -} - void MenuBar::insertSubWindow(SubWindow *subwindow) { auto *action = window->addAction(subwindow->windowTitle()); diff --git a/src/mainwindow/menubar.h b/src/mainwindow/menubar.h index f369bef..7795816 100644 --- a/src/mainwindow/menubar.h +++ b/src/mainwindow/menubar.h @@ -18,14 +18,11 @@ class MenuBar : public QMenuBar Q_OBJECT public: - MenuBar(MainWindow *parent = nullptr); - - QAction *insertPlugin(QMenu *menu); + MenuBar(QMenu *appMenu, MainWindow *parent = nullptr); void insertSubWindow(SubWindow *subwindow); private: MainWindow *m_parent = nullptr; - QMenu *smolbote = nullptr; QMenu *window = nullptr; QAction *pluginInsertLocation = nullptr; }; diff --git a/src/meson.build b/src/meson.build index 1101b7a..6a4abd8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,14 +8,14 @@ poi_settings_h = custom_target('default_config_value', ) subdir('about') +subdir('webengine') poi_sourceset.add(mod_qt5.preprocess( - moc_headers: ['browser.h', + moc_headers: ['browser.h', 'applicationmenu.h', 'mainwindow/mainwindow.h', 'mainwindow/addressbar.h', 'mainwindow/menubar.h', 'mainwindow/widgets/completer.h', 'mainwindow/widgets/urllineedit.h', 'mainwindow/widgets/dockwidget.h', 'mainwindow/widgets/navigationbar.h', 'mainwindow/widgets/searchform.h', 'bookmarks/bookmarkswidget.h', 'bookmarks/editbookmarkdialog.h', 'session/savesessiondialog.h', 'session/sessiondialog.h', - 'subwindow/subwindow.h', 'subwindow/tabwidget.h', - 'webengine/urlinterceptor.h', 'webengine/webpage.h', 'webengine/webview.h', 'webengine/webprofilemanager.h', 'webengine/webprofile.h'], + 'subwindow/subwindow.h', 'subwindow/tabwidget.h' ], ui_files: [ 'mainwindow/addressbar.ui', 'mainwindow/widgets/searchform.ui', 'bookmarks/bookmarksform.ui', 'bookmarks/editbookmarkdialog.ui', @@ -27,7 +27,7 @@ poi_sourceset.add(mod_qt5.preprocess( poi_sourceset.add(files( 'main.cpp', 'builtins.cpp', - 'browser.cpp', + 'browser.cpp', 'applicationmenu.cpp', 'util.cpp', 'util.h', 'mainwindow/mainwindow.cpp', @@ -48,13 +48,6 @@ poi_sourceset.add(files( 'subwindow/subwindow.cpp', 'subwindow/tabwidget.cpp', - 'webengine/urlinterceptor.cpp', - 'webengine/webpage.cpp', - 'webengine/webview.cpp', - 'webengine/webviewcontextmenu.cpp', - 'webengine/webprofile.cpp', - 'webengine/webprofilemanager.cpp', - 'wallet/wallet.cpp', 'wallet/wallet.h' ), version_h, poi_settings_h diff --git a/src/session/savesessiondialog.cpp b/src/session/savesessiondialog.cpp index ff5e696..e22ce3a 100644 --- a/src/session/savesessiondialog.cpp +++ b/src/session/savesessiondialog.cpp @@ -12,7 +12,7 @@ #include "session_json.hpp" #include "subwindow/subwindow.h" #include "ui_savesessiondialog.h" -#include "webengine/webprofilemanager.h" +#include "webengine/webprofile.h" #include "webengine/webview.h" #include <QFileDialog> #include <QPointer> @@ -37,7 +37,7 @@ SaveSessionDialog::SaveSessionDialog(QWidget *parent) for(const SubWindow *subwindow : window->subWindows()) { auto *subwindowItem = new QTreeWidgetItem(windowItem); subwindowItem->setText(0, tr("Subwindow")); - subwindowItem->setText(1, WebProfileManager::instance()->id(subwindow->profile())); + subwindowItem->setText(1, subwindow->profile()->getId()); ui->treeWidget->expandItem(subwindowItem); @@ -45,7 +45,7 @@ SaveSessionDialog::SaveSessionDialog(QWidget *parent) auto *tabItem = new QTreeWidgetItem(subwindowItem); auto *view = subwindow->view(i); tabItem->setText(0, view->title()); - tabItem->setText(1, WebProfileManager::instance()->id(view->profile())); + tabItem->setText(1, view->profile()->getId()); } } } diff --git a/src/subwindow/subwindow.cpp b/src/subwindow/subwindow.cpp index 8c2e3e7..588a070 100644 --- a/src/subwindow/subwindow.cpp +++ b/src/subwindow/subwindow.cpp @@ -81,10 +81,9 @@ SubWindow::SubWindow(QWidget *parent, Qt::WindowFlags flags) SubWindow::SubWindow(const Session::SubWindow &data, QWidget *parent, Qt::WindowFlags flags) : SubWindow(parent, flags) { - const auto *profileManager = WebProfileManager::instance(); - Q_CHECK_PTR(profileManager); + WebProfileManager profileManager; - auto *profile = profileManager->profile(data.profile); + auto *profile = profileManager.profile(data.profile); if(profile != nullptr) { setProfile(profile); } @@ -96,15 +95,12 @@ SubWindow::SubWindow(const Session::SubWindow &data, QWidget *parent, Qt::Window Session::SubWindow SubWindow::serialize() const { - const auto *profileManager = WebProfileManager::instance(); - Q_CHECK_PTR(profileManager); - QVector<Session::WebView> tabs(tabCount()); for(int i = 0; i < tabCount(); ++i) { tabs[i] = view(i)->serialize(); } - return { profileManager->id(profile()), tabs }; + return { profile()->getId(), tabs }; } void SubWindow::setProfile(WebProfile *profile) @@ -131,13 +127,20 @@ SubWindow::TabData SubWindow::tabData(int index) const return tabWidget->tabBar()->tabData(index).value<TabData>(); } +WebView *SubWindow::createView(QWebEnginePage::WebWindowType type) +{ + auto *view = new WebView(m_profile, std::bind(&SubWindow::createView, this, std::placeholders::_1), this); + tabWidget->addTab(view); + return view; +} + int SubWindow::addTab(const QUrl &url, WebProfile *profile) { Q_CHECK_PTR(m_profile); auto *_profile = (profile == nullptr) ? m_profile : profile; - auto *view = new WebView(_profile, this); + auto *view = new WebView(_profile, std::bind(&SubWindow::createView, this, std::placeholders::_1), this); if(url.isEmpty()) view->load(_profile->newtab()); else @@ -148,7 +151,7 @@ int SubWindow::addTab(const QUrl &url, WebProfile *profile) int SubWindow::addTab(const Session::WebView &data) { - auto *view = new WebView(data, this); + auto *view = new WebView(data, std::bind(&SubWindow::createView, this, std::placeholders::_1), this); return tabWidget->addTab(view); } diff --git a/src/subwindow/subwindow.h b/src/subwindow/subwindow.h index 02f50d0..88f3985 100644 --- a/src/subwindow/subwindow.h +++ b/src/subwindow/subwindow.h @@ -68,6 +68,8 @@ signals: void aboutToClose(); public slots: + WebView *createView(QWebEnginePage::WebWindowType type); + int addTab(const QUrl &url = QUrl(), WebProfile *profile = nullptr); int addTab(const Session::WebView &data); void closeTab(int index) diff --git a/src/subwindow/tabwidget.cpp b/src/subwindow/tabwidget.cpp index d09fffb..efa2b6a 100644 --- a/src/subwindow/tabwidget.cpp +++ b/src/subwindow/tabwidget.cpp @@ -17,17 +17,18 @@ #include <QWebEngineHistory> #include "subwindow.h" -inline WebView *createViewFromInfo(TabWidget::TabInformation &tab, QWidget *parent) +inline WebView *createViewFromInfo(TabWidget::TabInformation &tab, SubWindow *parent) { - auto *view = new WebView(tab.profile, parent); + auto *view = new WebView(tab.profile, std::bind(&SubWindow::createView, parent, std::placeholders::_1), parent); QDataStream stream(&tab.historyBuffer, QIODevice::ReadOnly); stream >> *view->history(); view->history()->goToItem(view->history()->itemAt(tab.historyIndex)); return view; } -TabWidget::TabWidget(QWidget *parent) +TabWidget::TabWidget(SubWindow *parent) : QTabWidget(parent) + , m_parent(parent) { setStyleSheet("QTabBar::tab { width: 200px; }"); @@ -132,7 +133,7 @@ int TabWidget::restoreLastTab() { if(!m_closedTabs.isEmpty()) { TabInformation tab = m_closedTabs.takeLast(); - return addTab(createViewFromInfo(tab, this)); + return addTab(createViewFromInfo(tab, m_parent)); } return -1; } @@ -147,7 +148,7 @@ void TabWidget::restoreTabMenu(QMenu *menu) connect(openAction, &QAction::triggered, this, [this, i]() { TabInformation tab = m_closedTabs.takeAt(i); - addTab(createViewFromInfo(tab, this)); + addTab(createViewFromInfo(tab, m_parent)); }); } diff --git a/src/subwindow/tabwidget.h b/src/subwindow/tabwidget.h index d67d2de..de5e6fb 100644 --- a/src/subwindow/tabwidget.h +++ b/src/subwindow/tabwidget.h @@ -18,6 +18,7 @@ class QMenu; class WebView; class WebProfile; class QWebEnginePage; +class SubWindow; class TabWidget : public QTabWidget { Q_OBJECT @@ -31,7 +32,7 @@ public: QByteArray historyBuffer; }; - explicit TabWidget(QWidget *parent = nullptr); + explicit TabWidget(SubWindow *parent = nullptr); ~TabWidget() override; public slots: @@ -46,6 +47,7 @@ protected: void mousePressEvent(QMouseEvent *event) override; private: + SubWindow *m_parent; int current = -1; int previous = -1; QMenu *tabContextMenu; diff --git a/src/webengine/meson.build b/src/webengine/meson.build new file mode 100644 index 0000000..db7bdc5 --- /dev/null +++ b/src/webengine/meson.build @@ -0,0 +1,27 @@ +webengine_moc = mod_qt5.preprocess( + moc_headers: [ 'webprofile.h', 'webpage.h', 'webview.h' ], + dependencies: dep_qt5 +) + +dep_webengine = declare_dependency( + include_directories: [ '.', smolbote_include ], + link_with: static_library('webengine', dependencies: dep_qt5, + include_directories: smolbote_include, + sources: [ 'webprofile.cpp', 'urlinterceptor.cpp', 'webprofilemanager.cpp', 'webpage.cpp', 'webview.cpp', 'webviewcontextmenu.cpp', webengine_moc ]) +) + +poi_sourceset.add(dep_webengine) + +test('profile', executable('profile', 'test/profile.cpp', dependencies: [ dep_qt5, dep_webengine, dep_catch ]), + env: { 'PROFILE' : meson.current_source_dir()/'test/testing.profile' }, + suite: 'webengine') + +test('profilemanager', executable('profilemanager', 'test/profilemanager.cpp', dependencies: [ dep_qt5, dep_webengine, dep_catch ]), + env: { 'PROFILES' : meson.current_source_dir()/'test/testing.profile' }, + suite: 'webengine') + +test('view', executable('view', 'test/view.cpp', dependencies: [ dep_qt5, dep_webengine, dep_catch ]), + args: [ '-platform', 'offscreen' ], + env: { 'PROFILE' : meson.current_source_dir()/'test/testing.profile', + 'URL' : meson.current_source_dir()/'test/sample.html' }, + suite: 'webengine') diff --git a/src/webengine/test/form.html b/src/webengine/test/form.html new file mode 100644 index 0000000..9d8b19e --- /dev/null +++ b/src/webengine/test/form.html @@ -0,0 +1,10 @@ +<html> + +<head> + <title>Form completion test</title> +</head> + +<body> +<h2>Form completion test</h2> +</body> +</html> diff --git a/src/webengine/test/icon.svg b/src/webengine/test/icon.svg new file mode 100644 index 0000000..a348cab --- /dev/null +++ b/src/webengine/test/icon.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" version="1.1"> + <circle cx="150" cy="150" r="100" stroke="#000000" stroke-width="6" fill="#e60026"></circle> + <circle cx="150" cy="150" r="87" stroke="#000000" stroke-width="4" fill="#e5e4e2"></circle> + <path d="M230,150 A80,80 0 0 0 150,70 L150,150 Z" /> + <path d="M70,150 A80,80 0 0 0 150,230 L150,150 Z" /> +</svg> + diff --git a/src/webengine/test/profile.cpp b/src/webengine/test/profile.cpp new file mode 100644 index 0000000..7351f66 --- /dev/null +++ b/src/webengine/test/profile.cpp @@ -0,0 +1,124 @@ +#define CATCH_CONFIG_RUNNER + +// clazy:excludeall=non-pod-global-static + +#include "webprofilemanager.h" +#include <QApplication> +#include <catch2/catch.hpp> + +TEST_CASE("loading profile settings") +{ + const QString search = GENERATE(as<QString>{}, "https://search.url/t=%1", "https://duckduckgo.com/?q=%1&ia=web", "aaabbbccc"); + const QUrl homepage = GENERATE(as<QUrl>{}, "https://homepage.net", "about:blank", "aaabbbccc"); + const QUrl newtab = GENERATE(as<QUrl>{}, "https://newtab.net", "about:blank", "aaabbbccc"); + + auto *settings = WebProfile::load(QString(), search, homepage, newtab); + + REQUIRE(settings != nullptr); + REQUIRE(settings->value("search").toString() == search); + REQUIRE(settings->value("homepage").toUrl() == homepage); + REQUIRE(settings->value("newtab").toUrl() == newtab); + + delete settings; +} + +SCENARIO("loading individual profiles") +{ + GIVEN("no profile preset") + { + const QString search = GENERATE(as<QString>{}, "https://search.url/t=%1", "https://duckduckgo.com/?q=%1&ia=web", "aaabbbccc"); + const QUrl homepage = GENERATE(as<QUrl>{}, "https://homepage.net", "about:blank", "aaabbbccc"); + const QUrl newtab = GENERATE(as<QUrl>{}, "https://newtab.net", "about:blank", "aaabbbccc"); + + const QString id{ "id" }; + auto *settings = WebProfile::load(QString(), search, homepage, newtab); + auto *profile = WebProfile::load(id, settings, true); + + REQUIRE(profile != nullptr); + REQUIRE(profile->getId() == id); + REQUIRE(profile->property("id").toString() == id); + REQUIRE(profile->name() == id); + REQUIRE(profile->search() == search); + REQUIRE(profile->homepage() == homepage); + REQUIRE(profile->newtab() == newtab); + + REQUIRE(profile->isOffTheRecord()); + delete settings; + delete profile; + } + + GIVEN("an off-the-record profile preset") + { + REQUIRE(qEnvironmentVariableIsSet("PROFILE")); + + const QString id{ "id" }; + auto *settings = WebProfile::load(qgetenv("PROFILE"), QString(), QUrl(), QUrl()); + auto *profile = WebProfile::load(id, settings, true); + + REQUIRE(profile != nullptr); + REQUIRE(profile->getId() == id); + REQUIRE(profile->isOffTheRecord()); + + WHEN("created") + { + THEN("uses default values") + { + REQUIRE(profile->name() == "Test Profile"); + REQUIRE(profile->search() == "https://duckduckgo.com/?q=%1&ia=web"); + REQUIRE(profile->homepage() == QUrl("about:blank")); + REQUIRE(profile->newtab() == QUrl("about:blank")); + } + } + + WHEN("changing profile name") + { + const QString name = GENERATE(as<QString>{}, "a", "bb", "ccc"); + profile->setName(name); + THEN("the name changes") + { + REQUIRE(profile->name() == name); + REQUIRE(settings->value("name").toString() == name); + } + } + WHEN("changing search url") + { + const QString search = GENERATE(as<QString>{}, "a", "bb", "ccc"); + profile->setSearch(search); + THEN("the search url changes") + { + REQUIRE(profile->search() == search); + REQUIRE(settings->value("search").toString() == search); + } + } + WHEN("changing homepage") + { + const QUrl url = GENERATE(as<QUrl>{}, "a", "bb", "ccc"); + profile->setHomepage(url); + THEN("homepage changes") + { + REQUIRE(profile->homepage() == url); + REQUIRE(settings->value("homepage").toUrl() == url); + } + } + WHEN("changing newtab") + { + const QUrl url = GENERATE(as<QUrl>{}, "a", "bb", "ccc"); + profile->setNewtab(url); + THEN("newtab changes") + { + REQUIRE(profile->newtab() == url); + REQUIRE(settings->value("newtab").toUrl() == url); + } + } + + delete settings; + delete profile; + } +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + const auto r = Catch::Session().run(argc, argv); + return r; +} diff --git a/src/webengine/test/profilemanager.cpp b/src/webengine/test/profilemanager.cpp new file mode 100644 index 0000000..dc7c903 --- /dev/null +++ b/src/webengine/test/profilemanager.cpp @@ -0,0 +1,120 @@ +#define CATCH_CONFIG_RUNNER + +// clazy:excludeall=non-pod-global-static + +#include "webprofilemanager.h" +#include <QApplication> +#include <catch2/catch.hpp> + +SCENARIO("WebProfileManager") +{ + const QString search{ "https://search.url/t=%1" }; + const QUrl homepage{ "https://homepage.net" }; + const QUrl newtab{ "https://newtab.net" }; + const QString default_id{ "default" }; + + GIVEN("an empty profile list") + { + WebProfileManager<false> profiles({}, default_id, search, homepage, newtab); + + REQUIRE(profiles.idList().count() == 1); + REQUIRE(profiles.profile(default_id) == WebProfile::defaultProfile()); + REQUIRE(profiles.profile("not-in-list") == nullptr); + + WHEN("adding a new profile") + { + const QString id{ "id" }; + auto *settings = WebProfile::load(QString(), search, homepage, newtab); + auto *profile = WebProfile::load(id, settings, true); + + THEN("doesn't add profile without settings") + { + profiles.add(id, profile, nullptr); + REQUIRE(profiles.idList().count() == 1); + } + + THEN("doesn't add settings without profile") + { + profiles.add(id, nullptr, settings); + REQUIRE(profiles.idList().count() == 1); + } + + THEN("adds new profile with settings") + { + profiles.add(id, profile, settings); + REQUIRE(profiles.idList().count() == 2); + } + } + + WHEN("moving") + { + WebProfileManager<false> other(std::move(profiles)); + THEN("moved has the same number of profiles") + { + REQUIRE(other.idList().count() == 1); + REQUIRE(other.profile(default_id) == WebProfile::defaultProfile()); + REQUIRE(other.profile("not-in-list") == nullptr); + } + } + } + + GIVEN("a number of profiles, default undefined") + { + REQUIRE(qEnvironmentVariableIsSet("PROFILES")); + + WebProfileManager<false> profiles(QString::fromLatin1(qgetenv("PROFILES")).split(';'), default_id, search, homepage, newtab); + + REQUIRE(profiles.idList().count() == 2); + REQUIRE(profiles.profile(default_id) == WebProfile::defaultProfile()); + REQUIRE(profiles.profile("testing") != nullptr); + REQUIRE(profiles.profile("not-in-list") == nullptr); + + WHEN("making global") + { + profiles.make_global(); + WebProfileManager other; + + THEN("global has the same number of profiles") + { + REQUIRE(other.idList().count() == 2); + REQUIRE(other.profile(default_id) == WebProfile::defaultProfile()); + REQUIRE(other.profile("testing") != nullptr); + REQUIRE(other.profile("not-in-list") == nullptr); + } + + THEN("walking has no nullptrs") + { + other.walk([](const QString &, WebProfile *profile, const QSettings *settings) { + REQUIRE(profile != nullptr); + REQUIRE(settings != nullptr); + }); + } + } + } + + GIVEN("a number of profiles, default defined") + { + REQUIRE(qEnvironmentVariableIsSet("PROFILES")); + + WebProfileManager<false> profiles(QString::fromLatin1(qgetenv("PROFILES")).split(';'), "testing", search, homepage, newtab); + + REQUIRE(profiles.idList().count() == 1); + REQUIRE(profiles.profile("testing") == WebProfile::defaultProfile()); + REQUIRE(profiles.profile("not-in-list") == nullptr); + + WHEN("walking") + { + profiles.walk([](const QString &, WebProfile *profile, const QSettings *settings) { + REQUIRE(profile != nullptr); + REQUIRE(settings != nullptr); + }); + } + } +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + const auto r = Catch::Session().run(argc, argv); + return r; +} diff --git a/src/webengine/test/sample.html b/src/webengine/test/sample.html new file mode 100644 index 0000000..54746d5 --- /dev/null +++ b/src/webengine/test/sample.html @@ -0,0 +1,7 @@ +<html> +<head><title>sample page</title></head> +<body> + <h2><img src="icon.svg" />This is a sample page</h2> + <iframe width="100%" height="80%" src="form.html"></iframe> +</body> +</html> diff --git a/src/webengine/test/testing.profile b/src/webengine/test/testing.profile new file mode 100644 index 0000000..e345a3e --- /dev/null +++ b/src/webengine/test/testing.profile @@ -0,0 +1,8 @@ +name=Test Profile +otr=true +search=https://duckduckgo.com/?q=%1&ia=web +homepage=about:blank +newtab=about:blank + +[headers] +Dnt=1 diff --git a/src/webengine/test/view.cpp b/src/webengine/test/view.cpp new file mode 100644 index 0000000..8aa639a --- /dev/null +++ b/src/webengine/test/view.cpp @@ -0,0 +1,92 @@ +#define CATCH_CONFIG_RUNNER + +// clazy:excludeall=non-pod-global-static + +#include "webprofile.h" +#include "webview.h" +#include <QApplication> +#include <QMainWindow> +#include <QTimer> +#include <QtWebEngine> +#include <catch2/catch.hpp> + +SCENARIO("WebView") +{ + const QString profile_id{ "default" }; + auto *settings = WebProfile::load(qgetenv("PROFILE"), "about:blank", QUrl{ "about:blank" }, QUrl{ "about:blank" }); + auto *profile = WebProfile::load(profile_id, settings, true); + + QMainWindow window; + auto *view = new WebView(profile, nullptr); + window.setCentralWidget(view); + window.show(); + window.resize(800, 600); + + WHEN("created") + { + THEN("using the default profile") + { + REQUIRE(view->profile() == profile); + } + THEN("serialized using default profile") + { + const auto data = view->serialize(); + REQUIRE(data.profile == profile_id); + REQUIRE(data.url.isEmpty()); + REQUIRE(!data.history.isEmpty()); + } + THEN("loading a url") + { + // block until a loadFinished signal + QEventLoop pause; + QObject::connect(view, &WebView::loadFinished, &pause, &QEventLoop::quit); + view->load(QUrl{ qgetenv("URL") }); + pause.exec(); + + REQUIRE(view->isLoaded()); + } + } + + WHEN("changing profiles") + { + const QString swap_profile_id{ "swap_profile" }; + auto *swap_settings = WebProfile::load(QString(), "about:blank", QUrl{ "about:blank" }, QUrl{ "about:blank" }); + auto *swap_profile = WebProfile::load(swap_profile_id, swap_settings, true); + + view->setProfile(swap_profile); + THEN("using the swap profile") + { + REQUIRE(view->profile() == swap_profile); + } + THEN("serialized using swap profile") + { + const auto data = view->serialize(); + REQUIRE(data.profile == swap_profile_id); + REQUIRE(data.url.isEmpty()); + REQUIRE(!data.history.isEmpty()); + } + + view->setProfile(profile); + delete swap_settings; + delete swap_profile; + } + + // cleanup + window.close(); + delete view; + delete settings; + delete profile; +} + +int main(int argc, char **argv) +{ + QtWebEngine::initialize(); + QApplication app(argc, argv); + + QTimer::singleShot(0, &app, [argc, argv, &app]() { + const auto n_failed = Catch::Session().run(argc, argv); + app.exit(n_failed); + }); + + return app.exec(); +} diff --git a/src/webengine/urlinterceptor.cpp b/src/webengine/urlinterceptor.cpp index 29cd869..047cad4 100644 --- a/src/webengine/urlinterceptor.cpp +++ b/src/webengine/urlinterceptor.cpp @@ -8,59 +8,24 @@ #include "urlinterceptor.h" #include "webprofile.h" -#include "urlfilter.h" // test DNT on https://browserleaks.com/donottrack -UrlRequestInterceptor::UrlRequestInterceptor(WebProfile* profile, QObject* parent) - : QWebEngineUrlRequestInterceptor(parent) +UrlRequestInterceptor::UrlRequestInterceptor(WebProfile* profile) + : QWebEngineUrlRequestInterceptor(profile) { Q_CHECK_PTR(profile); m_profile = profile; } -void UrlRequestInterceptor::addHttpHeader(const QByteArray &key, const QByteArray &value) -{ - headers.append(qMakePair(key, value)); -} - -void UrlRequestInterceptor::addFilter(UrlFilter *filter) -{ - if(filter != nullptr) - filters.append(filter); -} -void UrlRequestInterceptor::removeFilter(UrlFilter *filter) -{ - if(filter != nullptr) - filters.removeOne(filter); -} - void UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { - for(const auto *filter : qAsConst(filters)) { - const auto match = filter->match(info.firstPartyUrl(), info.requestUrl(), info.resourceType()); - - // skip if no match - if(match.first == UrlFilter::NotMatched) - continue; - - else { - if(match.first == UrlFilter::Allow) - info.block(false); - else if(match.first == UrlFilter::Block) - info.block(true); - else if(match.first == UrlFilter::Redirect) - info.redirect(QUrl::fromUserInput(match.second)); - // we found a match, skip the rest - break; - } + for(auto *filter : qAsConst(m_profile->m_filters)) { + filter->interceptRequest(info); } // set headers - for(const auto &header : qAsConst(headers)) { - info.setHttpHeader(header.first, header.second); - } - for(auto i = m_profile->headers().constBegin(); i != m_profile->headers().constEnd(); ++i) { + for(auto i = m_profile->m_headers.constBegin(); i != m_profile->m_headers.constEnd(); ++i) { info.setHttpHeader(i.key(), i.value()); } } diff --git a/src/webengine/urlinterceptor.h b/src/webengine/urlinterceptor.h index 4909586..eb3ce67 100644 --- a/src/webengine/urlinterceptor.h +++ b/src/webengine/urlinterceptor.h @@ -9,30 +9,23 @@ #ifndef SMOLBOTE_URLREQUESTINTERCEPTOR_H #define SMOLBOTE_URLREQUESTINTERCEPTOR_H -#include <QVector> #include <QWebEngineUrlRequestInterceptor> -#include <QByteArray> -class UrlFilter; class WebProfile; class UrlRequestInterceptor : public QWebEngineUrlRequestInterceptor { - Q_OBJECT + friend class WebProfile; + public: - explicit UrlRequestInterceptor(WebProfile *profile, QObject *parent = nullptr); ~UrlRequestInterceptor() override = default; - void addHttpHeader(const QByteArray &key, const QByteArray &value); - - void addFilter(UrlFilter *filter); - void removeFilter(UrlFilter *filter); - void interceptRequest(QWebEngineUrlRequestInfo &info) override; +protected: + explicit UrlRequestInterceptor(WebProfile *profile); + private: WebProfile *m_profile; - QVector<QPair<QByteArray, QByteArray>> headers; - QVector<UrlFilter*> filters; }; #endif // SMOLBOTE_URLREQUESTINTERCEPTOR_H diff --git a/src/webengine/webpage.cpp b/src/webengine/webpage.cpp index e79db9d..8c6b8db 100644 --- a/src/webengine/webpage.cpp +++ b/src/webengine/webpage.cpp @@ -12,7 +12,6 @@ #include <QTimer> #include <QWebEngineFullScreenRequest> #include <QWebEngineCertificateError> -#include <spdlog/spdlog.h> QString tr_terminationStatus(QWebEnginePage::RenderProcessTerminationStatus status) { @@ -106,17 +105,16 @@ void WebPage::featurePermissionDialog(const QUrl &securityOrigin, QWebEnginePage messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); messageBox.setDefaultButton(QMessageBox::No); - if(messageBox.exec() == QMessageBox::Yes) + if(messageBox.exec() == QMessageBox::Yes) { setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionGrantedByUser); - else + } else { setFeaturePermission(securityOrigin, feature, QWebEnginePage::PermissionDeniedByUser); + } } void WebPage::renderProcessCrashed(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode) { if(terminationStatus != QWebEnginePage::NormalTerminationStatus) { - spdlog::warn("render process terminated: [{}] {}", terminationStatus, exitCode); - QString page = "<html><body><h1>This tab has crashed!</h1>%message%</body></html>"; page.replace(QLatin1String("%message%"), QString("<p>%1<br>Exit code is %2.</p>" "<p>Press <a href='%3'>here</a> to reload this tab.</p>") diff --git a/src/webengine/webpage.h b/src/webengine/webpage.h index 48011cb..91ae4f3 100644 --- a/src/webengine/webpage.h +++ b/src/webengine/webpage.h @@ -14,13 +14,15 @@ class WebPage : public QWebEnginePage { Q_OBJECT + public: - explicit WebPage(QWebEngineProfile *profile, QObject *parent = nullptr); + WebPage(QWebEngineProfile *profile, QObject *parent = nullptr); + ~WebPage() override = default; protected: bool certificateError(const QWebEngineCertificateError &certificateError) override; -private slots: +protected slots: void featurePermissionDialog(const QUrl &securityOrigin, QWebEnginePage::Feature feature); void renderProcessCrashed(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode); }; diff --git a/src/webengine/webprofile.cpp b/src/webengine/webprofile.cpp index 5224189..f0368c9 100644 --- a/src/webengine/webprofile.cpp +++ b/src/webengine/webprofile.cpp @@ -11,6 +11,7 @@ #include <QSettings> #include <QWebEngineCookieStore> #include <QWebEngineSettings> +#include "urlinterceptor.h" static WebProfile *s_profile = nullptr; @@ -25,11 +26,89 @@ WebProfile *WebProfile::defaultProfile() return s_profile; } -WebProfile::WebProfile(const QString &name, QObject *parent) - : QWebEngineProfile(parent) +QSettings *WebProfile::load(const QString &path, const QString &search, const QUrl &homepage, const QUrl &newtab) +{ + auto *settings = new QSettings(path, QSettings::IniFormat); + + if(!settings->contains("search")) { + settings->setValue("search", search); + } + if(!settings->contains("homepage")) { + settings->setValue("homepage", homepage); + } + if(!settings->contains("newtab")) { + settings->setValue("newtab", newtab); + } + + return settings; +} + +WebProfile *WebProfile::load(const QString &id, QSettings *settings, bool isOffTheRecord) { - m_name = name; + WebProfile *profile = nullptr; + if(isOffTheRecord || settings->value("otr", isOffTheRecord).toBool()) { + profile = new WebProfile(id, nullptr); + } else { + profile = new WebProfile(id, id, nullptr); + } + + profile->m_name = settings->value("name", id).toString(); + connect(profile, &WebProfile::nameChanged, profile, [settings](const QString &name) { settings->setValue("name", name); }); + profile->m_search = settings->value("search", "").toString(); + connect(profile, &WebProfile::searchChanged, settings, [settings](const QString &url) { settings->setValue("search", url); }); + profile->m_homepage = settings->value("homepage", "").toUrl(); + connect(profile, &WebProfile::homepageChanged, settings, [settings](const QUrl &url) { settings->setValue("homepage", url); }); + profile->m_newtab = settings->value("newtab", "").toUrl(); + connect(profile, &WebProfile::newtabChanged, settings, [settings](const QUrl &url) { settings->setValue("newtab", url); }); + + { + settings->beginGroup("properties"); + const auto keys = settings->childKeys(); + for(const QString &key : keys) { + profile->setProperty(qUtf8Printable(key), settings->value(key)); + } + settings->endGroup(); // properties + connect(profile, &WebProfile::propertyChanged, [settings](const QString &property, const QVariant &value) { + settings->setValue("properties/" + property, value); + }); + } + { + settings->beginGroup("attributes"); + const auto keys = settings->childKeys(); + auto *s = profile->settings(); + for(const QString &key : keys) { + auto attribute = static_cast<QWebEngineSettings::WebAttribute>(key.toInt()); + s->setAttribute(attribute, settings->value(key).toBool()); + } + settings->endGroup(); + connect(profile, &WebProfile::attributeChanged, [settings](const QWebEngineSettings::WebAttribute attr, const bool value) { + settings->setValue("attributes/" + QString::number(attr), value); + }); + } + { + // headers + settings->beginGroup("headers"); + for(const QString &key : settings->childKeys()) { + profile->setHttpHeader(key.toLatin1(), settings->value(key).toString().toLatin1()); + } + settings->endGroup(); + connect(profile, &WebProfile::headerChanged, [settings](const QString &name, const QString &value) { + settings->setValue("headers/" + name, value); + }); + connect(profile, &WebProfile::headerRemoved, [settings](const QString &name) { + settings->remove("headers/" + name); + }); + } + return profile; +} + +// off-the-record constructor +WebProfile::WebProfile(const QString &id, QObject *parent) + : QWebEngineProfile(parent) + , m_id(id) +{ + QWebEngineProfile::setUrlRequestInterceptor(new UrlRequestInterceptor(this)); connect(this->cookieStore(), &QWebEngineCookieStore::cookieAdded, this, [this](const QNetworkCookie &cookie) { m_cookies.append(cookie); }); @@ -38,11 +117,12 @@ WebProfile::WebProfile(const QString &name, QObject *parent) }); } -WebProfile::WebProfile(const QString &storageName, const QString &name, QObject *parent) +// default constructor +WebProfile::WebProfile(const QString &id, const QString &storageName, QObject *parent) : QWebEngineProfile(storageName, parent) + , m_id(id) { - m_name = name; - + QWebEngineProfile::setUrlRequestInterceptor(new UrlRequestInterceptor(this)); connect(this->cookieStore(), &QWebEngineCookieStore::cookieAdded, this, [this](const QNetworkCookie &cookie) { m_cookies.append(cookie); }); @@ -50,109 +130,3 @@ WebProfile::WebProfile(const QString &storageName, const QString &name, QObject m_cookies.removeAll(cookie); }); } - -const QString WebProfile::name() const -{ - return m_name; -} - -void WebProfile::setName(const QString &name) -{ - m_name = name; - emit nameChanged(name); -} - -QString WebProfile::search() const -{ - return m_search; -} - -void WebProfile::setSearch(const QString &url) -{ - m_search = url; - emit searchChanged(m_search); -} - -QUrl WebProfile::homepage() const -{ - return m_homepage; -} - -void WebProfile::setHomepage(const QUrl &url) -{ - m_homepage = url; - emit homepageChanged(m_homepage); -} - -QUrl WebProfile::newtab() const -{ - return m_newtab; -} - -void WebProfile::setNewtab(const QUrl &url) -{ - m_newtab = url; - emit newtabChanged(m_newtab); -} - -void WebProfile::setCachePath(const QString &path) -{ - QWebEngineProfile::setCachePath(path); - emit propertyChanged("cachePath", path); -} - -void WebProfile::setPersistentStoragePath(const QString &path) -{ - QWebEngineProfile::setPersistentStoragePath(path); - emit propertyChanged("persistentStoragePath", path); -} - -void WebProfile::setPersistentCookiesPolicy(int policy) -{ - QWebEngineProfile::setPersistentCookiesPolicy(static_cast<QWebEngineProfile::PersistentCookiesPolicy>(policy)); - emit propertyChanged("persistentCookiesPolicy", policy); -} - -void WebProfile::setHttpAcceptLanguage(const QString &httpAcceptLanguage) -{ - QWebEngineProfile::setHttpAcceptLanguage(httpAcceptLanguage); - emit propertyChanged("httpAcceptLanguage", httpAcceptLanguage); -} - -void WebProfile::setHttpCacheMaximumSize(int maxSize) -{ - QWebEngineProfile::setHttpCacheMaximumSize(maxSize); - emit propertyChanged("httpCacheMaximumSize", maxSize); -} - -void WebProfile::setHttpCacheType(int type) -{ - QWebEngineProfile::setHttpCacheType(static_cast<QWebEngineProfile::HttpCacheType>(type)); - emit propertyChanged("httpCacheType", type); -} - -void WebProfile::setHttpUserAgent(const QString &userAgent) -{ - QWebEngineProfile::setHttpUserAgent(userAgent); - emit propertyChanged("httpUserAgent", userAgent); -} - -void WebProfile::setHttpHeader(const QString &name, const QString &value) -{ - m_headers[name.toLatin1()] = value.toLatin1(); - emit headerChanged(name, value); -} - -void WebProfile::removeHttpHeader(const QString &name) -{ - if(m_headers.contains(name.toLatin1())) { - m_headers.remove(name.toLatin1()); - emit headerRemoved(name); - } -} - -void WebProfile::setSpellCheckEnabled(bool enable) -{ - QWebEngineProfile::setSpellCheckEnabled(enable); - emit propertyChanged("spellCheckEnabed", enable); -} diff --git a/src/webengine/webprofile.h b/src/webengine/webprofile.h index 66154af..180cc9d 100644 --- a/src/webengine/webprofile.h +++ b/src/webengine/webprofile.h @@ -9,7 +9,6 @@ #ifndef SMOLBOTE_WEBENGINEPROFILE_H #define SMOLBOTE_WEBENGINEPROFILE_H -#include <QHash> #include <QMap> #include <QNetworkCookie> #include <QString> @@ -17,14 +16,17 @@ #include <QVector> #include <QWebEngineProfile> #include <QWebEngineSettings> +#include <QVariant> +#include <QSettings> -class WebProfileManager; +class UrlRequestInterceptor; class WebProfile : public QWebEngineProfile { - friend class WebProfileManager; + friend class UrlRequestInterceptor; Q_OBJECT + Q_PROPERTY(QString id READ getId CONSTANT) Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(QString search READ search WRITE setSearch NOTIFY searchChanged) Q_PROPERTY(QUrl homepage READ homepage WRITE setHomepage NOTIFY homepageChanged) @@ -42,72 +44,153 @@ class WebProfile : public QWebEngineProfile Q_PROPERTY(bool spellCheckEnabled READ isSpellCheckEnabled WRITE setSpellCheckEnabled NOTIFY propertyChanged) +signals: + void nameChanged(const QString &name); + void searchChanged(const QString &url); + void homepageChanged(const QUrl &url); + void newtabChanged(const QUrl &url); + + void propertyChanged(const QString &name, const QVariant &value); + void attributeChanged(QWebEngineSettings::WebAttribute attribute, bool value); + void headerChanged(const QString &name, const QString &value); + void headerRemoved(const QString &name); + public: + [[nodiscard]] static QSettings *load(const QString &path, const QString &search = QString(), const QUrl &homepage = QUrl(), const QUrl &newtab = QUrl()); + [[nodiscard]] static WebProfile *load(const QString &id, QSettings *settings, bool isOffTheRecord = true); + + WebProfile(const WebProfile &) = delete; + WebProfile& operator=(const WebProfile &) = delete; + WebProfile(WebProfile &&) = delete; + WebProfile& operator=(WebProfile &&) = delete; + static WebProfile *defaultProfile(); static void setDefaultProfile(WebProfile *profile); - ~WebProfile() = default; + ~WebProfile() override = default; - const QString name() const; - void setName(const QString &name); - - const QVector<QNetworkCookie> cookies() const + [[nodiscard]] QString getId() const { - return qAsConst(m_cookies); + return m_id; + } + [[nodiscard]] QString name() const + { + return m_name; } - const QMap<QByteArray, QByteArray> headers() const + void setName(const QString &name) { - return qAsConst(m_headers); + m_name = name; + emit nameChanged(name); } - // search url - QString search() const; - void setSearch(const QString &url); + [[nodiscard]] QVector<QNetworkCookie> cookies() const + { + return qAsConst(m_cookies); + } - // homepage url - QUrl homepage() const; - void setHomepage(const QUrl &url); + [[nodiscard]] QString search() const + { + return m_search; + } + void setSearch(const QString &url) + { + m_search = url; + emit searchChanged(m_search); + } - // new tab url - QUrl newtab() const; - void setNewtab(const QUrl &url); + [[nodiscard]] QUrl homepage() const + { + return m_homepage; + } + void setHomepage(const QUrl &url) + { + m_homepage = url; + emit homepageChanged(m_homepage); + } - void setCachePath(const QString &path); - void setPersistentStoragePath(const QString &path); - void setPersistentCookiesPolicy(int policy); + [[nodiscard]] QUrl newtab() const + { + return m_newtab; + } + void setNewtab(const QUrl &url) + { + m_newtab = url; + emit newtabChanged(m_newtab); + } - void setHttpAcceptLanguage(const QString &httpAcceptLanguage); - void setHttpCacheMaximumSize(int maxSize); - void setHttpCacheType(int type); - void setHttpUserAgent(const QString &userAgent); - void setHttpHeader(const QString &name, const QString &value); - void removeHttpHeader(const QString &name); + void setCachePath(const QString &path) + { + QWebEngineProfile::setCachePath(path); + emit propertyChanged("cachePath", path); + } + void setPersistentStoragePath(const QString &path) + { + QWebEngineProfile::setPersistentStoragePath(path); + emit propertyChanged("persistentStoragePath", path); + } + void setPersistentCookiesPolicy(int policy) + { + QWebEngineProfile::setPersistentCookiesPolicy(static_cast<QWebEngineProfile::PersistentCookiesPolicy>(policy)); + emit propertyChanged("persistentCookiesPolicy", policy); + } - void setSpellCheckEnabled(bool enable); + void setHttpAcceptLanguage(const QString &httpAcceptLanguage) + { + QWebEngineProfile::setHttpAcceptLanguage(httpAcceptLanguage); + emit propertyChanged("httpAcceptLanguage", httpAcceptLanguage); + } + void setHttpCacheMaximumSize(int maxSize) + { + QWebEngineProfile::setHttpCacheMaximumSize(maxSize); + emit propertyChanged("httpCacheMaximumSize", maxSize); + } + void setHttpCacheType(int type) + { + QWebEngineProfile::setHttpCacheType(static_cast<QWebEngineProfile::HttpCacheType>(type)); + emit propertyChanged("httpCacheType", type); + } + void setHttpUserAgent(const QString &userAgent) + { + QWebEngineProfile::setHttpUserAgent(userAgent); + emit propertyChanged("httpUserAgent", userAgent); + } + void setHttpHeader(const QString &name, const QString &value) + { + m_headers[name.toLatin1()] = value.toLatin1(); + emit headerChanged(name, value); + } + void removeHttpHeader(const QString &name) + { + if(m_headers.contains(name.toLatin1())) { + m_headers.remove(name.toLatin1()); + emit headerRemoved(name); + } + } -signals: - void nameChanged(const QString &name); - void searchChanged(const QString &url); - void homepageChanged(const QUrl &url); - void newtabChanged(const QUrl &url); + void setSpellCheckEnabled(bool enable) + { + QWebEngineProfile::setSpellCheckEnabled(enable); + emit propertyChanged("spellCheckEnabed", enable); + } - void propertyChanged(const QString &name, const QVariant &value); - void attributeChanged(const QWebEngineSettings::WebAttribute attribute, const bool value); - void headerChanged(const QString &name, const QString &value); - void headerRemoved(const QString &name); + void setUrlRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor) + { + m_filters.append(interceptor); + } protected: // off-the-record constructor - explicit WebProfile(const QString &name, QObject *parent = nullptr); + explicit WebProfile(const QString &id, QObject *parent = nullptr); // default constructor - explicit WebProfile(const QString &storageName, const QString &name, QObject *parent = nullptr); + explicit WebProfile(const QString &id, const QString &storageName, QObject *parent = nullptr); -private: + const QString m_id; QString m_name; QString m_search = QString("about:blank"); QUrl m_homepage = QUrl("about:blank"); QUrl m_newtab = QUrl("about:blank"); + QVector<QWebEngineUrlRequestInterceptor*> m_filters; QVector<QNetworkCookie> m_cookies; QMap<QByteArray, QByteArray> m_headers; }; diff --git a/src/webengine/webprofilemanager.cpp b/src/webengine/webprofilemanager.cpp index bdd10f0..785251b 100644 --- a/src/webengine/webprofilemanager.cpp +++ b/src/webengine/webprofilemanager.cpp @@ -7,28 +7,23 @@ */ #include "webprofilemanager.h" -#include "configuration.h" #include "webprofile.h" -#include <QFileInfo> -#include <QWebEngineSettings> -static WebProfileManager *s_instance = nullptr; +static WebProfileManager<false> *s_instance = nullptr; -auto WebProfileManager::instance() -> const WebProfileManager * +template <> +void WebProfileManager<false>::make_global() { - return s_instance; -} -void WebProfileManager::setInstance(WebProfileManager *ptr) -{ - s_instance = ptr; + if(s_instance == nullptr) { + s_instance = this; + } } -WebProfileManager::WebProfileManager(QObject *parent) - : QObject(parent) -{ -} +template <> +WebProfileManager<true>::~WebProfileManager() = default; -WebProfileManager::~WebProfileManager() +template <> +WebProfileManager<false>::~WebProfileManager() { for(Profile p : qAsConst(profiles)) { if(p.selfDestruct && p.settings != nullptr) { @@ -48,111 +43,30 @@ WebProfileManager::~WebProfileManager() } } -WebProfile *WebProfileManager::profile(const QString &id) const +template <> +WebProfile *WebProfileManager<true>::profile(const QString &id) const { - // Check if profile exists - if(profiles.contains(id)) { - return profiles.value(id).ptr; - } - - return nullptr; + return s_instance->profile(id); } -WebProfile *WebProfileManager::add(const QString &id, const QString &path, bool isOffTheRecord) +template <> +QStringList WebProfileManager<true>::idList() const { - if(profiles.contains(id)) { - return nullptr; - } - - Configuration conf; - Profile profile; - - if(!path.isEmpty()) - profile.settings = new QSettings(path, QSettings::IniFormat); - else - profile.settings = new QSettings; + return s_instance->idList(); +} - // QWebEngineCore cleans up profiles automatically, so no need to set parent - if(profile.settings->value("otr", isOffTheRecord).toBool()) { - // name parent - profile.ptr = new WebProfile(profile.settings->value("name", id).toString(), nullptr); - } else { - // storageName name parent - profile.ptr = new WebProfile(id, profile.settings->value("name", id).toString(), nullptr); +template <> +void WebProfileManager<false>::walk(std::function<void(const QString &id, WebProfile *profile, const QSettings *settings)> f) const +{ + for(auto iter = profiles.begin(); iter != profiles.end(); ++iter) { + f(iter.key(), iter.value().ptr, iter.value().settings); } - - profile.settings->setParent(profile.ptr); - - connect(profile.ptr, &WebProfile::nameChanged, profile.settings, [profile](const QString &name) { - profile.settings->setValue("name", name); - }); - - profile.ptr->setSearch(profile.settings->value("search", conf.value<QString>("profile.search").value()).toString()); - connect(profile.ptr, &WebProfile::searchChanged, profile.settings, [profile](const QString &url) { - profile.settings->setValue("search", url); - }); - - profile.ptr->setHomepage(profile.settings->value("homepage", conf.value<QString>("profile.homepage").value()).toUrl()); - connect(profile.ptr, &WebProfile::homepageChanged, profile.settings, [profile](const QUrl &url) { - profile.settings->setValue("homepage", url); - }); - - profile.ptr->setNewtab(profile.settings->value("newtab", conf.value<QString>("profile.newtab").value()).toUrl()); - connect(profile.ptr, &WebProfile::newtabChanged, profile.settings, [profile](const QUrl &url) { - profile.settings->setValue("newtab", url); - }); - - if(profile.settings != nullptr) { - profile.settings->beginGroup("properties"); - { - const auto keys = profile.settings->childKeys(); - for(const QString &key : keys) { - profile.ptr->setProperty(qUtf8Printable(key), profile.settings->value(key)); - } - } - profile.settings->endGroup(); // properties - connect(profile.ptr, &WebProfile::propertyChanged, [profile](const QString &property, const QVariant &value) { - profile.settings->setValue("properties/" + property, value); - }); - - profile.settings->beginGroup("attributes"); - { - const auto keys = profile.settings->childKeys(); - auto *settings = profile.ptr->settings(); - for(const QString &key : keys) { - auto attribute = static_cast<QWebEngineSettings::WebAttribute>(key.toInt()); - settings->setAttribute(attribute, profile.settings->value(key).toBool()); - } - } - profile.settings->endGroup(); - connect(profile.ptr, &WebProfile::attributeChanged, [profile](const QWebEngineSettings::WebAttribute attr, const bool value) { - profile.settings->setValue("attributes/" + QString::number(attr), value); - }); - - // headers - profile.settings->beginGroup("headers"); - for(const QString &key : profile.settings->childKeys()) { - profile.ptr->setHttpHeader(key.toLatin1(), profile.settings->value(key).toString().toLatin1()); - } - profile.settings->endGroup(); - connect(profile.ptr, &WebProfile::headerChanged, [profile](const QString &name, const QString &value) { - profile.settings->setValue("headers/" + name, value); - }); - connect(profile.ptr, &WebProfile::headerRemoved, [profile](const QString &name) { - profile.settings->remove("headers/" + name); - }); - - } // profile.settings != nullptr - - profiles[id] = profile; - return profile.ptr; } -void WebProfileManager::deleteProfile(const QString &id) +template <> +void WebProfileManager<true>::walk(std::function<void(const QString &id, WebProfile *profile, const QSettings *settings)> f) const { - if(profiles.contains(id)) { - profiles[id].selfDestruct = true; - } + s_instance->walk(f); } void profileMenu(QMenu *menu, const std::function<void(WebProfile *)> &callback, WebProfile *current, bool checkable) @@ -160,13 +74,10 @@ void profileMenu(QMenu *menu, const std::function<void(WebProfile *)> &callback, auto *group = new QActionGroup(menu); QObject::connect(menu, &QMenu::aboutToHide, group, &QActionGroup::deleteLater); - for(const auto &profile : qAsConst(s_instance->profiles)) { - auto *action = menu->addAction(profile.ptr->name(), profile.ptr, [profile, callback]() { - callback(profile.ptr); - }); + s_instance->walk([=](const QString &, WebProfile *profile, const QSettings *) { + auto *action = menu->addAction(profile->name(), profile, [=]() { callback(profile); }); action->setCheckable(checkable); - if(profile.ptr == current) - action->setChecked(true); + action->setChecked(profile == current); group->addAction(action); - } + }); } diff --git a/src/webengine/webprofilemanager.h b/src/webengine/webprofilemanager.h index 0e18d5f..22fb31c 100644 --- a/src/webengine/webprofilemanager.h +++ b/src/webengine/webprofilemanager.h @@ -9,63 +9,97 @@ #ifndef SMOLBOTE_WEBPROFILEMANAGER_H #define SMOLBOTE_WEBPROFILEMANAGER_H +#include "singleton.hpp" #include "webprofile.h" #include <QDir> #include <QFile> +#include <QFileInfo> #include <QMap> #include <QMenu> -#include <QObject> -#include <QSettings> #include <functional> void profileMenu(QMenu *menu, const std::function<void(WebProfile *)> &callback, WebProfile *current = nullptr, bool checkable = false); -class Browser; -class WebProfileManager : public QObject +template <bool use_global = true> +class consumable(unconsumed) WebProfileManager { - Q_OBJECT - - friend class Browser; - friend void profileMenu(QMenu *, const std::function<void(WebProfile *)> &, WebProfile *current, bool); - public: - explicit WebProfileManager(QObject *parent); - ~WebProfileManager() override; + return_typestate(unconsumed) WebProfileManager() + { + static_assert(use_global); + } + return_typestate(unconsumed) WebProfileManager(const QStringList &paths, const QString &default_id, + const QString &search = QString(), const QUrl &homepage = QUrl(), const QUrl &newtab = QUrl()) + { + static_assert(!use_global); + for(const auto &path : paths) { + const auto id = QFileInfo(path).baseName(); + Profile profile; + profile.settings = WebProfile::load(path, search, homepage, newtab); + profile.ptr = WebProfile::load(id, profile.settings, true); + profiles[id] = profile; + } - static auto instance() -> const WebProfileManager *; - static void setInstance(WebProfileManager *ptr); + if(!profiles.contains(default_id)) { + Profile profile; + profile.settings = WebProfile::load(QString(), search, homepage, newtab); + profile.ptr = WebProfile::load(default_id, profile.settings, true); + profiles[default_id] = profile; + } + WebProfile::setDefaultProfile(profiles[default_id].ptr); + } + ~WebProfileManager(); - /** Create a profile with specified id - * param id The profile ID - * return WebProfile* The profile, or nullptr if one could not be created - */ - WebProfile *profile(const QString &id) const; + WebProfileManager(const WebProfileManager &) = delete; + WebProfileManager &operator=(const WebProfileManager &) = delete; - /** Set a profile for deletion - * param id The profile ID - * return void - */ - [[deprecated]] void deleteProfile(const QString &id); + return_typestate(unconsumed) WebProfileManager(WebProfileManager<false> && other param_typestate(unconsumed)) + { + static_assert(!use_global); + profiles = std::move(other.profiles); + other.consume(); + } + WebProfileManager &operator=(WebProfileManager &&) = delete; - const QStringList idList() const + callable_when(unconsumed) [[nodiscard]] WebProfile *profile(const QString &id) const { - return profiles.keys(); + return profiles.value(id).ptr; + } + + callable_when(unconsumed) void add(const QString &id, WebProfile *profile, QSettings *settings) + { + if constexpr(use_global) { + return; + } + + if(profile != nullptr && settings != nullptr) { + profiles[id] = Profile{ profile, settings, false }; + } } - QString id(WebProfile *profile) const + + callable_when(unconsumed) void deleteProfile(const QString &id) { - QMapIterator<QString, Profile> i(profiles); - while(i.hasNext()) { - i.next(); - if(i.value().ptr == profile) - return i.key(); + if constexpr(use_global) { + return; + } + + if(profiles.contains(id)) { + profiles[id].selfDestruct = true; } - return QString(); } -protected: - WebProfile *add(const QString &id, const QString &path = QString(), bool isOffTheRecord = true); + callable_when(unconsumed) [[nodiscard]] QStringList idList() const + { + return profiles.keys(); + } + + callable_when(unconsumed) void walk(std::function<void(const QString &id, WebProfile *profile, const QSettings *settings)>) const; + + callable_when(unconsumed) void make_global(); private: + set_typestate(consumed) void consume() {} + struct Profile { WebProfile *ptr = nullptr; QSettings *settings = nullptr; diff --git a/src/webengine/webview.cpp b/src/webengine/webview.cpp index c64333e..135f25c 100644 --- a/src/webengine/webview.cpp +++ b/src/webengine/webview.cpp @@ -7,20 +7,16 @@ */ #include "webview.h" -#include "subwindow/subwindow.h" #include "webpage.h" #include "webprofile.h" #include "webprofilemanager.h" #include "webviewcontextmenu.h" #include <QContextMenuEvent> -#include <QJsonObject> #include <QWebEngineHistoryItem> WebView::WebView(QWidget *parent) : QWebEngineView(parent) { - m_parentWindow = qobject_cast<SubWindow *>(parent); - // load status and progress connect(this, &QWebEngineView::loadStarted, this, [this]() { m_loaded = false; @@ -37,21 +33,22 @@ WebView::WebView(QWidget *parent) }); } -WebView::WebView(WebProfile *profile, QWidget *parent) +WebView::WebView(WebProfile *profile, cb_createWindow_t cb, QWidget *parent) : WebView(parent) { + cb_createWindow = cb; Q_CHECK_PTR(profile); m_profile = profile; setPage(new WebPage(profile, this)); } -WebView::WebView(const Session::WebView &data, QWidget *parent) +WebView::WebView(const Session::WebView &data, cb_createWindow_t cb, QWidget *parent) : WebView(parent) { - const auto *profileManager = WebProfileManager::instance(); - Q_CHECK_PTR(profileManager); + cb_createWindow = cb; + WebProfileManager profileManager; - auto *profile = profileManager->profile(data.profile); + auto *profile = profileManager.profile(data.profile); if(profile != nullptr) { setProfile(profile); } @@ -75,19 +72,11 @@ void WebView::setProfile(WebProfile *profile) Session::WebView WebView::serialize() const { - const auto *profileManager = WebProfileManager::instance(); - Q_CHECK_PTR(profileManager); - QByteArray historyData; QDataStream historyStream(&historyData, QIODevice::WriteOnly); historyStream << *history(); - return { profileManager->id(profile()), QString(), historyData }; -} - -bool WebView::isLoaded() const -{ - return m_loaded; + return { profile()->getId(), QString(), historyData }; } void WebView::search(const QString &term) @@ -96,35 +85,6 @@ void WebView::search(const QString &term) load(searchUrl); } -WebView *WebView::createWindow(QWebEnginePage::WebWindowType type) -{ - Q_CHECK_PTR(m_parentWindow); - - // parent Window has been found - auto index = m_parentWindow->addTab(); - WebView *view = m_parentWindow->view(index); - switch(type) { - case QWebEnginePage::WebBrowserWindow: - // a complete web browser window - break; - - case QWebEnginePage::WebBrowserTab: - // a web browser tab - m_parentWindow->setCurrentTab(index); - break; - - case QWebEnginePage::WebDialog: - // a window without decorations - break; - - case QWebEnginePage::WebBrowserBackgroundTab: - // a web browser tab, but don't swap to it - break; - } - - return view; -} - void WebView::contextMenuEvent(QContextMenuEvent *event) { auto *menu = new WebViewContextMenu(this); diff --git a/src/webengine/webview.h b/src/webengine/webview.h index 5748691..a9c6866 100644 --- a/src/webengine/webview.h +++ b/src/webengine/webview.h @@ -11,10 +11,10 @@ #include "webpage.h" #include <QWebEngineView> +#include <functional> #include <session.hpp> class WebProfile; -class SubWindow; class WebViewContextMenu; class WebView final : public QWebEngineView { @@ -25,8 +25,10 @@ class WebView final : public QWebEngineView explicit WebView(QWidget *parent = nullptr); public: - explicit WebView(WebProfile *profile = nullptr, QWidget *parent = nullptr); - explicit WebView(const Session::WebView &data, QWidget *parent = nullptr); + typedef std::function<WebView *(QWebEnginePage::WebWindowType)> cb_createWindow_t; + + WebView(WebProfile *profile, cb_createWindow_t cb, QWidget *parent = nullptr); + WebView(const Session::WebView &data, cb_createWindow_t cb, QWidget *parent = nullptr); ~WebView() = default; [[nodiscard]] WebProfile *profile() const @@ -37,7 +39,10 @@ public: [[nodiscard]] Session::WebView serialize() const; - bool isLoaded() const; + bool isLoaded() const + { + return m_loaded; + } public slots: void search(const QString &term); @@ -46,11 +51,14 @@ signals: void newBookmark(const QString &title, const QUrl &url); protected: - WebView *createWindow(QWebEnginePage::WebWindowType type) override; + WebView *createWindow(QWebEnginePage::WebWindowType type) override + { + return cb_createWindow ? cb_createWindow(type) : nullptr; + } void contextMenuEvent(QContextMenuEvent *event) override; private: - SubWindow *m_parentWindow = nullptr; + cb_createWindow_t cb_createWindow; WebProfile *m_profile = nullptr; bool m_loaded = false; diff --git a/src/webengine/webviewcontextmenu.cpp b/src/webengine/webviewcontextmenu.cpp index 856ff9c..1f6b337 100644 --- a/src/webengine/webviewcontextmenu.cpp +++ b/src/webengine/webviewcontextmenu.cpp @@ -7,7 +7,6 @@ */ #include "webviewcontextmenu.h" -#include "wallet/wallet.h" #include "webprofilemanager.h" #include "webview.h" #include <QContextMenuEvent> @@ -216,6 +215,7 @@ WebViewContextMenu::WebViewContextMenu(WebView *view) } #ifndef NDEBUG + /* { this->addSeparator(); auto *autofillAction = this->addAction(tr("Autofill form")); @@ -223,5 +223,6 @@ WebViewContextMenu::WebViewContextMenu(WebView *view) Wallet::autocompleteForm(view); }); }; + */ #endif } |