view libgui/src/documentation.cc @ 31619:ad014fc78bd6

use individual local gui_settings objects Previously, we created a single gui_settings object (derived from QSettings) and accessed it from the resource_manager object. That design is not necessary and is not the way QSettings was designed to be used. Instead of managing a single object, we should be using individual QSettings objects where needed. Each individual QSettings object manages thread-safe access to a single global collection of settings. The Qt docs say that operations on QSettings are not thread safe, but that means that you can't create a QSettings object in one thread and use it in another without some locking. I'm not sure whether we were doing that correctly, but with this change it no longer matters. Each QSettings object does perform locking when reading or writing the underlying global data. * resource-manager.h, resource-manager.cc (resource_manager::m_settings): Delete data member. (resource_manager::get_settings): Delete. * annotation-dialog.cc, QTerminal.cc, QTerminal.h, command-widget.cc, command-widget.h, community-news.cc, dialog.cc, documentation-bookmarks.cc, documentation-bookmarks.h, documentation-dock-widget.cc, documentation-dock-widget.h, documentation.cc, documentation.h, dw-main-window.cc, dw-main-window.h, external-editor-interface.cc, files-dock-widget.cc, files-dock-widget.h, find-files-dialog.cc, history-dock-widget.cc, history-dock-widget.h, file-editor-interface.h, file-editor-tab.cc, file-editor-tab.h, file-editor.cc, file-editor.h, find-dialog.cc, octave-qscintilla.cc, main-window.cc, main-window.h, news-reader.cc, octave-dock-widget.cc, octave-dock-widget.h, qt-interpreter-events.cc, qt-interpreter-events.h, release-notes.cc, resource-manager.cc, resource-manager.h, set-path-dialog.cc, settings-dialog.cc, settings-dialog.h, shortcut-manager.cc, shortcut-manager.h, terminal-dock-widget.cc, terminal-dock-widget.h, variable-editor.cc, variable-editor.h, welcome-wizard.cc, workspace-model.cc, workspace-model.h, workspace-view.cc: Use local gui_settings objects instead of accessing a pointer to a single gui_settings object owned by the resource_manager object.
author John W. Eaton <jwe@octave.org>
date Fri, 02 Dec 2022 14:23:53 -0500
parents c8ad083a5802
children 0645ea65ca6b
line wrap: on
line source

////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2018-2022 The Octave Project Developers
//
// See the file COPYRIGHT.md in the top-level directory of this
// distribution or <https://octave.org/copyright/>.
//
// This file is part of Octave.
//
// Octave is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Octave is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Octave; see the file COPYING.  If not, see
// <https://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////

#if defined (HAVE_CONFIG_H)
#  include "config.h"
#endif

#include <QAction>
#include <QApplication>
#include <QCompleter>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFontDatabase>
#include <QHelpContentWidget>
#include <QHelpIndexWidget>
#if defined (HAVE_NEW_QHELPINDEXWIDGET_API) \
  || defined (HAVE_QHELPENGINE_DOCUMENTSFORIDENTIFIER)
#  include <QHelpLink>
#endif
#include <QHelpSearchEngine>
#include <QHelpSearchQueryWidget>
#include <QHelpSearchResultWidget>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QTabWidget>
#include <QTimer>
#include <QVBoxLayout>

#include "documentation.h"
#include "documentation-bookmarks.h"
#include "gui-preferences-global.h"
#include "gui-preferences-dc.h"
#include "gui-preferences-sc.h"
#include "gui-settings.h"
#include "octave-qobject.h"
#include "shortcut-manager.h"

#include "defaults.h"
#include "file-ops.h"
#include "oct-env.h"

namespace octave
{
  // The documentation splitter, which is the main widget
  // of the doc dock widget
  documentation::documentation (QWidget *p, base_qobject& oct_qobj)
    : QSplitter (Qt::Horizontal, p),
      m_octave_qobj (oct_qobj), m_doc_widget (this),
      m_tool_bar (new QToolBar (this)),
      m_query_string (QString ()),
      m_indexed (false),
      m_current_ref_name (QString ()),
      m_prev_pages_menu (new QMenu (this)),
      m_next_pages_menu (new QMenu (this)),
      m_prev_pages_count (0),
      m_next_pages_count (0),
      m_findnext_shortcut (new QShortcut (this)),
      m_findprev_shortcut (new QShortcut (this))
  {
    // Get original collection
    QString collection = getenv ("OCTAVE_QTHELP_COLLECTION");
    if (collection.isEmpty ())
      collection = QString::fromStdString (config::oct_doc_dir ()
                                           + sys::file_ops::dir_sep_str ()
                                           + "octave_interpreter.qhc");

    // Setup the help engine with the original collection, use a writable copy
    // of the original collection and load the help data
    m_help_engine = new QHelpEngine (collection, this);

    // Mark help as readonly to avoid error if collection file is stored in a
    // readonly location
    m_help_engine->setProperty ("_q_readonly",
                                QVariant::fromValue<bool> (true));

    QString tmpdir = QString::fromStdString (sys::env::get_temp_directory ());
    m_collection
      = QString::fromStdString (sys::tempnam (tmpdir.toStdString (),
                                              "oct-qhelp-"));

    if (m_help_engine->copyCollectionFile (m_collection))
      m_help_engine->setCollectionFile (m_collection);
    else
#ifdef ENABLE_DOCS
      // FIXME: Perhaps a better way to do this would be to keep a count
      // in the GUI preferences file.  After issuing this warning 3 times
      // it would be disabled.  The count would need to be reset when a new
      // version of Octave is installed.
      QMessageBox::warning (this, tr ("Octave Documentation"),
                            tr ("Could not copy help collection to temporary\n"
                                "file. Search capabilities may be affected.\n"
                                "%1").arg (m_help_engine->error ()));
#endif

    connect(m_help_engine->searchEngine (), SIGNAL(indexingFinished ()),
            this, SLOT(load_index ()));
    connect(m_help_engine, SIGNAL(setupFinished ()),
            m_help_engine->searchEngine (), SLOT(reindexDocumentation ()));

    if (! m_help_engine->setupData())
      {
#ifdef ENABLE_DOCS
        QMessageBox::warning (this, tr ("Octave Documentation"),
                              tr ("Could not setup the data required for the\n"
                                  "documentation viewer. Only help texts in\n"
                                  "the Command Window will be available."));
#endif

        disconnect (m_help_engine, 0, 0, 0);

        delete m_help_engine;
        m_help_engine = nullptr;
      }

    // The browser
    QWidget *browser_find = new QWidget (this);
    m_doc_browser = new documentation_browser (m_help_engine, browser_find);
    connect (m_doc_browser, &documentation_browser::cursorPositionChanged,
             this, &documentation::handle_cursor_position_change);

    // Tool bar
    construct_tool_bar ();

    // Find bar
    QWidget *find_footer = new QWidget (browser_find);
    QLabel *find_label = new QLabel (tr ("Find:"), find_footer);
    m_find_line_edit = new QLineEdit (find_footer);
    connect (m_find_line_edit, &QLineEdit::returnPressed,
             this, [=] () { find (); });
    connect (m_find_line_edit, &QLineEdit::textEdited,
             this, &documentation::find_forward_from_anchor);
    QToolButton *forward_button = new QToolButton (find_footer);
    forward_button->setText (tr ("Search forward"));
    forward_button->setToolTip (tr ("Search forward"));
    resource_manager& rmgr = m_octave_qobj.get_resource_manager ();
    forward_button->setIcon (rmgr.icon ("go-down"));
    connect (forward_button, &QToolButton::pressed,
             this, [=] () { find (); });
    QToolButton *backward_button = new QToolButton (find_footer);
    backward_button->setText (tr ("Search backward"));
    backward_button->setToolTip (tr ("Search backward"));
    backward_button->setIcon (rmgr.icon ("go-up"));
    connect (backward_button, &QToolButton::pressed,
             this, &documentation::find_backward);
    QHBoxLayout *h_box_find_footer = new QHBoxLayout (find_footer);
    h_box_find_footer->addWidget (find_label);
    h_box_find_footer->addWidget (m_find_line_edit);
    h_box_find_footer->addWidget (forward_button);
    h_box_find_footer->addWidget (backward_button);
    h_box_find_footer->setMargin (2);
    find_footer->setLayout (h_box_find_footer);

    QVBoxLayout *v_box_browser_find = new QVBoxLayout (browser_find);
    v_box_browser_find->addWidget (m_tool_bar);
    v_box_browser_find->addWidget (m_doc_browser);
    v_box_browser_find->addWidget (find_footer);
    browser_find->setLayout (v_box_browser_find);

    notice_settings ();

    m_findnext_shortcut->setContext (Qt::WidgetWithChildrenShortcut);
    connect (m_findnext_shortcut, &QShortcut::activated,
             this, [=] () { find (); });
    m_findprev_shortcut->setContext (Qt::WidgetWithChildrenShortcut);
    connect (m_findprev_shortcut, &QShortcut::activated,
             this, &documentation::find_backward);

    find_footer->hide ();
    m_search_anchor_position = 0;

    if (m_help_engine)
      {
#if defined (HAVE_NEW_QHELPINDEXWIDGET_API)
        // Starting in Qt 5.15, help engine uses filters instead of old API
        m_help_engine->setUsesFilterEngine (true);
#endif
        // Layout contents, index and search
        QTabWidget *navi = new QTabWidget (this);
        navi->setTabsClosable (false);
        navi->setMovable (true);

        // Contents
        QHelpContentWidget *content = m_help_engine->contentWidget ();
        content->setObjectName ("documentation_tab_contents");
        navi->addTab (content, tr ("Contents"));

        connect (m_help_engine->contentWidget (),
                 &QHelpContentWidget::linkActivated,
                 m_doc_browser, [=] (const QUrl& url) {
                   m_doc_browser->handle_index_clicked (url); });

        // Index
        QHelpIndexWidget *index = m_help_engine->indexWidget ();

        m_filter = new QComboBox (this);
        m_filter->setToolTip (tr ("Enter text to search the indices"));
        m_filter->setEditable (true);
        m_filter->setInsertPolicy (QComboBox::NoInsert);
        m_filter->setMaxCount (10);
        m_filter->setMaxVisibleItems (10);
        m_filter->setSizeAdjustPolicy (QComboBox::AdjustToMinimumContentsLengthWithIcon);
        QSizePolicy sizePol (QSizePolicy::Expanding, QSizePolicy::Preferred);
        m_filter->setSizePolicy (sizePol);
        m_filter->completer ()->setCaseSensitivity (Qt::CaseSensitive);
        QLabel *filter_label = new QLabel (tr ("Search"));

        QWidget *filter_all = new QWidget (navi);
        QHBoxLayout *h_box_index = new QHBoxLayout (filter_all);
        h_box_index->addWidget (filter_label);
        h_box_index->addWidget (m_filter);
        h_box_index->setMargin (2);
        filter_all->setLayout (h_box_index);

        QWidget *index_all = new QWidget (navi);
        index_all->setObjectName ("documentation_tab_index");
        QVBoxLayout *v_box_index = new QVBoxLayout (index_all);
        v_box_index->addWidget (filter_all);
        v_box_index->addWidget (index);
        index_all->setLayout (v_box_index);

        navi->addTab (index_all, tr ("Function Index"));

#if defined (HAVE_NEW_QHELPINDEXWIDGET_API)
        connect (m_help_engine->indexWidget (),
                 &QHelpIndexWidget::documentActivated,
                 this, [=] (const QHelpLink &link) {
                   m_doc_browser->handle_index_clicked (link.url); });
#else
        connect (m_help_engine->indexWidget (),
                 &QHelpIndexWidget::linkActivated,
                 m_doc_browser, &documentation_browser::handle_index_clicked);
#endif

        connect (m_filter, &QComboBox::editTextChanged,
                 this, &documentation::filter_update);

        connect (m_filter->lineEdit (), &QLineEdit::editingFinished,
                 this, &documentation::filter_update_history);

        // Bookmarks (own class)
        m_bookmarks
          = new documentation_bookmarks (this, m_doc_browser, m_octave_qobj, navi);
        navi->addTab (m_bookmarks, tr ("Bookmarks"));

        connect (m_action_bookmark, &QAction::triggered,
                 m_bookmarks, [=] () { m_bookmarks->add_bookmark (); });

        // Search
        QHelpSearchEngine *search_engine = m_help_engine->searchEngine ();
        QHelpSearchQueryWidget *search = search_engine->queryWidget ();
        QHelpSearchResultWidget *result = search_engine->resultWidget ();
        QWidget *search_all = new QWidget (navi);
        QVBoxLayout *v_box_search = new QVBoxLayout (search_all);
        v_box_search->addWidget (search);
        v_box_search->addWidget (result);
        search_all->setLayout (v_box_search);
        search_all->setObjectName ("documentation_tab_search");
        navi->addTab (search_all, tr ("Search"));

        connect (search, &QHelpSearchQueryWidget::search,
                 this, &documentation::global_search);

        connect (search_engine, &QHelpSearchEngine::searchingStarted,
                 this, &documentation::global_search_started);
        connect (search_engine, &QHelpSearchEngine::searchingFinished,
                 this, &documentation::global_search_finished);

        connect (search_engine->resultWidget (),
                 &QHelpSearchResultWidget::requestShowLink,
                 this, &documentation::handle_search_result_clicked);

        // Fill the splitter
        insertWidget (0, navi);
        insertWidget (1, browser_find);
        setStretchFactor (1, 1);
      }
  }

  documentation::~documentation (void)
  {
    // Cleanup temporary file and directory
    QFile file (m_collection);
    if (file.exists ())
      {
        QFileInfo finfo (file);
        QString bname = finfo.fileName ();
        QDir dir = finfo.absoluteDir ();
        dir.setFilter (QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden);
        QStringList namefilter;
        namefilter.append ("*" + bname + "*");
        for (const auto& fi : dir.entryInfoList (namefilter))
          {
            std::string file_name = fi.absoluteFilePath ().toStdString ();
            sys::recursive_rmdir (file_name);
          }

        file.remove();
      }
  }

  QAction * documentation::add_action (const QIcon& icon, const QString& text,
                                       const char *member, QWidget *receiver,
                                       QToolBar *tool_bar)
  {
    QAction *a;
    QWidget *r = this;
    if (receiver != nullptr)
      r = receiver;

    a = new QAction (icon, text, this);

    if (member)
      connect (a, SIGNAL (triggered ()), r, member);

    if (tool_bar)
      tool_bar->addAction (a);

    m_doc_widget->addAction (a);  // important for shortcut context
    a->setShortcutContext (Qt::WidgetWithChildrenShortcut);

    return a;
  }

  void documentation::construct_tool_bar (void)
  {
    // Home, Previous, Next
    resource_manager& rmgr = m_octave_qobj.get_resource_manager ();
    m_action_go_home
      = add_action (rmgr.icon ("go-home"), tr ("Go home"), SLOT (home (void)),
                    m_doc_browser, m_tool_bar);

    m_action_go_prev
      = add_action (rmgr.icon ("go-previous"), tr ("Go back"),
                    SLOT (backward (void)), m_doc_browser, m_tool_bar);
    m_action_go_prev->setEnabled (false);

    // popdown menu with prev pages files
    QToolButton *popdown_button_prev_pages = new QToolButton ();
    popdown_button_prev_pages->setToolTip (tr ("Previous pages"));
    popdown_button_prev_pages->setMenu (m_prev_pages_menu);
    popdown_button_prev_pages->setPopupMode (QToolButton::InstantPopup);
    popdown_button_prev_pages->setToolButtonStyle (Qt::ToolButtonTextOnly);
    popdown_button_prev_pages->setCheckable (false);
    popdown_button_prev_pages->setArrowType(Qt::DownArrow);
    m_tool_bar->addWidget (popdown_button_prev_pages);

    m_action_go_next
      = add_action (rmgr.icon ("go-next"), tr ("Go forward"),
                    SLOT (forward (void)), m_doc_browser, m_tool_bar);
    m_action_go_next->setEnabled (false);

    // popdown menu with prev pages files
    QToolButton *popdown_button_next_pages = new QToolButton ();
    popdown_button_next_pages->setToolTip (tr ("Next pages"));
    popdown_button_next_pages->setMenu (m_next_pages_menu);
    popdown_button_next_pages->setPopupMode (QToolButton::InstantPopup);
    popdown_button_next_pages->setToolButtonStyle (Qt::ToolButtonTextOnly);
    popdown_button_next_pages->setArrowType(Qt::DownArrow);
    m_tool_bar->addWidget (popdown_button_next_pages);

    connect (m_doc_browser, &documentation_browser::backwardAvailable,
             m_action_go_prev, &QAction::setEnabled);
    connect (m_doc_browser, &documentation_browser::backwardAvailable,
             popdown_button_prev_pages, &QToolButton::setEnabled);
    connect (m_doc_browser, &documentation_browser::forwardAvailable,
             m_action_go_next, &QAction::setEnabled);
    connect (m_doc_browser, &documentation_browser::forwardAvailable,
             popdown_button_next_pages, &QToolButton::setEnabled);
    connect (m_doc_browser, &documentation_browser::historyChanged,
             this, &documentation::update_history_menus);

    // Init prev/next menus
    for (int i = 0; i < max_history_entries; ++i)
      {
        m_prev_pages_actions[i] = new QAction (this);
        m_prev_pages_actions[i]->setVisible (false);
        m_next_pages_actions[i] = new QAction (this);
        m_next_pages_actions[i]->setVisible (false);
        m_prev_pages_menu->addAction (m_prev_pages_actions[i]);
        m_next_pages_menu->addAction (m_next_pages_actions[i]);
      }

    connect (m_prev_pages_menu, &QMenu::triggered,
             this, &documentation::open_hist_url);
    connect (m_next_pages_menu, &QMenu::triggered,
             this, &documentation::open_hist_url);

    // Find
    m_tool_bar->addSeparator ();
    m_action_find
      = add_action (rmgr.icon ("edit-find"), tr ("Find"),
                    SLOT (activate_find (void)), this, m_tool_bar);

    // Zoom
    m_tool_bar->addSeparator ();
    m_action_zoom_in
      = add_action (rmgr.icon ("view-zoom-in"), tr ("Zoom in"),
                    SLOT (zoom_in (void)), m_doc_browser, m_tool_bar);
    m_action_zoom_out
      = add_action (rmgr.icon ("view-zoom-out"), tr ("Zoom out"),
                    SLOT (zoom_out (void)), m_doc_browser, m_tool_bar);
    m_action_zoom_original
      = add_action (rmgr.icon ("view-zoom-original"), tr ("Zoom original"),
                    SLOT (zoom_original (void)), m_doc_browser, m_tool_bar);

    // Bookmarks (connect slots later)
    m_tool_bar->addSeparator ();
    m_action_bookmark
      = add_action (rmgr.icon ("bookmark-new"), tr ("Bookmark current page"),
                    nullptr, nullptr, m_tool_bar);
  }

  void documentation::global_search (void)
  {
    if (! m_help_engine)
      return;

    QString query_string;
#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
    QString queries
      = m_help_engine->searchEngine ()->queryWidget ()->searchInput ();
    query_string = queries;
#else
    // FIXME: drop this part when support for Qt4 is dropped
    QList<QHelpSearchQuery> queries
      = m_help_engine->searchEngine ()->queryWidget ()->query ();
    if (queries.count ())
      query_string = queries.first ().wordList.join (" ");
    else
      query_string = "";
#endif

    if (query_string.isEmpty ())
      return;

    // Get quoted search strings first, then take first string as fall back
    QRegExp rx ("\"([^\"]*)\"");
    if (rx.indexIn (query_string, 0) != -1)
      m_internal_search = rx.cap (1);
    else
#if defined (HAVE_QT_SPLITBEHAVIOR_ENUM)
      m_internal_search = query_string.split (" ", Qt::SkipEmptyParts).first ();
#else
      m_internal_search = query_string.split (" ", QString::SkipEmptyParts).first ();
#endif

    m_help_engine->searchEngine ()->search (queries);
  }

  void documentation::global_search_started (void)
  {
    qApp->setOverrideCursor(QCursor(Qt::WaitCursor));
  }

  void documentation::global_search_finished (int)
  {
    if (! m_help_engine)
      return;

    if (! m_internal_search.isEmpty ())
      {
        m_query_string = m_internal_search;

        QHelpSearchEngine *search_engine = m_help_engine->searchEngine ();
        if (search_engine)
          {
#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
            QVector<QHelpSearchResult> res
              = search_engine->searchResults (0, search_engine->searchResultCount ());
#else
            QList< QPair<QString, QString> > res
              = search_engine->hits (0, search_engine->hitCount ());
#endif

            if (res.count ())
              {
                QUrl url;

                if (res.count () == 1)
#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
                  url = res.front ().url ();
#else
                  url = res.front ().first;
#endif
                else
                  {
                    // Remove the quotes we added
                    QString search_string = m_internal_search;

                    for (auto r = res.begin (); r != res.end (); r++)
                      {
#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
                        QString title = r->title ().toLower ();
                        QUrl tmpurl = r->url ();
#else
                        QString title = r->second.toLower ();
                        QUrl tmpurl = r->first;
#endif
                        if (title.contains (search_string.toLower ()))
                          {
                            if (title.indexOf (search_string.toLower ()) == 0)
                              {
                                url = tmpurl;
                                break;
                              }
                            else if (url.isEmpty ())
                              url = tmpurl;
                          }
                      }
                  }

                if (! url.isEmpty ())
                  {
                    connect (this, &documentation::show_single_result,
                             this, &documentation::handle_search_result_clicked);

                    emit show_single_result (url);
                  }
              }
          }

        m_internal_search = QString ();
      }

    qApp->restoreOverrideCursor();
  }

  void documentation::handle_search_result_clicked (const QUrl& url)
  {
    // Open url with matching text
    m_doc_browser->handle_index_clicked (url);

    // Select all occurrences of matching text
    select_all_occurrences (m_query_string);

    // Open search widget with matching text as search string
    m_find_line_edit->setText (m_query_string);
    m_find_line_edit->parentWidget ()->show ();

    // If no occurrence can be found go to the top of the page
    if (! m_doc_browser->find (m_find_line_edit->text ()))
      m_doc_browser->moveCursor (QTextCursor::Start);
    else
      {
        // Go to to first occurrence of search text.  Going to the end and then
        // search backwards until the last occurrence ensures the search text
        // is visible in the first line of the visible part of the text.
        m_doc_browser->moveCursor (QTextCursor::End);
        while (m_doc_browser->find (m_find_line_edit->text (),
                                    QTextDocument::FindBackward));
      }
  }

  void documentation::select_all_occurrences (const QString& text)
  {
    // Get highlight background and text color
    QPalette pal = QApplication::palette ();
    QTextCharFormat format;
    QColor col = pal.color (QPalette::Highlight);
    col.setAlphaF (0.25);
    format.setBackground (QBrush (col));
    format.setForeground (QBrush (pal.color (QPalette::Text)));

    // Create list for extra selected items
    QList<QTextEdit::ExtraSelection> selected;
    m_doc_browser->moveCursor (QTextCursor::Start);

    // Find all occurrences and add them to the selection
    while ( m_doc_browser->find (text) )
      {
        QTextEdit::ExtraSelection selected_item;
        selected_item.cursor = m_doc_browser->textCursor ();
        selected_item.format = format;
        selected.append (selected_item);
      }

    // Apply selection and move back to the beginning
    m_doc_browser->setExtraSelections (selected);
    m_doc_browser->moveCursor (QTextCursor::Start);
  }

  void documentation::notice_settings (void)
  {
    gui_settings settings;

    // If m_help_engine is not defined, the objects accessed by this method
    // are not valid.  Thus, just return in this case.
    if (! m_help_engine)
      return;

    // Icon size in the toolbar.
    int size_idx = settings.value (global_icon_size).toInt ();
    size_idx = (size_idx > 0) - (size_idx < 0) + 1;  // Make valid index from 0 to 2

    QStyle *st = style ();
    int icon_size = st->pixelMetric (global_icon_sizes[size_idx]);
    m_tool_bar->setIconSize (QSize (icon_size, icon_size));

    // Shortcuts
    shortcut_manager& scmgr = m_octave_qobj.get_shortcut_manager ();

    scmgr.set_shortcut (m_action_find, sc_edit_edit_find_replace);
    scmgr.shortcut (m_findnext_shortcut, sc_edit_edit_find_next);
    scmgr.shortcut (m_findprev_shortcut, sc_edit_edit_find_previous);
    scmgr.set_shortcut (m_action_zoom_in, sc_edit_view_zoom_in);
    scmgr.set_shortcut (m_action_zoom_out, sc_edit_view_zoom_out);
    scmgr.set_shortcut (m_action_zoom_original, sc_edit_view_zoom_normal);
    scmgr.set_shortcut (m_action_go_home, sc_doc_go_home);
    scmgr.set_shortcut (m_action_go_prev, sc_doc_go_back);
    scmgr.set_shortcut (m_action_go_next, sc_doc_go_next);
    scmgr.set_shortcut (m_action_bookmark, sc_doc_bookmark);

    // Settings for the browser
    m_doc_browser->notice_settings ();
  }

  void documentation::save_settings (void)
  {
    gui_settings settings;

    m_doc_browser->save_settings ();
    m_bookmarks->save_settings ();
  }

  void documentation::copyClipboard (void)
  {
    if (m_doc_browser->hasFocus ())
      {
        m_doc_browser->copy();
      }
  }

  void documentation::pasteClipboard (void) { }

  void documentation::selectAll (void) { }

  void documentation::load_index (void)
  {
    m_indexed = true;

    // Show index if no other page is required.
    if (m_current_ref_name.isEmpty ())
      m_doc_browser->setSource
        (QUrl ("qthelp://org.octave.interpreter-1.0/doc/octave.html/index.html"));
    else
      load_ref (m_current_ref_name);

    m_help_engine->contentWidget ()->expandToDepth (0);
  }

  void documentation::load_ref (const QString& ref_name)
  {
    if (! m_help_engine || ref_name.isEmpty ())
      return;

    m_current_ref_name = ref_name;

    if (! m_indexed)
      return;

#if defined (HAVE_QHELPENGINE_DOCUMENTSFORIDENTIFIER)
    QList<QHelpLink> found_links
      = m_help_engine->documentsForIdentifier (ref_name);
#else
    QMap<QString, QUrl> found_links
      = m_help_engine->linksForIdentifier (ref_name);
#endif

    QTabWidget *navi = static_cast<QTabWidget *> (widget (0));

    if (found_links.count() > 0)
      {
        // First search in the function index
#if defined (HAVE_QHELPENGINE_DOCUMENTSFORIDENTIFIER)
        QUrl first_url = found_links.constFirst().url;
#else
        QUrl first_url = found_links.constBegin().value ();
#endif

        m_doc_browser->setSource (first_url);

        // Switch to function index tab
        m_help_engine->indexWidget()->filterIndices (ref_name);
        QWidget *index_tab
          = navi->findChild<QWidget *> ("documentation_tab_index");
        navi->setCurrentWidget (index_tab);
      }
    else
      {
        // Use full text search to provide the best match
        QHelpSearchEngine *search_engine = m_help_engine->searchEngine ();
        QHelpSearchQueryWidget *search_query = search_engine->queryWidget ();

#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
        QString query = ref_name;
        query.prepend ("\"").append ("\"");
#else
        QList<QHelpSearchQuery> query;
        query << QHelpSearchQuery (QHelpSearchQuery::DEFAULT,
                                   QStringList (QString("\"") + ref_name + QString("\"")));
#endif
        m_internal_search = ref_name;
        search_engine->search (query);

        // Switch to search tab
#if defined (HAVE_QHELPSEARCHQUERYWIDGET_SEARCHINPUT)
        search_query->setSearchInput (query);
#else
        search_query->setQuery (query);
#endif
        QWidget *search_tab
          = navi->findChild<QWidget *> ("documentation_tab_search");
        navi->setCurrentWidget (search_tab);
      }
  }

  void documentation::activate_find (void)
  {
    if (m_find_line_edit->parentWidget ()->isVisible ())
      {
        m_find_line_edit->parentWidget ()->hide ();
        m_doc_browser->setFocus ();
      }
    else
      {
        m_find_line_edit->parentWidget ()->show ();
        m_find_line_edit->selectAll ();
        m_find_line_edit->setFocus ();
      }
  }

  void documentation::filter_update (const QString& expression)
  {
    if (! m_help_engine)
      return;

    QString wildcard;
    if (expression.contains (QLatin1Char('*')))
      wildcard = expression;

    m_help_engine->indexWidget ()->filterIndices(expression, wildcard);
  }

  void documentation::filter_update_history (void)
  {
    QString text = m_filter->currentText ();   // get current text
    int index = m_filter->findText (text);     // and its actual index

    if (index > -1)
      m_filter->removeItem (index);            // remove if already existing

    m_filter->insertItem (0, text);            // (re)insert at beginning
    m_filter->setCurrentIndex (0);
  }

  void documentation::find_backward (void)
  {
    find (true);
  }

  void documentation::find (bool backward)
  {
    if (! m_help_engine)
      return;

    QTextDocument::FindFlags find_flags;
    if (backward)
      find_flags = QTextDocument::FindBackward;

    if (! m_doc_browser->find (m_find_line_edit->text (), find_flags))
      {
        // Nothing was found, restart search from the begin or end of text
        QTextCursor textcur = m_doc_browser->textCursor ();
        if (backward)
          textcur.movePosition (QTextCursor::End);
        else
          textcur.movePosition (QTextCursor::Start);
        m_doc_browser->setTextCursor (textcur);
        m_doc_browser->find (m_find_line_edit->text (), find_flags);
      }

    record_anchor_position ();
  }

  void documentation::find_forward_from_anchor (const QString& text)
  {
    if (! m_help_engine)
      return;

    // Search from the current position
    QTextCursor textcur = m_doc_browser->textCursor ();
    textcur.setPosition (m_search_anchor_position);
    m_doc_browser->setTextCursor (textcur);

    if (! m_doc_browser->find (text))
      {
        // Nothing was found, restart search from the beginning
        textcur.movePosition (QTextCursor::Start);
        m_doc_browser->setTextCursor (textcur);
        m_doc_browser->find (text);
      }
  }

  void documentation::record_anchor_position (void)
  {
    if (! m_help_engine)
      return;

    m_search_anchor_position = m_doc_browser->textCursor ().position ();
  }

  void documentation::handle_cursor_position_change (void)
  {
    if (! m_help_engine)
      return;

    if (m_doc_browser->hasFocus ())
      record_anchor_position ();
  }

  void documentation::registerDoc (const QString& qch)
  {
    if (m_help_engine)
      {
        QString ns = m_help_engine->namespaceName (qch);
        bool do_setup = true;
        if (m_help_engine->registeredDocumentations ().contains (ns))
          {
            if (m_help_engine->documentationFileName (ns) == qch)
              do_setup = false;
            else
              {
                m_help_engine->unregisterDocumentation (ns);
                m_help_engine->registerDocumentation (qch);
              }
          }
        else if (! m_help_engine->registerDocumentation (qch))
          {
            QMessageBox::warning (this, tr ("Octave Documentation"),
                                  tr ("Unable to register help file %1.").
                                  arg (qch));
            return;
          }

        if (do_setup)
          m_help_engine->setupData();
      }
  }

  void documentation::unregisterDoc (const QString& qch)
  {
    if (! m_help_engine)
      return;

    QString ns = m_help_engine->namespaceName (qch);
    if (m_help_engine
        && m_help_engine->registeredDocumentations ().contains (ns)
        && m_help_engine->documentationFileName (ns) == qch)
      {
        m_help_engine->unregisterDocumentation (ns);
        m_help_engine->setupData ();
      }
  }

  void documentation::update_history_menus (void)
  {
    if (m_prev_pages_count != m_doc_browser->backwardHistoryCount ())
      {
        update_history (m_doc_browser->backwardHistoryCount (),
                        m_prev_pages_actions);
        m_prev_pages_count = m_doc_browser->backwardHistoryCount ();
      }

    if (m_next_pages_count != m_doc_browser->forwardHistoryCount ())
      {
        update_history (m_doc_browser->forwardHistoryCount (),
                        m_next_pages_actions);
        m_next_pages_count = m_doc_browser->forwardHistoryCount ();
      }
  }

  void documentation::update_history (int new_count, QAction **actions)
  {
    // Which menu has to be updated?
    int prev_next = -1;
    QAction *a = m_action_go_prev;
    if (actions == m_next_pages_actions)
      {
        prev_next = 1;
        a = m_action_go_next;
      }

    // Get maximal count limited by array size
    int count = qMin (new_count, int (max_history_entries));

    // Fill used menu entries
    for (int i = 0; i < count; i++)
      {
        QString title
          = title_and_anchor (m_doc_browser->historyTitle (prev_next*(i+1)),
                              m_doc_browser->historyUrl (prev_next*(i+1)));

        if (i == 0)
          a->setText (title); // set tool tip for prev/next buttons

        actions[i]->setText (title);
        actions[i]->setData (m_doc_browser->historyUrl (prev_next*(i+1)));
        actions[i]->setEnabled (true);
        actions[i]->setVisible (true);
      }

    // Hide unused menu entries
    for (int j = count; j < max_history_entries; j++)
      {
        actions[j]->setEnabled (false);
        actions[j]->setVisible (false);
      }
  }

  void documentation::open_hist_url (QAction *a)
  {
    m_doc_browser->setSource (a->data ().toUrl ());
  }

  // Utility functions

  QString documentation::title_and_anchor (const QString& title, const QUrl& url)
  {
    QString retval = title;
    QString u = url.toString ();

    retval.remove (QRegExp ("\\s*\\(*GNU Octave \\(version [^\\)]*\\)[: \\)]*"));

    // Since the title only contains the section name and not the
    // specific anchor, extract the latter from the url and append
    // it to the title
    if (u.contains ('#'))
      {
        // Get the anchor from the url
        QString anchor = u.split ('#').last ();
        // Remove internal string parts
        anchor.remove (QRegExp ("^index-"));
        anchor.remove (QRegExp ("^SEC_"));
        anchor.remove (QRegExp ("^XREF"));
        anchor.remove ("Concept-Index_cp_letter-");
        anchor.replace ("-", " ");

        // replace encoded special chars by their unencoded versions
        QRegExp rx = QRegExp ("_00([0-7][0-9a-f])");
        int pos = 0;
        while ((pos = rx.indexIn(anchor, pos)) != -1)
          {
            anchor.replace ("_00"+rx.cap (1), QChar (rx.cap (1).toInt (nullptr, 16)));
            pos += rx.matchedLength();
          }

        if (retval != anchor)
          retval = retval + ": " + anchor;
      }

    return retval;
  }

  //
  // The documentation browser
  //

  documentation_browser::documentation_browser (QHelpEngine *he, QWidget *p)
    : QTextBrowser (p), m_help_engine (he), m_zoom_level (max_zoom_level+1)
  {
    setOpenLinks (false);
    connect (this, &documentation_browser::anchorClicked,
             this, [=] (const QUrl& url) { handle_index_clicked (url); });

    // Make sure we have access to one of the monospace fonts listed in
    // octave.css for rendering formated code blocks
    QStringList fonts = {"Fantasque Sans Mono", "FreeMono", "Courier New",
                         "Cousine", "Courier"};

    bool load_default_font = true;

    for (int i = 0; i < fonts.size (); ++i)
      {
        QFont font (fonts.at (i));
        if (font.exactMatch ())
          {
            load_default_font = false;
            break;
          }
      }

    if (load_default_font)
      {
        QString fonts_dir =
          QString::fromStdString (sys::env::getenv ("OCTAVE_FONTS_DIR")
                                  + sys::file_ops::dir_sep_str ());

        QStringList default_fonts = {"FreeMono", "FreeMonoBold",
                                     "FreeMonoBoldOblique", "FreeMonoOblique"};

        for (int i = 0; i < default_fonts.size (); ++i)
          {
            QString fontpath =
              fonts_dir + default_fonts.at(i) + QString (".otf");
            QFontDatabase::addApplicationFont (fontpath);
          }
      }
  }

  void documentation_browser::handle_index_clicked (const QUrl& url,
                                                    const QString&)
  {
    if (url.scheme () == "qthelp")
      setSource (url);
    else
      QDesktopServices::openUrl (url);
  }

  void documentation_browser::notice_settings (void)
  {
    gui_settings settings;

    // Zoom level only at startup, not when other settings have changed
    if (m_zoom_level > max_zoom_level)
      {
        m_zoom_level = settings.value (dc_browser_zoom_level).toInt ();
        zoomIn (m_zoom_level);
      }
  }

  QVariant documentation_browser::loadResource (int type, const QUrl& url)
  {
    if (m_help_engine && url.scheme () == "qthelp")
      return QVariant (m_help_engine->fileData(url));
    else
      return QTextBrowser::loadResource(type, url);
  }

  void documentation_browser::save_settings (void)
  {
    gui_settings settings;

    settings.setValue (dc_browser_zoom_level.key, m_zoom_level);

    settings.sync ();
  }

  void documentation_browser::zoom_in (void)
  {
    if (m_zoom_level < max_zoom_level)
      {
        zoomIn ();
        m_zoom_level++;
      }
  }

  void documentation_browser::zoom_out (void)
  {
    if (m_zoom_level > min_zoom_level)
      {
        zoomOut ();
        m_zoom_level--;
      }
  }

  void documentation_browser::zoom_original (void)
  {
    zoomIn (- m_zoom_level);
    m_zoom_level = 0;
  }

  void documentation_browser::wheelEvent (QWheelEvent *we)
  {
    if (we->modifiers () == Qt::ControlModifier)
      {
#if defined (HAVE_QWHEELEVENT_ANGLEDELTA)
        if (we->angleDelta().y () > 0)
#else
        if (we->delta() > 0)
#endif
          zoom_in ();
        else
          zoom_out ();

        we->accept ();
      }
    else
      QTextEdit::wheelEvent (we);
  }

}