view libgui/src/documentation.cc @ 33586:3216c01fd6a7 stable tip

fix dragging editor from main window into floating state (bug #65725) * file-editor.cc (toplevel_changes): added missing call to original slot octave_doc_widget::toplevel_changed
author Torsten Lilge <ttl-octave@mailbox.org>
date Tue, 14 May 2024 22:03:47 +0200
parents abce1aa7f66f
children 05f246fa1e06
line wrap: on
line source

////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2018-2024 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 <QRegularExpression>
#include <QTabWidget>
#include <QTimer>
#include <QWheelEvent>
#include <QVBoxLayout>
#include <QWheelEvent>

#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 "defaults.h"
#include "file-ops.h"
#include "oct-env.h"

OCTAVE_BEGIN_NAMESPACE(octave)

// The documentation splitter, which is the main widget
// of the doc dock widget
documentation::documentation (QWidget *p)
  : QSplitter (Qt::Horizontal, p),
    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
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  m_help_engine->setReadOnly (true);
#else
  m_help_engine->setProperty ("_q_readonly",
                              QVariant::fromValue<bool> (true));
#endif

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

  bool copy_ok = false;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
  // FIXME: Qt6: copyCollectionFile truncates the collection file.
  // This workaround normally copies the file. Since the relativ
  // link to the qch file is not updated then, the namespace and
  // the original qch file are re-registered.
  QStringList namespaces = m_help_engine->registeredDocumentations ();
  QString qch_file;
  if (! namespaces.isEmpty ())
    qch_file = m_help_engine->documentationFileName (namespaces.at (0));
  QFile collection_file (collection);
  copy_ok = collection_file.copy (m_collection);
#else
  // Qt5: use copyCollectionFile
  copy_ok = m_help_engine->copyCollectionFile (m_collection);
#endif

  if (copy_ok)
    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. Maybe the Qt SQlite\n"
                                "module is missing?\n"
                                "Only help text in the Command Window will\n"
                                "be available."));
#endif
      disconnect (m_help_engine, 0, 0, 0);

      delete m_help_engine;
      m_help_engine = nullptr;
    }
  else
    {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
      // Qt6: un- and re-register qch-file for fixing not having
      // used copyCollectionFile with automatic path update
      if (! namespaces.isEmpty ())
        m_help_engine->unregisterDocumentation (namespaces.at (0));
      m_help_engine->registerDocumentation (qch_file);
#endif
    }

  // 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"));

  gui_settings settings;

  forward_button->setIcon (settings.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 (settings.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->setContentsMargins (2, 2, 2, 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 function index"));
      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->setContentsMargins (2, 2, 2, 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, 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);

      restoreState (settings.byte_array_value (dc_splitter_state));
    }
}

documentation::~documentation ()
{
  // 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 ()
{
  // Home, Previous, Next
  gui_settings settings;

  m_action_go_home
    = add_action (settings.icon ("go-home"), tr ("Go home"), SLOT (home ()),
                  m_doc_browser, m_tool_bar);

  m_action_go_prev
    = add_action (settings.icon ("go-previous"), tr ("Go back"),
                  SLOT (backward ()), 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 (settings.icon ("go-next"), tr ("Go forward"),
                  SLOT (forward ()), 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 (settings.icon ("edit-find"), tr ("Find"),
                  SLOT (activate_find ()), this, m_tool_bar);

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

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

void documentation::global_search ()
{
  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
  QRegularExpression rx {"\"([^\"]*)\""};
  QRegularExpressionMatch match = rx.match (query_string);
  if (match.hasMatch ())
    m_internal_search = match.captured (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 ()
{
  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 (const auto& r : res)
                    {
#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 ()
{
  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.int_value (global_icon_size);
  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
  settings.set_shortcut (m_action_find, sc_edit_edit_find_replace);
  settings.shortcut (m_findnext_shortcut, sc_edit_edit_find_next);
  settings.shortcut (m_findprev_shortcut, sc_edit_edit_find_previous);
  settings.set_shortcut (m_action_zoom_in, sc_edit_view_zoom_in);
  settings.set_shortcut (m_action_zoom_out, sc_edit_view_zoom_out);
  settings.set_shortcut (m_action_zoom_original, sc_edit_view_zoom_normal);
  settings.set_shortcut (m_action_go_home, sc_doc_go_home);
  settings.set_shortcut (m_action_go_prev, sc_doc_go_back);
  settings.set_shortcut (m_action_go_next, sc_doc_go_next);
  settings.set_shortcut (m_action_bookmark, sc_doc_bookmark);

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

void documentation::save_settings ()
{
  gui_settings settings;

  settings.setValue (dc_splitter_state.settings_key (), saveState ());
  m_doc_browser->save_settings ();
  m_bookmarks->save_settings ();
}

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

void documentation::pasteClipboard () { }

void documentation::selectAll () { }

void documentation::load_index ()
{
  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 ()
{
  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 ()
{
  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 ()
{
  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 ()
{
  if (! m_help_engine)
    return;

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

void documentation::handle_cursor_position_change ()
{
  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 ()
{
  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 (QRegularExpression {"\\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 (QRegularExpression {"^index-"});
      anchor.remove (QRegularExpression {"^SEC_"});
      anchor.remove (QRegularExpression {"^XREF"});
      anchor.remove ("Concept-Index_cp_letter-");
      anchor.replace ("-", " ");

      // replace encoded special chars by their unencoded versions
      QRegularExpression rx {"_00([0-7][0-9a-f])"};
      QRegularExpressionMatch match = rx.match (anchor, 0);
      int pos = 0;
      while (match.hasMatch ())
        {
          anchor.replace ("_00" + match.captured (1),
                          QChar (match.captured (1).toInt (nullptr, 16)));
          pos += match.capturedLength ();
          match = rx.match (anchor, pos);
        }

      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 (QUrl (url));
  else
    QDesktopServices::openUrl (url);
}

void documentation_browser::notice_settings ()
{
  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.int_value (dc_browser_zoom_level);
      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 ()
{
  gui_settings settings;

  settings.setValue (dc_browser_zoom_level.settings_key (), m_zoom_level);

  settings.sync ();
}

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

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

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

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

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

OCTAVE_END_NAMESPACE(octave)