// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only /*! \example webenginewidgets/recipebrowser \title Recipe Browser \meta tag {widgets, webengine, webchannel, webenginescript} \ingroup webengine-widgetexamples \brief Injecting custom stylsheets into web pages and providing a rich text preview tool for a custom markup language. \examplecategory {Web Technologies} \image recipebrowser.webp \e {Recipe Browser} is a small hybrid web browser application. It demonstrates how to use the \l{Qt WebEngine Widgets C++ Classes} {Qt WebEngine C++ classes} to combine C++ and JavaScript logic in the following ways. \list \li Running arbitrary JavaScript code via \c QWebEnginePage::runJavaScript() to inject custom CSS stylesheets \li Using QWebEngineScript and QWebEngineScriptCollection to persist the JavaScript code and inject it to every page \li Using QWebChannel to interact with and provide a rich text preview for a custom markup language \endlist \l{http://daringfireball.net/projects/markdown/}{Markdown} is a lightweight markup language with a plain text formatting syntax. Some services, such as \l{http://github.com}{github}, acknowledge the format, and render the content as rich text when viewed in a browser. The Recipe Browser main window is split into a navigation on the left and a preview area on the right. The preview area on the right switches to an editor when the user clicks the Edit button on the top left of the main window. The editor supports the Markdown syntax and is implemented by using QPlainTextEdit. The document is rendered as rich text in the preview area, once the user clicks the View button,to which the Edit button transforms to. This rendering is implemented by using QWebEngineView. To render the text, a JavaScript library inside the web engine converts the Markdown text to HTML. The preview is updated from the editor through QWebChannel. \include examples-run.qdocinc \section1 Exposing Document Text To render the current Markdown text it needs to be exposed to the web engine through QWebChannel. To achieve this it has to be part of Qt metatype system. This is done by using a dedicated \c Document class that exposes the document text as a \c {Q_PROPERTY}: \quotefromfile webenginewidgets/recipebrowser/document.h \skipto class Document \printto #endif The \c Document class wraps a QString \c m_currentText to be set on the C++ side with the \c setText() method and exposes it at runtime as a \c text property with a \c textChanged signal. We define the \c setText method as follows: \quotefromfile webenginewidgets/recipebrowser/document.cpp \skipto Document::setText(const QString &text) \printuntil /^\}/ Additionally, the \c Document class keeps track of the current recipe via \c m_currentPage. We call the recipes pages here, because each recipe has its distinct HTML document that contains the initial text content. Furthermore, \c m_textCollection is a QMap that contains the key/value pairs \{page, text\}, so that changes made to the text content of a page is persisted between navigation. Nevertheless, we do not write the modified text contents to the drive, but instead we persist them between application start and shutdown via QSettings. \section1 Creating the Main Window The \c MainWindow class inherits the QMainWindow class: \quotefromfile webenginewidgets/recipebrowser/mainwindow.h \skipto class MainWindow : \printto endif The class declares private slots that match the two buttons on the top left, over the navigation list view. Additionally, helper methods for custom CSS stylesheets are declared. The actual layout of the main window is specified in a \c .ui file. The widgets and actions are available at runtime in the \c ui member variable. \c m_isEditMode is a boolean that toggles between the editor and the preview area. \c m_content is an instance of the \c Document class. The actual setup of the different objects is done in the \c MainWindow constructor: \quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp \skipto MainWindow::MainWindow \printto connect The constructor first calls \c setupUi to construct the widgets and menu actions according to the UI file. The text editor font is set to one with a fixed character width, and the QWebEngineView widget is told not to show a context menu. Furthermore, the editor is hidden away. \printto ui->recipes Here the \c clicked signals of QPushButton are connected to respective functions that show the stylesheets dialog or toggle between edit and view mode, that is, hide and show the editor and preview area respectively. \printto m_content.setTextEdit Here the navigation QListWidget on the left is setup with the 7 recipes. Also, the currentItemChanged signal of QListWidget is connected to a lambda that loads the new, current recipe page and updates the page in \c m_content. \printto connect Next, the pointer to the ui editor, a QPlainTextEdit, is passed to \c m_content to ensure that calls to \c Document::setInitialText() work properly. \printto QSettings Here the \c textChanged signal of the editor is connected to a lambda that updates the text in \c m_content. This object is then exposed to the JS side by \c QWebChannel under the name \c{content}. \printto ui->recipes By using QSettings we persist stylesheets between application runs. If there should be no stylesheets configured, for example, because the user deleted all of them in a previous run, we load default ones. \printto } Finally, we set the currently selected list item to the first contained in the navigation list widget. This triggers the previously mentioned QListWidget::currentItemChanged signal and navigates to the page of the list item. \section1 Working With Stylesheets We use JavaScript to create and append CSS elements to the documents. After declaring the script source, QWebEnginePage::runJavaScript() can run it immediately and apply newly created styles on the current content of the web view. Encapsulating the script into a QWebEngineScript and adding it to the script collection of QWebEnginePage makes its effect permanent. \quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp \skipto MainWindow::insertStyleSheet \printuntil /^\}/ Removing stylesheets can be done similarly: \quotefromfile webenginewidgets/recipebrowser/mainwindow.cpp \skipto MainWindow::removeStyleSheet \printuntil /^\}/ \section1 Creating a recipe file \quotefile webenginewidgets/recipebrowser/assets/pages/burger.html All the different recipe pages are set up the same way. In the \c part they include two CSS files: \c markdown.css, that styles the markdown, and custom.css, that does some further styling but most importantly hides the \c
with id \e content, as this \c
only contains the unmodified, initial content text. Also, three JS scripts are included. \c marked.js is responsible for parsing the markdown and transforming it into HTML. \c custom.js does some configuration of \c marked.js, and \c qwebchannel.js exposes the QWebChannel JavaScript API. In the body there are two \c
elements. The \c
with id \e placeholder gets the markdown text injected that is rendered and visible. The \c
with id \e content is hidden by \c custom.css and only contains the original, unmodified text content of the recipe. Finally, on the bottom of each recipe HTML file is a script that is responsible for the communication between the C++ and JavaScript side via QWebChannel. The original, unmodified text content inside the \c
with id \e content is passed to the C++ side and a callback is setup that is invoked when the \c textChanged signal of \c m_content is emitted. The callback then updates the contents of the \c
\e placeholder with the parsed markdown. \section1 Files and Attributions The example bundles the following code with third-party licenses: \table \row \li \l{recipebrowser-marked}{Marked} \li MIT License \row \li \l{recipebrowser-markdowncss}{Markdown.css} \li Apache License 2.0 \endtable */