/* This file is (c) 2008-2012 Konstantin Isakov <ikm@goldendict.org>
 * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */

#ifndef NO_EPWING_SUPPORT
#include "dict/epwing_book.hh"
#endif

#include "mainwindow.hh"
#include <QWebEngineProfile>
#include "editdictionaries.hh"
#include "dict/loaddictionaries.hh"
#include "preferences.hh"
#include "about.hh"
#include "mruqmenu.hh"
#include "gestures.hh"
#include "dictheadwords.hh"
#include <QTextStream>
#include <QDir>
#include <QUrl>
#include <QMessageBox>
#include <QIcon>
#include <QList>
#include <QToolBar>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QProcess>
#include <QCryptographicHash>
#include <QFileDialog>
#include <QPrinter>
#include <QPageSetupDialog>
#include <QPrintPreviewDialog>
#include <QPrintDialog>
#include <QRunnable>
#include <QThreadPool>
#include <QSslConfiguration>

#include <set>
#include <map>
#include "gddebug.hh"

#include "dictinfo.hh"
#include "fsencoding.hh"
#include "historypanewidget.hh"
#include "utils.hh"
#include <qscreen.h>
#include "ui_authentication.h"
#include "resourceschemehandler.hh"

#include "keyboardstate.hh"
#include "base/globalregex.hh"

#ifdef Q_OS_MAC
#include "macmouseover.hh"
#endif

#ifdef Q_OS_WIN32
#include <windows.h>
#include "wstring.hh"
#include "wstring_qt.hh"
#endif

#include <QWebEngineSettings>
#include <QWebEngineProfile>
#include <QProxyStyle>

#ifdef HAVE_X11
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
#include <QGuiApplication>
#else
#include <QX11Info>
#endif
#include <X11/Xlib.h>
#include <fixx11h.h>
#endif

#define MIN_THREAD_COUNT 4

using std::set;
using std::map;
using std::pair;

#ifndef QT_NO_SSL

class InitSSLRunnable : public QRunnable
{
  void run() override
  {
    /// This action force SSL library initialisation which may continue a few seconds
    QSslConfiguration::setDefaultConfiguration( QSslConfiguration::defaultConfiguration() );
  }
};

#endif

void MainWindow::changeWebEngineViewFont()
{
  if( cfg.preferences.webFontFamily.isEmpty() )
  {
    webEngineProfile->settings()->resetFontFamily( QWebEngineSettings::StandardFont );
  }
  else
  {
    webEngineProfile->settings()->setFontFamily( QWebEngineSettings::StandardFont, cfg.preferences.webFontFamily );
  }
}

MainWindow::MainWindow( Config::Class & cfg_ ):
  trayIcon( 0 ),
  groupLabel( &searchPaneTitleBar ),
  foundInDictsLabel( &dictsPaneTitleBar ),
  escAction( this ),
  focusTranslateLineAction( this ),
  addTabAction( this ),
  closeCurrentTabAction( this ),
  closeAllTabAction( this ),
  closeRestTabAction( this ),
  switchToNextTabAction( this ),
  switchToPrevTabAction( this ),
  showDictBarNamesAction( tr( "Show Names in Dictionary &Bar" ), this ),
  useSmallIconsInToolbarsAction( tr( "Show Small Icons in &Toolbars" ), this ),
  toggleMenuBarAction( tr( "&Menubar" ), this ),
  focusHeadwordsDlgAction( this ),
  focusArticleViewAction( this ),
  addAllTabToFavoritesAction( this ),
  trayIconMenu( this ),
  addTab( this ),
  cfg( cfg_ ),
  history( History::Load(), cfg_.preferences.maxStringsInHistory, cfg_.maxHeadwordSize ),
  dictionaryBar( this, configEvents, cfg.editDictionaryCommandLine, cfg.preferences.maxDictionaryRefsInContextMenu ),
  articleMaker( dictionaries, groupInstances, cfg.preferences ),
  articleNetMgr( this, dictionaries, articleMaker,
                 cfg.preferences.disallowContentFromOtherSites, cfg.preferences.hideGoldenDictHeader ),
  dictNetMgr( this ),
  audioPlayerFactory( cfg.preferences ),
  wordFinder( this ),
  newReleaseCheckTimer( this ),
  latestReleaseReply( 0 ),
  wordListSelChanged( false )
, wasMaximized( false )
, blockUpdateWindowTitle( false )
, headwordsDlg( 0 )
, ftsIndexing( dictionaries )
, ftsDlg( 0 )
, starIcon( ":/icons/star.svg" )
, blueStarIcon( ":/icons/star_blue.svg" )
{
  if( QThreadPool::globalInstance()->maxThreadCount() < MIN_THREAD_COUNT )
    QThreadPool::globalInstance()->setMaxThreadCount( MIN_THREAD_COUNT );

#ifndef QT_NO_SSL
  QThreadPool::globalInstance()->start( new InitSSLRunnable );
#endif

  GlobalBroadcaster::instance()->setPreference( &cfg.preferences );

  webEngineProfile.reset( new QWebEngineProfile( "GoldenDictProfile" ) );
  GlobalBroadcaster::instance()->profile = webEngineProfile.get();
  localSchemeHandler                     = new LocalSchemeHandler( articleNetMgr, this );
  webEngineProfile->installUrlSchemeHandler( "gdlookup", localSchemeHandler );
  webEngineProfile->installUrlSchemeHandler( "bword", localSchemeHandler );
  webEngineProfile->installUrlSchemeHandler( "entry", localSchemeHandler );

  iframeSchemeHandler = new IframeSchemeHandler( this );
  webEngineProfile->installUrlSchemeHandler( "ifr", iframeSchemeHandler );

  QStringList localSchemes = { "gdau", "gico", "qrcx", "bres", "gdprg", "gdvideo", "gdpicture", "gdtts" };
  resourceSchemeHandler    = new ResourceSchemeHandler( articleNetMgr, this );
  for( const auto & localScheme : localSchemes ) {
    webEngineProfile->installUrlSchemeHandler( localScheme.toLatin1(), resourceSchemeHandler );
  }

  webEngineProfile->setUrlRequestInterceptor( new WebUrlRequestInterceptor( this ) );

  if( !cfg.preferences.hideGoldenDictHeader ) {
    webEngineProfile->setHttpUserAgent( webEngineProfile->httpUserAgent() + " GoldenDict/WebEngine" );
  }

  qRegisterMetaType< Config::InputPhrase >();

#ifndef NO_EPWING_SUPPORT
  Epwing::initialize();
#endif

  ui.setupUi( this );

  // Set own gesture recognizers
#ifndef Q_OS_MAC
  Gestures::registerRecognizers();
#endif
  // use our own, custom statusbar
  setStatusBar(0);
  mainStatusBar = new MainStatusBar( this );

  // Make the toolbar
  navToolbar = addToolBar( tr( "&Navigation" ) );
  navToolbar->setObjectName( "navToolbar" );

  navBack = navToolbar->addAction( QIcon( ":/icons/previous.svg" ), tr( "Back" ) );
  navToolbar->widgetForAction( navBack )->setObjectName( "backButton" );
  navForward = navToolbar->addAction( QIcon( ":/icons/next.svg" ), tr( "Forward" ) );
  navToolbar->widgetForAction( navForward )->setObjectName( "forwardButton" );

  QWidget * translateBoxWidget = new QWidget( this );
  QHBoxLayout * translateBoxLayout = new QHBoxLayout( translateBoxWidget );
  translateBoxWidget->setLayout( translateBoxLayout );
  translateBoxLayout->setContentsMargins( 0, 0, 0, 0 );
  translateBoxLayout->setSpacing( 0 );

  // translate box
  groupListInToolbar = new GroupComboBox( navToolbar );
  groupListInToolbar->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Preferred );
  groupListInToolbar->setSizeAdjustPolicy( QComboBox::AdjustToContents );
  translateBoxLayout->addWidget( groupListInToolbar );

  translateBox = new TranslateBox( navToolbar );
  translateBox->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
  translateBoxLayout->addWidget( translateBox );
  translateBoxToolBarAction = navToolbar->addWidget( translateBoxWidget );

  // scan popup
  navToolbar->addSeparator();

  enableScanningAction = navToolbar->addAction( QIcon( ":/icons/wizard.svg" ), tr( "Enable Scanning" ) );
  enableScanningAction->setCheckable( true );

  navToolbar->widgetForAction( enableScanningAction )->setObjectName( "scanPopupButton" );

  navToolbar->addSeparator();

  // sound
  navPronounce = navToolbar->addAction( QIcon( ":/icons/playsound_full.png" ), tr( "Pronounce Word (Alt+S)" ) );
  navPronounce->setShortcut( QKeySequence( "Alt+S" ) );
  navPronounce->setEnabled( false );
  navToolbar->widgetForAction( navPronounce )->setObjectName( "soundButton" );

  connect( navPronounce, SIGNAL( triggered() ),
           this, SLOT( pronounce() ) );

  // zooming
  // named separator (to be able to hide it via CSS)
  navToolbar->widgetForAction( navToolbar->addSeparator() )->setObjectName( "separatorBeforeZoom" );

  zoomIn = navToolbar->addAction( QIcon( ":/icons/icon32_zoomin.png" ), tr( "Zoom In" ) );
  zoomIn->setShortcuts( QList< QKeySequence >() <<
                       QKeySequence::ZoomIn <<
                       QKeySequence( "Ctrl+=" ) );
  navToolbar->widgetForAction( zoomIn )->setObjectName( "zoomInButton" );

  zoomOut = navToolbar->addAction( QIcon( ":/icons/icon32_zoomout.png" ), tr( "Zoom Out" ) );
  zoomOut->setShortcut( QKeySequence::ZoomOut );
  navToolbar->widgetForAction( zoomOut )->setObjectName( "zoomOutButton" );

  zoomBase = navToolbar->addAction( QIcon( ":/icons/icon32_zoombase.png" ), tr( "Normal Size" ) );
  zoomBase->setShortcut( QKeySequence( "Ctrl+0" ) );
  navToolbar->widgetForAction( zoomBase )->setObjectName( "zoomBaseButton" );

  // named separator (to be able to hide it via CSS)
  navToolbar->widgetForAction( navToolbar->addSeparator() )->setObjectName( "separatorBeforeSave" );

  navToolbar->addAction( ui.saveArticle );
  navToolbar->widgetForAction( ui.saveArticle )->setObjectName( "saveArticleButton" );

  navToolbar->addAction( ui.print );
  navToolbar->widgetForAction( ui.print )->setObjectName( "printButton" );

  navToolbar->widgetForAction( navToolbar->addSeparator() )->setObjectName( "separatorBeforeAddToFavorites" );

  addToFavorites = navToolbar->addAction( starIcon, tr( "Add current tab to Favorites" ) );
  navToolbar->widgetForAction( addToFavorites )->setObjectName( "addToFavoritesButton" );

  connect( addToFavorites, &QAction::triggered, this, &MainWindow::handleAddToFavoritesButton );
  connect( ui.actionAddToFavorites, &QAction::triggered, this, &MainWindow::addCurrentTabToFavorites );

  beforeOptionsSeparator = navToolbar->addSeparator();
  navToolbar->widgetForAction( beforeOptionsSeparator )->setObjectName( "beforeOptionsSeparator" );
  beforeOptionsSeparator->setVisible( cfg.preferences.hideMenubar);

  QMenu * buttonMenu = new QMenu( this );
  buttonMenu->addAction( ui.dictionaries );
  buttonMenu->addAction( ui.preferences );
  buttonMenu->addSeparator();
  buttonMenu->addMenu( ui.menuFavorites );
  buttonMenu->addMenu( ui.menuHistory );
  buttonMenu->addSeparator();
  buttonMenu->addMenu( ui.menuFile );
  buttonMenu->addMenu( ui.menuView );
  buttonMenu->addMenu( ui.menuSearch );
  buttonMenu->addMenu( ui.menu_Help );

  ui.fullTextSearchAction->setEnabled( cfg.preferences.fts.enabled );

  menuButton = new QToolButton( navToolbar );
  menuButton->setPopupMode( QToolButton::InstantPopup );
  menuButton->setMenu( buttonMenu );
  menuButton->setIcon( QIcon (":/icons/menu_button.svg") );
  menuButton->addAction( ui.menuOptions );
  menuButton->setToolTip( tr( "Menu Button" ) );
  menuButton->setObjectName( "menuButton" );
  menuButton->setFocusPolicy( Qt::NoFocus );

  menuButtonAction = navToolbar->addWidget(menuButton);
  menuButtonAction->setVisible( cfg.preferences.hideMenubar );

  // Make the search pane's titlebar
  groupLabel.setText( tr( "Look up in:" ) );
  groupListInDock = new GroupComboBox( &searchPaneTitleBar );

  searchPaneTitleBarLayout.setContentsMargins( 8, 5, 8, 4 );
  searchPaneTitleBarLayout.addWidget( &groupLabel );
  searchPaneTitleBarLayout.addWidget( groupListInDock );
  searchPaneTitleBarLayout.addStretch();

  searchPaneTitleBar.setLayout( &searchPaneTitleBarLayout );

  ui.searchPane->setTitleBarWidget( &searchPaneTitleBar );
  connect( ui.searchPane->toggleViewAction(), &QAction::triggered, this, &MainWindow::updateSearchPaneAndBar );

  if ( cfg.preferences.searchInDock )
  {
    groupList = groupListInDock;
    translateLine = ui.translateLine;
    wordList = ui.wordList;
  }
  else
  {
    groupList = groupListInToolbar;
    translateLine = translateBox->translateLine();
    wordList = translateBox->wordList();
  }
  wordList->attachFinder( &wordFinder );

  // for the old UI:
  ui.wordList->setTranslateLine( ui.translateLine );

  groupList->setFocusPolicy(Qt::ClickFocus);
  wordList->setFocusPolicy(Qt::ClickFocus);

  wordListDefaultFont = wordList->font();
  translateLineDefaultFont = translateLine->font();
  groupListDefaultFont = groupList->font();

  // Make the dictionaries pane's titlebar
  foundInDictsLabel.setText( tr( "Found in Dictionaries:" ) );
  dictsPaneTitleBarLayout.addWidget( &foundInDictsLabel );
  dictsPaneTitleBarLayout.setContentsMargins(5, 5, 5, 5);
  dictsPaneTitleBar.setLayout( &dictsPaneTitleBarLayout );
  dictsPaneTitleBar.setObjectName("dictsPaneTitleBar");
  ui.dictsPane->setTitleBarWidget( &dictsPaneTitleBar );
  ui.dictsList->setContextMenuPolicy( Qt::CustomContextMenu );

  connect( ui.dictsPane, SIGNAL( visibilityChanged( bool ) ),
           this, SLOT( dictsPaneVisibilityChanged ( bool ) ) );

  connect( ui.dictsList, &QListWidget::itemClicked, this, &MainWindow::foundDictsPaneClicked );

  connect( ui.dictsList, &QWidget::customContextMenuRequested, this, &MainWindow::foundDictsContextMenuRequested );

  connect( zoomIn, &QAction::triggered, this, &MainWindow::zoomin );
  connect( zoomOut, &QAction::triggered, this, &MainWindow::zoomout );
  connect( zoomBase, &QAction::triggered, this, &MainWindow::unzoom );

  ui.menuZoom->addAction( zoomIn );
  ui.menuZoom->addAction( zoomOut );
  ui.menuZoom->addAction( zoomBase );

  ui.menuZoom->addSeparator();

  wordsZoomIn = ui.menuZoom->addAction( QIcon( ":/icons/icon32_zoomin.png" ), tr( "Words Zoom In" ) );
  wordsZoomIn->setShortcuts( QList< QKeySequence >() <<
                            QKeySequence( "Alt++" ) <<
                            QKeySequence( "Alt+=" ) );
  wordsZoomOut = ui.menuZoom->addAction( QIcon( ":/icons/icon32_zoomout.png" ), tr( "Words Zoom Out" ) );
  wordsZoomOut->setShortcut( QKeySequence( "Alt+-" ) );
  wordsZoomBase = ui.menuZoom->addAction( QIcon( ":/icons/icon32_zoombase.png" ), tr( "Words Normal Size" ) );
  wordsZoomBase->setShortcut( QKeySequence( "Alt+0" ) );

  connect( wordsZoomIn, &QAction::triggered, this, &MainWindow::doWordsZoomIn );
  connect( wordsZoomOut, &QAction::triggered, this, &MainWindow::doWordsZoomOut );
  connect( wordsZoomBase, &QAction::triggered, this, &MainWindow::doWordsZoomBase );

  // tray icon
  connect( trayIconMenu.addAction( tr( "Show &Main Window" ) ),
    &QAction::triggered,
    this,
    &MainWindow::showMainWindow );
  trayIconMenu.addAction( enableScanningAction );

  trayIconMenu.addSeparator();
  connect( trayIconMenu.addAction( tr( "&Quit" ) ), &QAction::triggered, this, &MainWindow::quitApp );

  addGlobalAction( &escAction, SLOT( handleEsc() ) );
  escAction.setShortcut( QKeySequence( "Esc" ) );

  addGlobalAction( &focusTranslateLineAction, SLOT( focusTranslateLine() ) );
  focusTranslateLineAction.setShortcuts( QList< QKeySequence >() <<
                                         QKeySequence( "Alt+D" ) <<
                                         QKeySequence( "Ctrl+L" ) );

  addGlobalAction( &focusHeadwordsDlgAction, SLOT( focusHeadwordsDialog() ) );
  focusHeadwordsDlgAction.setShortcut( QKeySequence( "Ctrl+D" ) );

  addGlobalAction( &focusArticleViewAction, SLOT( focusArticleView() ) );
  focusArticleViewAction.setShortcut( QKeySequence( "Ctrl+N" ) );

  addGlobalAction( ui.fullTextSearchAction, SLOT( showFullTextSearchDialog() ) );

  addTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  addTabAction.setShortcut( QKeySequence( "Ctrl+T" ) );

  // Tab management
  tabListMenu = new MRUQMenu(tr("Opened tabs"), ui.tabWidget);

  connect( tabListMenu, &MRUQMenu::requestTabChange, ui.tabWidget, &MainTabWidget::setCurrentIndex );

  connect( &addTabAction, &QAction::triggered, this, &MainWindow::addNewTab );

  addAction( &addTabAction );

  closeCurrentTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  closeCurrentTabAction.setShortcut( QKeySequence( "Ctrl+W" ) );
  closeCurrentTabAction.setText( tr("Close current tab") );

  connect( &closeCurrentTabAction, &QAction::triggered, this, &MainWindow::closeCurrentTab );

  addAction( &closeCurrentTabAction );

  closeAllTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  closeAllTabAction.setShortcut( QKeySequence( "Ctrl+Shift+W" ) );
  closeAllTabAction.setText( tr("Close all tabs") );

  connect( &closeAllTabAction, &QAction::triggered, this, &MainWindow::closeAllTabs );

  addAction( &closeAllTabAction );

  closeRestTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  closeRestTabAction.setText( tr("Close all tabs except current") );

  connect( &closeRestTabAction, &QAction::triggered, this, &MainWindow::closeRestTabs );

  addAction( &closeRestTabAction );

  switchToNextTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  switchToNextTabAction.setShortcut( QKeySequence( "Ctrl+PgDown" ) );

  connect( &switchToNextTabAction, &QAction::triggered, this, &MainWindow::switchToNextTab );

  addAction( &switchToNextTabAction );

  switchToPrevTabAction.setShortcutContext( Qt::WidgetWithChildrenShortcut );
  switchToPrevTabAction.setShortcut( QKeySequence( "Ctrl+PgUp" ) );

  connect( &switchToPrevTabAction, &QAction::triggered, this, &MainWindow::switchToPrevTab );

  addAction( &switchToPrevTabAction );

  addAllTabToFavoritesAction.setText( tr( "Add all tabs to Favorites" ) );

  connect( &addAllTabToFavoritesAction, &QAction::triggered, this, &MainWindow::addAllTabsToFavorites );

  tabMenu = new QMenu(this);
  tabMenu->addAction( &closeCurrentTabAction );
  tabMenu->addAction( &closeRestTabAction );
  tabMenu->addSeparator();
  tabMenu->addAction( &closeAllTabAction );
  tabMenu->addSeparator();
  tabMenu->addAction( addToFavorites );
  tabMenu->addAction( &addAllTabToFavoritesAction );

  // Dictionary bar names

  showDictBarNamesAction.setCheckable( true );
  showDictBarNamesAction.setChecked( cfg.showingDictBarNames );

  connect( &showDictBarNamesAction, &QAction::triggered, this, &MainWindow::showDictBarNamesTriggered );

  // Use small icons in toolbars

  useSmallIconsInToolbarsAction.setCheckable( true );
  useSmallIconsInToolbarsAction.setChecked( cfg.usingSmallIconsInToolbars );

  connect( &useSmallIconsInToolbarsAction, &QAction::triggered, this, &MainWindow::useSmallIconsInToolbarsTriggered );

  // Toggle Menubar
  toggleMenuBarAction.setCheckable( true );
  toggleMenuBarAction.setChecked( !cfg.preferences.hideMenubar );
  toggleMenuBarAction.setShortcut( QKeySequence( "Ctrl+M" ) );

  connect( &toggleMenuBarAction, &QAction::triggered, this, &MainWindow::toggleMenuBarTriggered );

  // Populate 'View' menu

  ui.menuView->addAction( &toggleMenuBarAction );
  ui.menuView->addSeparator();
  ui.menuView->addAction( ui.searchPane->toggleViewAction() );
  ui.searchPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+S" ) );
  ui.menuView->addAction( ui.dictsPane->toggleViewAction() );
  ui.dictsPane->toggleViewAction()->setShortcut( QKeySequence( "Ctrl+R" ) );
  ui.menuView->addAction( ui.favoritesPane->toggleViewAction() );
  ui.menuView->addAction( ui.historyPane->toggleViewAction() );

  ui.menuView->addSeparator();
  ui.menuView->addAction( dictionaryBar.toggleViewAction() );
  ui.menuView->addAction( navToolbar->toggleViewAction() );
  ui.menuView->addSeparator();
  ui.menuView->addAction( &showDictBarNamesAction );
  ui.menuView->addAction( &useSmallIconsInToolbarsAction );
  ui.menuView->addSeparator();
  ui.alwaysOnTop->setChecked( cfg.preferences.alwaysOnTop );
  ui.menuView->addAction( ui.alwaysOnTop );

  // Dictionary bar

  Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if( cfg.lastMainGroupId == Instances::Group::AllGroupId )
  {
    if( igrp )
      igrp->checkMutedDictionaries( &cfg.mutedDictionaries );
    dictionaryBar.setMutedDictionaries( &cfg.mutedDictionaries );
  }
  else
  {
    Config::Group * grp = cfg.getGroup( cfg.lastMainGroupId );
    if( igrp && grp )
      igrp->checkMutedDictionaries( &grp->mutedDictionaries );
    dictionaryBar.setMutedDictionaries( grp ? &grp->mutedDictionaries : 0 );
  }
  GlobalBroadcaster::instance()->currentGroupId = cfg.lastMainGroupId;

  showDictBarNamesTriggered(); // Make update its state according to initial
                               // setting

  connect( this, SIGNAL( clickOnDictPane( QString const & ) ),
           &dictionaryBar, SLOT( dictsPaneClicked( QString const & ) ) );

  addToolBar( &dictionaryBar );

  connect( dictionaryBar.toggleViewAction(), &QAction::triggered, this, &MainWindow::dictionaryBarToggled );
  // This one will be disconnected once the slot is activated. It exists
  // only to handle the initial appearance of the dictionary bar.
  connect( dictionaryBar.toggleViewAction(), &QAction::toggled, this, &MainWindow::dictionaryBarToggled );

  connect( &dictionaryBar, &DictionaryBar::editGroupRequested, this, &MainWindow::editCurrentGroup );

  connect( &dictionaryBar, &DictionaryBar::showDictionaryInfo, this, &MainWindow::showDictionaryInfo );

  connect( &dictionaryBar,&DictionaryBar::showDictionaryHeadwords,this,&MainWindow::showDictionaryHeadwords);

  connect( &dictionaryBar, &DictionaryBar::openDictionaryFolder, this, &MainWindow::openDictionaryFolder );

  // Favorites

  ui.favoritesPaneWidget->setUp( &cfg, ui.menuFavorites );
  ui.favoritesPaneWidget->setSaveInterval( cfg.preferences.favoritesStoreInterval );

  connect( ui.favoritesPane, &QDockWidget::visibilityChanged, this, &MainWindow::updateFavoritesMenu );
  connect(ui.showHideFavorites,&QAction::triggered,this,&MainWindow::toggle_favoritesPane);

  connect( ui.favoritesPaneWidget,
    &FavoritesPaneWidget::favoritesItemRequested,
    this,
    &MainWindow::headwordFromFavorites );

  // History
  ui.historyPaneWidget->setUp( &cfg, &history, ui.menuHistory );
  history.enableAdd( cfg.preferences.storeHistory );

  connect( ui.historyPaneWidget, &HistoryPaneWidget::historyItemRequested, this, &MainWindow::showHistoryItem );

  connect( ui.historyPane, &QDockWidget::visibilityChanged, this, &MainWindow::updateHistoryMenu );
  connect( ui.showHideHistory, &QAction::triggered, this, &MainWindow::toggle_historyPane );


#if !defined( HAVE_X11 )
  // Show tray icon early so the user would be happy. It won't be functional
  // though until the program inits fully.
  // Do not create dummy tray icon in X. Cause QT5 failed to upgrade systemtray context menu.
  // And as result menu for some DEs apppear to be empty, for example in MATE DE.

  if ( cfg.preferences.enableTrayIcon )
  {
    trayIcon = new QSystemTrayIcon( QIcon::fromTheme("goldendict-tray", QIcon( ":/icons/programicon_old.png" )), this );
    trayIcon->setToolTip( tr( "Loading..." ) );
    trayIcon->show();
  }
#endif

  connect( navBack, &QAction::triggered, this, &MainWindow::backClicked );
  connect( navForward, &QAction::triggered, this, &MainWindow::forwardClicked );

  addTab.setAutoRaise( true );
  addTab.setToolTip( tr( "New Tab"  ) );
  addTab.setFocusPolicy( Qt::NoFocus );
  addTab.setIcon( QIcon( ":/icons/addtab.svg" ) );

  ui.tabWidget->setHideSingleTab(cfg.preferences.hideSingleTab);
  ui.tabWidget->clear();

  ui.tabWidget->setCornerWidget( &addTab, Qt::TopLeftCorner );
  //ui.tabWidget->setCornerWidget( &closeTab, Qt::TopRightCorner );

  ui.tabWidget->setMovable( true );

#ifndef Q_OS_WIN32
  ui.tabWidget->setDocumentMode( true );
#endif

  ui.tabWidget->setContextMenuPolicy( Qt::CustomContextMenu );

  connect( &addTab, &QAbstractButton::clicked, this, &MainWindow::addNewTab );

  connect( ui.tabWidget, &MainTabWidget::doubleClicked, this, &MainWindow::addNewTab );

  connect( ui.tabWidget, &QTabWidget::tabCloseRequested, this, &MainWindow::tabCloseRequested );

  connect( ui.tabWidget, &QTabWidget::currentChanged, this, &MainWindow::tabSwitched );

  connect( ui.tabWidget, &QWidget::customContextMenuRequested, this, &MainWindow::tabMenuRequested );

  ui.tabWidget->setTabsClosable( true );

  connect( ui.quit, &QAction::triggered, this, &MainWindow::quitApp );

  connect( ui.dictionaries, &QAction::triggered, this, &MainWindow::editDictionaries );

  connect( ui.preferences, &QAction::triggered, this, &MainWindow::editPreferences );

  connect( ui.visitHomepage, &QAction::triggered, this, &MainWindow::visitHomepage );
  connect( ui.visitForum, &QAction::triggered, this, &MainWindow::visitForum );
  connect( ui.openConfigFolder, &QAction::triggered, this, &MainWindow::openConfigFolder );
  connect( ui.about, &QAction::triggered, this, &MainWindow::showAbout );
  connect( ui.showReference, &QAction::triggered, []() {
    Utils::Help::openHelpWebpage();
  } );

  connect( groupListInDock, &GroupComboBox::currentIndexChanged,
           this, &MainWindow::currentGroupChanged );

  connect( groupListInToolbar, &GroupComboBox::currentIndexChanged,
           this, &MainWindow::currentGroupChanged );

  connect( ui.translateLine, &QLineEdit::textChanged, this, &MainWindow::translateInputChanged );

  connect( translateBox->translateLine(), &QLineEdit::textChanged, this, &MainWindow::translateInputChanged );

  connect( ui.translateLine, SIGNAL( returnPressed() ), this, SLOT( translateInputFinished() ) );

  connect( translateBox->translateLine(), SIGNAL( returnPressed() ),
           this, SLOT( translateInputFinished() ) );

  connect( ui.wordList, &QListWidget::itemSelectionChanged, this, &MainWindow::wordListSelectionChanged );

  connect( translateBox->wordList(), SIGNAL( itemDoubleClicked ( QListWidgetItem * ) ),
           this, SLOT( wordListItemActivated( QListWidgetItem * ) ) );

  connect( ui.wordList, &QListWidget::itemClicked, this, &MainWindow::wordListItemActivated );

  connect( ui.wordList, &WordList::statusBarMessage, this, &MainWindow::showStatusBarMessage );

  connect( translateBox->wordList(), &WordList::statusBarMessage, this, &MainWindow::showStatusBarMessage );

  connect( ui.dictsList, &QListWidget::itemSelectionChanged, this, &MainWindow::dictsListSelectionChanged );

  connect( ui.dictsList, &QListWidget::itemDoubleClicked, this, &MainWindow::dictsListItemActivated );

  connect( &configEvents, &Config::Events::mutedDictionariesChanged, this, &MainWindow::mutedDictionariesChanged );

  this->installEventFilter( this );

  ui.translateLine->installEventFilter( this );
  translateBox->translateLine()->installEventFilter( this );

  ui.wordList->installEventFilter( this );
  translateBox->wordList()->installEventFilter( this );

  ui.wordList->viewport()->installEventFilter( this );
  translateBox->wordList()->viewport()->installEventFilter( this );

  ui.dictsList->installEventFilter( this );
  ui.dictsList->viewport()->installEventFilter( this );
  //tabWidget doesn't propagate Ctrl+Tab to the parent widget unless event filter is installed
  ui.tabWidget->installEventFilter( this );

  ui.historyList->installEventFilter( this );

  ui.favoritesTree->installEventFilter( this );

  groupListInDock->installEventFilter( this );
  groupListInToolbar->installEventFilter( this );

  connect( &ftsIndexing, &FTS::FtsIndexing::newIndexingName, this, &MainWindow::showFTSIndexingName );

  applyProxySettings();

  //set  webengineview font
  changeWebEngineViewFont();

  connect( &dictNetMgr, &QNetworkAccessManager::proxyAuthenticationRequired, this, &MainWindow::proxyAuthentication );

  connect( &articleNetMgr,
    &QNetworkAccessManager::proxyAuthenticationRequired,
    this,
    &MainWindow::proxyAuthentication );

  setupNetworkCache( cfg.preferences.maxNetworkCacheSize );

  makeDictionaries();

  // After we have dictionaries and groups, we can populate history
//  historyChanged();

  setWindowTitle( "GoldenDict-ng" );

  blockUpdateWindowTitle = true;
  addNewTab();

  // Create tab list menu
  createTabList();

  if( cfg.mainWindowGeometry.size() )
    restoreGeometry( cfg.mainWindowGeometry );
  if( cfg.mainWindowState.size() )
    restoreState( cfg.mainWindowState, 1 );

  // Show the initial welcome text

  {
    ArticleView *view = getCurrentArticleView();

    history.enableAdd( false );

    blockUpdateWindowTitle = true;

    view->showDefinition( tr( "Welcome!" ), Instances::Group::HelpGroupId );

    history.enableAdd( cfg.preferences.storeHistory );
  }

  translateLine->setFocus();

  applyQtStyleSheet( cfg.preferences.addonStyle, cfg.preferences.displayStyle, cfg.preferences.darkMode );

  // Scanpopup related
  scanPopup = new ScanPopup(nullptr, cfg, articleNetMgr, audioPlayerFactory.player(),
    dictionaries, groupInstances, history );

  scanPopup->setStyleSheet(styleSheet());

  connect( scanPopup,SIGNAL(editGroupRequested(unsigned)),
    this, SLOT(editDictionaries(unsigned)),
    Qt::QueuedConnection);

  connect( scanPopup, &ScanPopup::sendPhraseToMainWindow,
    this,&MainWindow::phraseReceived,
    Qt::QueuedConnection);

  connect( scanPopup, &ScanPopup::inspectSignal,this,&MainWindow::inspectElement );
  connect( scanPopup, &ScanPopup::forceAddWordToHistory, this, &MainWindow::forceAddWordToHistory );
  connect( scanPopup, &ScanPopup::showDictionaryInfo, this, &MainWindow::showDictionaryInfo );
  connect ( scanPopup, &ScanPopup::openDictionaryFolder, this, &MainWindow::openDictionaryFolder );
  connect( scanPopup, &ScanPopup::sendWordToHistory, this, &MainWindow::addWordToHistory );
  connect( this, &MainWindow::setPopupGroupByName, scanPopup, &ScanPopup::setGroupByName );
  connect( scanPopup, &ScanPopup::sendWordToFavorites, this, &MainWindow::addWordToFavorites );
  connect( scanPopup, &ScanPopup::isWordPresentedInFavorites, this, &MainWindow::isWordPresentedInFavorites );

#ifdef Q_OS_MAC
  macClipboard = new gd_clipboard(this);
  connect(macClipboard, &gd_clipboard::changed, this, &MainWindow::clipboardChange );
#endif

  connect( enableScanningAction, &QAction::toggled, this, [ = ]( bool on ) {
    if( on )
    {
      enableScanningAction->setIcon( QIcon( ":/icons/wizard-selected.svg" ) );
    }
    else
    {
      enableScanningAction->setIcon( QIcon( ":/icons/wizard.svg" ) );
    }

#ifdef Q_OS_MAC
    if( !MacMouseOver::isAXAPIEnabled() )
      mainStatusBar->showMessage( tr( "Accessibility API is not enabled" ), 10000, QPixmap( ":/icons/error.svg" ) );

    if( on )
    {
      macClipboard->start();
    }
    else
    {
      macClipboard->stop();
    }
#else
      if( on ) {
        connect( QApplication::clipboard(), &QClipboard::changed, this, &MainWindow::clipboardChange );
      }
      else
      {
        disconnect(QApplication::clipboard(), &QClipboard::changed, this, &MainWindow::clipboardChange);
      }
#endif

    installHotKeys();
    updateTrayIcon();
  } );

  if( cfg.preferences.startWithScanPopupOn )
  {
    enableScanningAction->trigger();
  }

  updateSearchPaneAndBar( cfg.preferences.searchInDock );
  ui.searchPane->setVisible( cfg.preferences.searchInDock );

  if ( trayIcon )
  {
    // Upgrade existing dummy tray icon into a full-functional one

    trayIcon->setContextMenu( &trayIconMenu );
    trayIcon->show();

    connect( trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated );
  }

  updateTrayIcon();

  // Update zoomers
  adjustCurrentZoomFactor();
  scaleArticlesByCurrentZoomFactor();
  applyWordsZoomLevel();

  // Update autostart info
  setAutostart(cfg.preferences.autoStart);

  // Initialize global hotkeys
  installHotKeys();

  if ( cfg.preferences.alwaysOnTop )
  {
    on_alwaysOnTop_triggered( true );
  }

  // Only show window initially if it wasn't configured differently
  if ( !cfg.preferences.enableTrayIcon || !cfg.preferences.startToTray )
  {
    show();
    focusTranslateLine();
  }

  connect( &newReleaseCheckTimer, &QTimer::timeout, this, &MainWindow::checkForNewRelease );

  if ( cfg.preferences.hideMenubar )
  {
    toggleMenuBarTriggered( false );
  }

  prepareNewReleaseChecks();

  // makeDictionaries() didn't do deferred init - we do it here, at the end.
  doDeferredInit( dictionaries );

  updateStatusLine();

#ifdef Q_OS_MAC
  if( cfg.preferences.startWithScanPopupOn && !MacMouseOver::isAXAPIEnabled() )
      mainStatusBar->showMessage( tr( "Accessibility API is not enabled" ), 10000,
                                      QPixmap( ":/icons/error.svg" ) );
#endif

  wasMaximized = isMaximized();

  history.setSaveInterval( cfg.preferences.historyStoreInterval );

  ui.centralWidget->grabGesture( Gestures::GDPinchGestureType );
  ui.centralWidget->grabGesture( Gestures::GDSwipeGestureType );

  if( layoutDirection() == Qt::RightToLeft )
  {
    // Adjust button icons for Right-To-Left layout
    navBack->setIcon( QIcon( ":/icons/next.svg" ) );
    navForward->setIcon( QIcon( ":/icons/previous.svg" ) );
  }

  inspector.reset( new ArticleInspector( this ));

#ifdef Q_OS_WIN
  // Regiser and update URL Scheme for windows
  // https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)

  // Windows will automatically map registry key to Computer\HKEY_CLASSES_ROOT\ */
  QSettings urlRegistry(R"(HKEY_CURRENT_USER\Software\Classes)", QSettings::NativeFormat);

  urlRegistry.beginGroup("goldendict");
  urlRegistry.setValue("Default", "URL: goldendict Protocol");
  urlRegistry.setValue("URL Protocol", "");
  urlRegistry.setValue("shell/open/command/Default",
    QString("\"%1\"").arg( QDir::toNativeSeparators(QApplication::applicationFilePath())) + " \"%1\"");
  urlRegistry.endGroup();
#endif

  useSmallIconsInToolbarsTriggered();
}

void MainWindow::clipboardChange( QClipboard::Mode m)
{
  if( !scanPopup )
  {
    return;
  }

#if defined(HAVE_X11)
      if(m == QClipboard::Clipboard){
        if(!cfg.preferences.trackClipboardScan) return;
        scanPopup->translateWordFromClipboard();
        return;
      }

      if(m == QClipboard::Selection){

        // Multiple ways to stopping a word from showing up when selecting

        // Explicitly disabled on preferences
        if(!cfg.preferences.trackSelectionScan) return;

        // Explicitly disabled on preferences to ignore gd's own selection

        if( cfg.preferences.ignoreOwnClipboardChanges
          && QApplication::clipboard()->ownsSelection() ){
          return ;
        }

        // Keyboard Modifier
        if(cfg.preferences.enableScanPopupModifiers &&
          !KeyboardState::checkModifiersPressed(cfg.preferences.scanPopupModifiers)){
          return;
        }

        // Show a Flag instead of translate directly.
        // And hand over the control of showing the popup to scanFlag
        if ( cfg.preferences.showScanFlag ) {
          scanPopup->showScanFlag();
          return;
        }

        // Use delay show to prevent multiple popups while selection in progress
        scanPopup->selectionDelayTimer.start();
      }
#elif defined(Q_OS_MAC)
      scanPopup->translateWord(macClipboard->text());
#else
      scanPopup->translateWordFromClipboard();
#endif

}

void MainWindow::ctrlTabPressed()
{
    emit fillWindowsMenu();
    tabListButton->click();
}

void MainWindow::updateSearchPaneAndBar( bool searchInDock )
{
  QString text = translateLine->text();

  if( ftsDlg )
    removeGroupComboBoxActionsFromDialog( ftsDlg, groupList );
  if( headwordsDlg )
    removeGroupComboBoxActionsFromDialog( headwordsDlg, groupList );

  if ( searchInDock )
  {
    cfg.preferences.searchInDock = true;

    navToolbar->setAllowedAreas( Qt::AllToolBarAreas );

    groupList = groupListInDock;
    translateLine = ui.translateLine;
    wordList = ui.wordList;

    translateBoxToolBarAction->setVisible( false );
  }
  else
  {
    cfg.preferences.searchInDock = false;

    // handle the main toolbar, it must not be on the side, since it should
    // contain the group widget and the translate line. Valid locations: Top and Bottom.
    navToolbar->setAllowedAreas( Qt::BottomToolBarArea | Qt::TopToolBarArea );
    if ( toolBarArea( navToolbar ) & ( Qt::LeftToolBarArea | Qt::RightToolBarArea ) )
    {
      if ( toolBarArea( &dictionaryBar ) == Qt::TopToolBarArea )
      {
        insertToolBar( &dictionaryBar, navToolbar );
      }
      else
      {
        addToolBar( Qt::TopToolBarArea, navToolbar );
      }
    }

    groupList = groupListInToolbar;
    translateLine = translateBox->translateLine();
    wordList = translateBox->wordList();

    translateBoxToolBarAction->setVisible( true );
  }

  if( ftsDlg )
    addGroupComboBoxActionsToDialog( ftsDlg, groupList );
  if( headwordsDlg )
    addGroupComboBoxActionsToDialog( headwordsDlg, groupList );

  translateLine->setToolTip( tr( "String to search in dictionaries. The wildcards '*', '?' and sets of symbols '[...]' are allowed.\nTo find '*', '?', '[', ']' symbols use '\\*', '\\?', '\\[', '\\]' respectively" ) );

  // reset the flag when switching UI modes
  wordListSelChanged = false;

  wordList->attachFinder( &wordFinder );

  updateGroupList();
  applyWordsZoomLevel();

  setTranslateBoxTextAndKeepSuffix( text, WildcardsAreAlreadyEscaped, DisablePopup );
  focusTranslateLine();
}

void MainWindow::mousePressEvent( QMouseEvent *event)
{

  if (handleBackForwardMouseButtons( event ) )
  {
    return;
  }

  if (event->button() != Qt::MiddleButton)
    return QMainWindow::mousePressEvent(event);

  // middle clicked
  QString subtype = "plain";

  QString str = QApplication::clipboard()->text( subtype, QClipboard::Selection );
  setTranslateBoxTextAndClearSuffix( str, EscapeWildcards, NoPopupChange );

  QKeyEvent ev( QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier );
  QApplication::sendEvent( translateLine, &ev );
}

MainWindow::~MainWindow()
{
  closeHeadwordsDialog();

  ftsIndexing.stopIndexing();

  ui.centralWidget->ungrabGesture( Gestures::GDPinchGestureType );
  ui.centralWidget->ungrabGesture( Gestures::GDSwipeGestureType );
//  Gestures::unregisterRecognizers();

  // Close all tabs -- they should be destroyed before network managers
  // do.
  while( ui.tabWidget->count() )
  {
    QWidget * w = ui.tabWidget->widget( 0 );

    ui.tabWidget->removeTab( 0 );

    delete w;
  }

#ifndef NO_EPWING_SUPPORT
  Epwing::finalize();
#endif
}

void MainWindow::addGlobalAction( QAction * action, const char * slot )
{
  action->setShortcutContext( Qt::WidgetWithChildrenShortcut );
  connect( action, SIGNAL( triggered() ), this, slot );

  ui.centralWidget->addAction( action );
  ui.dictsPane->addAction( action );
  ui.searchPaneWidget->addAction( action );
  ui.favoritesPane->addAction( action );
  ui.historyPane->addAction( action );
  groupList->addAction( action );
  translateBox->addAction( action );
}

void MainWindow::addGlobalActionsToDialog( QDialog * dialog )
{
  dialog->addAction( &focusTranslateLineAction );
  dialog->addAction( &focusHeadwordsDlgAction );
  dialog->addAction( &focusArticleViewAction );
  dialog->addAction( ui.fullTextSearchAction );
}

void MainWindow::addGroupComboBoxActionsToDialog( QDialog * dialog, GroupComboBox * pGroupComboBox )
{
  dialog->addActions( pGroupComboBox->getExternActions() );
}

void MainWindow::removeGroupComboBoxActionsFromDialog( QDialog * dialog, GroupComboBox * pGroupComboBox )
{
  QList< QAction * > actions = pGroupComboBox->getExternActions();
  for( QList< QAction * >::iterator it = actions.begin(); it != actions.end(); ++it )
    dialog->removeAction( *it );
}

void MainWindow::commitData( QSessionManager & )
{
  commitData();
}

void MainWindow::commitData()
{
  if( cfg.preferences.clearNetworkCacheOnExit )
    if( QAbstractNetworkCache * cache = articleNetMgr.cache() )
      cache->clear();

  try
  {
    // Save MainWindow state and geometry
    cfg.mainWindowState = saveState( 1 );
    cfg.mainWindowGeometry = saveGeometry();

    // Save popup window state and geometry
    if( scanPopup )
      scanPopup->saveConfigData();

    // Save any changes in last chosen groups etc
    try
    {
      Config::save( cfg );
    }
    catch( std::exception & e )
    {
      gdWarning( "Configuration saving failed, error: %s\n", e.what() );
    }

    // Save history
    history.save();

    // Save favorites
    ui.favoritesPaneWidget->saveData();
  }
  catch( std::exception & e )
  {
    gdWarning( "Commit data failed, error: %s\n", e.what() );
  }
}

QPrinter & MainWindow::getPrinter()
{
  if ( printer.get() )
    return *printer;

  printer =  std::make_shared<QPrinter>( QPrinter::HighResolution );

  return *printer;
}

void MainWindow::applyQtStyleSheet( QString const & addonStyle,QString const & displayStyle, bool const & darkMode )
{
  #ifdef Q_OS_WIN32
  if( darkMode )
  {
    //https://forum.qt.io/topic/101391/windows-10-dark-theme
    #ifdef Q_OS_WIN32
    qApp->setStyle( QStyleFactory::create( "Fusion" ) );
    #endif
    QPalette darkPalette;
    QColor darkColor     = QColor( 45, 45, 45 );
    QColor disabledColor = QColor( 127, 127, 127 );
    darkPalette.setColor( QPalette::Window, darkColor );
    darkPalette.setColor( QPalette::WindowText, Qt::white );
    darkPalette.setColor( QPalette::Base, QColor( 18, 18, 18 ) );
    darkPalette.setColor( QPalette::AlternateBase, darkColor );
    darkPalette.setColor( QPalette::ToolTipBase, Qt::white );
    darkPalette.setColor( QPalette::ToolTipText, Qt::white );
    darkPalette.setColor( QPalette::Text, Qt::white );
    darkPalette.setColor( QPalette::Disabled, QPalette::Text, disabledColor );
    darkPalette.setColor( QPalette::Button, darkColor );
    darkPalette.setColor( QPalette::ButtonText, Qt::white );
    darkPalette.setColor( QPalette::Dark, QColor( 35, 35, 35 ) );
    darkPalette.setColor( QPalette::Shadow, QColor( 20, 20, 20 ) );
    darkPalette.setColor( QPalette::Disabled, QPalette::ButtonText, disabledColor );
    darkPalette.setColor( QPalette::BrightText, Qt::red );
    darkPalette.setColor( QPalette::Link, QColor( 42, 130, 218 ) );

    darkPalette.setColor( QPalette::Highlight, QColor( 42, 130, 218 ) );
    darkPalette.setColor( QPalette::HighlightedText, Qt::black );
    darkPalette.setColor( QPalette::Disabled, QPalette::HighlightedText, disabledColor );

    qApp->setPalette( darkPalette );
  }
  else
  {
    qApp->setStyle( new QProxyStyle() );
    qApp->setPalette( QPalette() );
  }
  #endif

  QFile builtInCssFile( ":qt-style.css" );
  builtInCssFile.open( QFile::ReadOnly );
  QByteArray css = builtInCssFile.readAll();

#if defined(Q_OS_MAC)
  QFile macCssFile( ":qt-style-macos.css" );
  macCssFile.open( QFile::ReadOnly );
  css += macCssFile.readAll();
#endif

#if defined(Q_OS_WIN)
  QFile winCssFile( ":qt-style-win.css" );
  winCssFile.open( QFile::ReadOnly );
  css += winCssFile.readAll();

  // Load an additional stylesheet
  // Dark Mode doesn't work nice with custom qt style sheets,
  if (!darkMode){
    QFile additionalStyle( QString( ":qt-%1.css" ).arg( displayStyle ) );
    if ( additionalStyle.open( QFile::ReadOnly ) ){
      css += additionalStyle.readAll();
    }
  }

#endif

  // Try loading a style sheet if there's one
  QFile cssFile( Config::getUserQtCssFileName() );

  if ( cssFile.open( QFile::ReadOnly ) )
    css += cssFile.readAll();

  if( !addonStyle.isEmpty() )
  {
    QString name = Config::getStylesDir() + addonStyle
                   + QDir::separator() + "qt-style.css";
    QFile addonCss( name );
    if( addonCss.open( QFile::ReadOnly ) )
      css += addonCss.readAll();
  }

  #ifdef Q_OS_WIN32
  if(darkMode){
    css += "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }";
  }
  #endif

  setStyleSheet( css );
}

void MainWindow::updateTrayIcon()
{
  if ( !trayIcon && cfg.preferences.enableTrayIcon )
  {
    // Need to show it
    trayIcon = new QSystemTrayIcon( QIcon::fromTheme("goldendict-tray", QIcon( ":/icons/programicon_old.png" )), this );
    trayIcon->setContextMenu( &trayIconMenu );
    trayIcon->show();

    connect( trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated );
  }
  else if( trayIcon && !cfg.preferences.enableTrayIcon )
  {
    // Need to hide it
    delete trayIcon;

    trayIcon = 0;
  }
  if ( trayIcon )
  {
    // Update the icon to reflect the scanning mode
    trayIcon->setIcon( enableScanningAction->isChecked() ?
        QIcon::fromTheme("goldendict-scan-tray", QIcon( ":/icons/programicon_scan.png" )) :
        QIcon::fromTheme("goldendict-tray", QIcon( ":/icons/programicon_old.png" )) );

    trayIcon->setToolTip( "GoldenDict-ng" );
  }

  // The 'Close to tray' action is associated with the tray icon, so we hide
  // or show it here.
  ui.actionCloseToTray->setVisible( cfg.preferences.enableTrayIcon );
}

void MainWindow::wheelEvent( QWheelEvent *ev )
{
  if ( ev->modifiers().testFlag( Qt::ControlModifier ) )
  {
    if ( ev->angleDelta().y() > 0 )
    {
        zoomin();
    }
    else if ( ev->angleDelta().y()  < 0 )
    {
        zoomout();
    }
    ev->accept();
  }
  else
  {
    ev->ignore();
  }
}

void MainWindow::closeEvent( QCloseEvent * ev )
{
  if ( cfg.preferences.enableTrayIcon && cfg.preferences.closeToTray )
  {
    if( !cfg.preferences.searchInDock )
      translateBox->setPopupEnabled( false );

#ifdef Q_OS_MACOS
    if (!ev->spontaneous() || !isVisible()) {
      return;
    }
#endif
#ifdef HAVE_X11
    // Don't ignore the close event, because doing so cancels session logout if
    // the main window is visible when the user attempts to log out.
    // The main window will be only hidden, because QApplication::quitOnLastWindowClosed
    // property is false and Qt::WA_DeleteOnClose widget  is not set.
    Q_ASSERT(!QApplication::quitOnLastWindowClosed());
    Q_ASSERT(!testAttribute(Qt::WA_DeleteOnClose));
#else
    // Ignore the close event because closing the main window breaks global hotkeys on Windows.
    ev->ignore();
    hide();
#endif
  }
  else
  {
    ev->accept();
    quitApp();
  }
}

void MainWindow::quitApp()
{
  if( inspector && inspector->isVisible() )
  {
    inspector->close();
  }
  commitData();
  qApp->quit();
}

void MainWindow::applyProxySettings()
{
  if( cfg.preferences.proxyServer.enabled && cfg.preferences.proxyServer.useSystemProxy )
  {
    QList<QNetworkProxy> proxies = QNetworkProxyFactory::systemProxyForQuery();
    if( !cfg.preferences.proxyServer.systemProxyUser.isEmpty() )
    {
      proxies.first().setUser( cfg.preferences.proxyServer.systemProxyUser );
      proxies.first().setPassword( cfg.preferences.proxyServer.systemProxyPassword );
    }
    QNetworkProxy::setApplicationProxy( proxies.first() );
    return;
  }

  QNetworkProxy::ProxyType type = QNetworkProxy::NoProxy;

  if ( cfg.preferences.proxyServer.enabled )
  {
    switch( cfg.preferences.proxyServer.type )
    {
      case Config::ProxyServer::Socks5:
        type = QNetworkProxy::Socks5Proxy;
      break;
      case Config::ProxyServer::HttpConnect:
        type = QNetworkProxy::HttpProxy;
      break;
      case Config::ProxyServer::HttpGet:
        type = QNetworkProxy::HttpCachingProxy;
      break;

      default:
      break;
    }
  }

  QNetworkProxy proxy( type );

  if ( cfg.preferences.proxyServer.enabled )
  {
   proxy.setHostName( cfg.preferences.proxyServer.host );
   proxy.setPort( cfg.preferences.proxyServer.port );

   if ( cfg.preferences.proxyServer.user.size() )
     proxy.setUser( cfg.preferences.proxyServer.user );

   if ( cfg.preferences.proxyServer.password.size() )
     proxy.setPassword( cfg.preferences.proxyServer.password );
  }

  QNetworkProxy::setApplicationProxy( proxy );
}

void MainWindow::setupNetworkCache( int maxSize )
{
  // x << 20 == x * 2^20 converts mebibytes to bytes.
  qint64 const maxCacheSizeInBytes = maxSize <= 0 ? qint64( 0 ) : static_cast< qint64 >( maxSize ) << 20;

  if( QAbstractNetworkCache * abstractCache = articleNetMgr.cache() )
  {
    QNetworkDiskCache * const diskCache = qobject_cast< QNetworkDiskCache * >( abstractCache );
    Q_ASSERT_X( diskCache, Q_FUNC_INFO, "Unexpected network cache type." );
    diskCache->setMaximumCacheSize( maxCacheSizeInBytes );
    return;
  }
  if( maxCacheSizeInBytes == 0 )
    return; // There is currently no cache and it is not needed.

  QString const cacheDirectory = Config::getNetworkCacheDir();
  if( !QDir().mkpath( cacheDirectory ) ) {
    gdWarning( "Cannot create a cache directory %s. Disabling network cache.", cacheDirectory.toUtf8().constData() );
    return;
  }
  QNetworkDiskCache * const diskCache = new QNetworkDiskCache( this );
  diskCache->setMaximumCacheSize( maxCacheSizeInBytes );
  diskCache->setCacheDirectory( cacheDirectory );
  articleNetMgr.setCache( diskCache );

  webEngineProfile->setCachePath( cacheDirectory );
  webEngineProfile->setPersistentStoragePath( cacheDirectory );
}

void MainWindow::makeDictionaries()
{

  wordFinder.clear();

  dictionariesUnmuted.clear();

  ftsIndexing.stopIndexing();
  ftsIndexing.clearDictionaries();

  loadDictionaries( this, isVisible(), cfg, dictionaries, dictNetMgr, false );

  //create map
  dictMap = Dictionary::dictToMap(dictionaries);

  for( unsigned x = 0; x < dictionaries.size(); x++ )
  {
    dictionaries[ x ]->setFTSParameters( cfg.preferences.fts );
    dictionaries[ x ]->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
  }

  ftsIndexing.setDictionaries( dictionaries );
  ftsIndexing.doIndexing();

  updateStatusLine();
  updateGroupList();
}

void MainWindow::updateStatusLine()
{
  unsigned articleCount = 0, wordCount = 0;

  for( unsigned x = dictionaries.size(); x--; )
  {
    articleCount += dictionaries[ x ]->getArticleCount();
    wordCount += dictionaries[ x ]->getWordCount();
  }

  mainStatusBar->showMessage( tr( "%1 dictionaries, %2 articles, %3 words" ).
                              arg( dictionaries.size() ).arg( articleCount ).
                              arg( wordCount ), 10000 );
}

void MainWindow::updateGroupList()
{
  bool haveGroups = cfg.groups.size();

  groupList->setVisible( haveGroups );

  groupLabel.setText( haveGroups ? tr( "Look up in:" ) : tr( "Look up:" ) );

  // currentIndexChanged() signal is very trigger-happy. To avoid triggering
  // it, we disconnect it while we're clearing and filling back groups.
  disconnect( groupList, &GroupComboBox::currentIndexChanged,
              this, &MainWindow::currentGroupChanged );

  groupInstances.clear();

  // Add dictionaryOrder first, as the 'All' group.
  {
    Instances::Group g( cfg.dictionaryOrder, dictionaries, Config::Group() );

    // Add any missing entries to dictionary order
    Instances::complementDictionaryOrder( g,
                                          Instances::Group( cfg.inactiveDictionaries, dictionaries, Config::Group() ),
                                          dictionaries );

    g.name = tr( "All" );
    g.id = Instances::Group::AllGroupId;
    g.icon = "folder.png";

    groupInstances.push_back( g );
  }

  for( int x  = 0; x < cfg.groups.size(); ++x )
    groupInstances.push_back( Instances::Group( cfg.groups[ x ], dictionaries, cfg.inactiveDictionaries ) );

  // Update names for dictionaries that are present, so that they could be
  // found in case they got moved.
  Instances::updateNames( cfg, dictionaries );

  groupList->fill( groupInstances );
  groupList->setCurrentGroup( cfg.lastMainGroupId );

  updateCurrentGroupProperty();

  updateDictionaryBar();

#ifdef QT_DEBUG
  qDebug() << "Reloading all the tabs...";
#endif

  for( int i = 0; i < ui.tabWidget->count(); ++i )
  {
    ArticleView & view =
      dynamic_cast< ArticleView & >( *( ui.tabWidget->widget( i ) ) );

    view.reload();
  }

  connect( groupList, &GroupComboBox::currentIndexChanged,
           this, &MainWindow::currentGroupChanged );
}

void MainWindow::updateDictionaryBar()
{
  if ( !dictionaryBar.toggleViewAction()->isChecked() )
    return; // It's not enabled, therefore hidden -- don't waste time

  unsigned currentId = groupList -> getCurrentGroup();
  Instances::Group * grp = groupInstances.findGroup( currentId );

  dictionaryBar.setMutedDictionaries( 0 );
  if ( grp ) { // Should always be !0, but check as a safeguard
    if( currentId == Instances::Group::AllGroupId )
      dictionaryBar.setMutedDictionaries( &cfg.mutedDictionaries );
    else
    {
      Config::Group * grp = cfg.getGroup( currentId );
      dictionaryBar.setMutedDictionaries( grp ? &grp->mutedDictionaries : 0 );
    }

    dictionaryBar.setDictionaries( grp->dictionaries );

    int extent = useSmallIconsInToolbarsAction.isChecked() ?
               QApplication::style()->pixelMetric( QStyle::PM_SmallIconSize ) :
               QApplication::style()->pixelMetric(QStyle::PM_ToolBarIconSize);

    dictionaryBar.setDictionaryIconSize( extent );
  }
}

vector< sptr< Dictionary::Class > > const & MainWindow::getActiveDicts()
{
  if ( groupInstances.empty() )
    return dictionaries;

  int current = groupList->currentIndex();

  if ( current < 0 || current >= (int) groupInstances.size() )
  {
    // This shouldn't ever happen
    return dictionaries;
  }

  Config::MutedDictionaries const * mutedDictionaries = dictionaryBar.getMutedDictionaries();
  if ( !dictionaryBar.toggleViewAction()->isChecked() || mutedDictionaries == 0 )
    return groupInstances[ current ].dictionaries;
  else
  {
    vector< sptr< Dictionary::Class > > const & activeDicts =
      groupInstances[ current ].dictionaries;

    // Populate the special dictionariesUnmuted array with only unmuted
    // dictionaries

    dictionariesUnmuted.clear();
    dictionariesUnmuted.reserve( activeDicts.size() );

    for( unsigned x = 0; x < activeDicts.size(); ++x )
      if ( !mutedDictionaries->contains(
              QString::fromStdString( activeDicts[ x ]->getId() ) ) )
        dictionariesUnmuted.push_back( activeDicts[ x ] );

    return dictionariesUnmuted;
  }
}

void MainWindow::createTabList()
{
  tabListMenu->setIcon(QIcon(":/icons/windows-list.svg"));
  connect( tabListMenu, &QMenu::aboutToShow, this, &MainWindow::fillWindowsMenu );
  connect( tabListMenu, &QMenu::triggered, this, &MainWindow::switchToWindow );

  tabListButton = new QToolButton(ui.tabWidget);
  tabListButton->setAutoRaise(true);
  tabListButton->setIcon(tabListMenu->icon());
  tabListButton->setMenu(tabListMenu);
  tabListButton->setToolTip( tr( "Open Tabs List" ) );
  tabListButton->setPopupMode(QToolButton::InstantPopup);
  ui.tabWidget->setCornerWidget(tabListButton);
  tabListButton->setFocusPolicy(Qt::NoFocus);
}

void MainWindow::fillWindowsMenu()
{
  tabListMenu->clear();

  if(cfg.preferences.mruTabOrder)
  {
    for (int i = 0; i < mruList.count(); i++)
    {
      QAction *act = tabListMenu->addAction(ui.tabWidget->tabIcon(ui.tabWidget->indexOf(mruList.at(i))), ui.tabWidget->tabText(ui.tabWidget->indexOf(mruList.at(i))));

      //remember the index of the Tab to be later used in ctrlReleased()
      act->setData(ui.tabWidget->indexOf(mruList.at(i)));

      if (ui.tabWidget->currentIndex() == ui.tabWidget->indexOf(mruList.at(i)))
      {
        QFont f( act->font() );
        f.setBold( true );
        act->setFont( f );
      }
    }
    if (tabListMenu->actions().size() > 1)
    {
      tabListMenu->setActiveAction(tabListMenu->actions().at(1));
    }
  }
  else
  {
    for (int i = 0; i < ui.tabWidget->count(); i++)
    {
      QAction *act = tabListMenu->addAction( ui.tabWidget->tabIcon( i ),
      ui.tabWidget->tabText( i ) );
      act->setData( i );
      if (ui.tabWidget->currentIndex() == i)
      {
        QFont f( act->font() );
        f.setBold( true );
        act->setFont( f );
      }
    }
  }
  return;
}

void MainWindow::switchToWindow(QAction *act)
{
  int idx = act->data().toInt();
  ui.tabWidget->setCurrentIndex(idx);
}

void MainWindow::addNewTab()
{
  createNewTab( true, tr( "(untitled)" ) );
}

ArticleView * MainWindow::createNewTab( bool switchToIt,
                                        QString const & name )
{
  ArticleView * view = new ArticleView( this, articleNetMgr, audioPlayerFactory.player(),
                                        dictionaries, groupInstances, false, cfg,
                                        *ui.searchInPageAction,
                                        translateLine,
                                        dictionaryBar.toggleViewAction(),
                                        groupList );

  connect( view, &ArticleView::inspectSignal,this,&MainWindow::inspectElement);

  connect( view, &ArticleView::titleChanged, this, &MainWindow::titleChanged );

  connect( view, &ArticleView::iconChanged, this, &MainWindow::iconChanged );

  connect( view, &ArticleView::pageLoaded, this, &MainWindow::pageLoaded );

  connect( view, &ArticleView::updateFoundInDictsList, this, &MainWindow::updateFoundInDictsList );

  connect( view, &ArticleView::openLinkInNewTab, this, &MainWindow::openLinkInNewTab );

  connect( view, &ArticleView::showDefinitionInNewTab, this, &MainWindow::showDefinitionInNewTab );

  connect( view, &ArticleView::typingEvent, this, &MainWindow::typingEvent );

  connect( view, &ArticleView::activeArticleChanged, this, &MainWindow::activeArticleChanged );

  connect( view, &ArticleView::statusBarMessage, this, &MainWindow::showStatusBarMessage );

  connect( view, &ArticleView::showDictsPane, this, &MainWindow::showDictsPane );

  connect( view, &ArticleView::forceAddWordToHistory, this, &MainWindow::forceAddWordToHistory );

  connect( view, &ArticleView::sendWordToHistory, this, &MainWindow::addWordToHistory );

  connect( view, &ArticleView::sendWordToInputLine, this, &MainWindow::sendWordToInputLine );

  connect( view, &ArticleView::storeResourceSavePath, this, &MainWindow::storeResourceSavePath );

  connect( view, &ArticleView::zoomIn, this, &MainWindow::zoomin );

  connect( view, &ArticleView::zoomOut, this, &MainWindow::zoomout );
  connect( view, &ArticleView::saveBookmarkSignal, this, &MainWindow::addBookmarkToFavorite );

  view->setSelectionBySingleClick( cfg.preferences.selectWordBySingleClick );

  int index = cfg.preferences.newTabsOpenAfterCurrentOne ?
              ui.tabWidget->currentIndex() + 1 : ui.tabWidget->count();

  QString escaped = name;
  escaped.replace( "&", "&&" );

  ui.tabWidget->insertTab( index, view, escaped );
  mruList.append(dynamic_cast<QWidget*>(view));

  if ( switchToIt )
    ui.tabWidget->setCurrentIndex( index );

  view->setZoomFactor( cfg.preferences.zoomFactor );

#ifdef Q_OS_WIN32
  view->installEventFilter( this );
#endif
  return view;
}

void MainWindow::inspectElement( QWebEnginePage * page )
{
  inspector->triggerAction( page );
}

void MainWindow::tabCloseRequested( int x )
{
  QWidget * w = ui.tabWidget->widget( x );

  mruList.removeOne(w);

  // In MRU case: First, we switch to the appropriate tab
  // and only then remove the old one.

  //activate a tab in accordance with MRU
  if ( cfg.preferences.mruTabOrder && mruList.size() > 0 ) {
    ui.tabWidget->setCurrentWidget(mruList.at(0));
  }
  else if( ui.tabWidget->count() > 1 )
  {
    //activate neighboring tab
    int n = x >= ui.tabWidget->count() - 1 ? x - 1 : x + 1;
    if( n >= 0 )
      ui.tabWidget->setCurrentIndex( n );
  }

  ui.tabWidget->removeTab( x );
  delete w;

  // if everything is closed, add a new tab
  if ( ui.tabWidget->count() == 0 )
    addNewTab();
}

void MainWindow::closeCurrentTab()
{
  tabCloseRequested( ui.tabWidget->currentIndex() );
}

void MainWindow::closeAllTabs()
{
  while (ui.tabWidget->count() > 1)
    closeCurrentTab();

  // close last tab
  closeCurrentTab();
}

void MainWindow::closeRestTabs()
{
  if ( ui.tabWidget->count() < 2 )
    return;

  int idx = ui.tabWidget->currentIndex();

  for (int i = 0; i < idx; i++)
    tabCloseRequested(0);

  ui.tabWidget->setCurrentIndex(0);

  while (ui.tabWidget->count() > 1)
    tabCloseRequested(1);
}

void MainWindow::switchToNextTab()
{
  if ( ui.tabWidget->count() < 2 )
    return;

  ui.tabWidget->setCurrentIndex( ( ui.tabWidget->currentIndex() + 1 ) % ui.tabWidget->count() );
}

void MainWindow::switchToPrevTab()
{
  if ( ui.tabWidget->count() < 2 )
    return;

  if ( !ui.tabWidget->currentIndex() )
    ui.tabWidget->setCurrentIndex( ui.tabWidget->count() - 1 );
  else
    ui.tabWidget->setCurrentIndex( ui.tabWidget->currentIndex() - 1 );
}

void MainWindow::backClicked()
{
  GD_DPRINTF( "Back\n" );

  ArticleView *view = getCurrentArticleView();

  view->back();
}

void MainWindow::forwardClicked()
{
  GD_DPRINTF( "Forward\n" );

  ArticleView *view = getCurrentArticleView();

  view->forward();
}

void MainWindow::titleChanged( ArticleView * view, QString const & title )
{
  //the title can be url if html title is empty.according to qwebenginepage title() document.
  QString escaped ;
  if (title!=nullptr && title.contains(":")) {
    //check if the title is url.
    QUrl url(title);
    escaped = Utils::Url::queryItemValue(url,"word");
  } else {
    escaped = title;
  }
  escaped.replace( "&", "&&" );

  if( escaped.isRightToLeft() )
  {
    escaped.insert( 0, (ushort)0x202E ); // RLE, Right-to-Left Embedding
    escaped.append( (ushort)0x202C ); // PDF, POP DIRECTIONAL FORMATTING
  }

  int index = ui.tabWidget->indexOf( view );
  if( !escaped.isEmpty() )
    ui.tabWidget->setTabText( index, escaped );

  if( index == ui.tabWidget->currentIndex() )
  {
    // Set icon for "Add to Favorites" action
    if( isWordPresentedInFavorites( title, cfg.lastMainGroupId ) )
    {
      addToFavorites->setIcon( blueStarIcon );
      addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
    }
    else
    {
      addToFavorites->setIcon( starIcon );
      addToFavorites->setToolTip( tr( "Add current tab to Favorites" ) );
    }

    updateWindowTitle();
  }
}

void MainWindow::iconChanged( ArticleView * view, QIcon const & icon )
{
  ui.tabWidget->setTabIcon( ui.tabWidget->indexOf( view ), groupInstances.size() > 1 ? icon : QIcon() );
}

void MainWindow::updateWindowTitle()
{
  ArticleView *view = getCurrentArticleView();
  if ( view )
  {
    QString str = view->getTitle();
    if( !str.isEmpty() )
    {
      if( str.isRightToLeft() )
      {
        str.insert( 0, (ushort)0x202E ); // RLE, Right-to-Left Embedding
        str.append( (ushort)0x202C ); // PDF, POP DIRECTIONAL FORMATTING
      }
      if( !blockUpdateWindowTitle )
        setWindowTitle( tr( "%1 - %2" ).arg( str, "GoldenDict-ng" ) );
      blockUpdateWindowTitle = false;
    }
  }
}

void MainWindow::pageLoaded( ArticleView * view )
{
  if( view != getCurrentArticleView() )
    return; // It was background action

  updateBackForwardButtons();

  updatePronounceAvailability();

  if ( cfg.preferences.pronounceOnLoadMain )
    pronounce( view );

  //updateFoundInDictsList();
}

void MainWindow::showStatusBarMessage( QString const & message, int timeout, QPixmap const & icon )
{
  if( message.isEmpty() )
    mainStatusBar->clearMessage();
  else
    mainStatusBar->showMessage( message, timeout, icon );
}

void MainWindow::tabSwitched( int )
{
  translateBox->setPopupEnabled( false );
  updateBackForwardButtons();
  updatePronounceAvailability();
  updateFoundInDictsList();
  updateWindowTitle();
  if (mruList.size() > 1)
  {
    int from = mruList.indexOf( ui.tabWidget->widget( ui.tabWidget->currentIndex() ) );
    if ( from > 0)
      mruList.move( from, 0 );
  }

  // Set icon for "Add to Favorites" action
  QString headword = ui.tabWidget->tabText( ui.tabWidget->currentIndex() );
  if( isWordPresentedInFavorites( unescapeTabHeader( headword ), cfg.lastMainGroupId ) )
  {
    addToFavorites->setIcon( blueStarIcon );
    addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
  }
  else
  {
    addToFavorites->setIcon( starIcon );
    addToFavorites->setToolTip( tr( "Add current tab to Favorites" ) );
  }
}

void MainWindow::tabMenuRequested(QPoint pos)
{
//  // do not show this menu for single tab
//  if ( ui.tabWidget->count() < 2 )
//    return;

  tabMenu->popup(ui.tabWidget->mapToGlobal(pos));
}

void MainWindow::dictionaryBarToggled( bool )
{
  // From now on, only the triggered() signal is interesting to us
  disconnect( dictionaryBar.toggleViewAction(), &QAction::toggled, this, &MainWindow::dictionaryBarToggled );

  updateDictionaryBar(); // Updates dictionary bar contents if it's shown
  applyMutedDictionariesState(); // Visibility change affects searches and results
}

void MainWindow::pronounce( ArticleView * view )
{
  if ( view )
    view->playSound();
  else
    getCurrentArticleView()->playSound();
}

void MainWindow::showDictsPane( )
{
  if( !ui.dictsPane->isVisible() )
    ui.dictsPane->show();
}

void MainWindow::dictsPaneVisibilityChanged( bool visible )
{
  if (visible) {
    updateFoundInDictsList();
  }
}

void MainWindow::updateFoundInDictsList()
{
  if (!ui.dictsList->isVisible())
  {
    // nothing to do, the list is not visible
    return;
  }

  ui.dictsList->clear();

  ArticleView *view = getCurrentArticleView();

  if ( view )
  {
    QStringList ids = view->getArticlesList();
    QString activeId = view->getActiveArticleId();

    for( QStringList::const_iterator i = ids.constBegin(); i != ids.constEnd(); ++i)
    {
      // Find this dictionary

      for( unsigned x = dictionaries.size(); x--; )
      {
        if ( dictionaries[ x ]->getId() == i->toUtf8().data() )
        {
          QString dictName = QString::fromUtf8( dictionaries[ x ]->getName().c_str() );
          QString dictId = QString::fromUtf8( dictionaries[ x ]->getId().c_str() );
          QListWidgetItem * item =
              new QListWidgetItem(
                dictionaries[ x ]->getIcon(),
                dictName,
                ui.dictsList, QListWidgetItem::Type );
          item->setData(Qt::UserRole, QVariant( dictId ) );
          item->setToolTip(dictName);

          ui.dictsList->addItem( item );
          if (dictId == activeId)
          {
            ui.dictsList->setCurrentItem(item);
          }
          break;
        }
      }
    }

    //if no item in dict List panel has been choose ,select first one.
    if (ui.dictsList->count() > 0 && ui.dictsList->selectedItems().empty()) {
        ui.dictsList->setCurrentRow(0);
    }

  }
}

void MainWindow::updateBackForwardButtons()
{
  ArticleView *view = getCurrentArticleView();

  if ( view )
  {
    navBack->setEnabled(view->canGoBack());
    navForward->setEnabled(view->canGoForward());
  }
}

void MainWindow::updatePronounceAvailability()
{
    if (ui.tabWidget->count() > 0) {
        getCurrentArticleView()->hasSound([this](bool has) {
            navPronounce->setEnabled( has );
        });
    }
    else {
        navPronounce->setEnabled( false );
    }
}

void MainWindow::editDictionaries( unsigned editDictionaryGroup )
{
  hotkeyWrapper.reset(); // No hotkeys while we're editing dictionaries
  closeHeadwordsDialog();
  closeFullTextSearchDialog();

  wordFinder.clear();
  dictionariesUnmuted.clear();

  { // Limit existence of newCfg

  Config::Class newCfg = cfg;
  EditDictionaries dicts( this, newCfg, dictionaries, groupInstances, dictNetMgr );

  connect( &dicts, &EditDictionaries::showDictionaryInfo, this, &MainWindow::showDictionaryInfo );

  connect( &dicts, &EditDictionaries::showDictionaryHeadwords,this, &MainWindow::showDictionaryHeadwords);

  if ( editDictionaryGroup != Instances::Group::NoGroupId )
    dicts.editGroup( editDictionaryGroup );

  dicts.restoreGeometry( cfg.dictionariesDialogGeometry );
  dicts.exec();
  cfg.dictionariesDialogGeometry = newCfg.dictionariesDialogGeometry = dicts.saveGeometry();

  if ( dicts.areDictionariesChanged() || dicts.areGroupsChanged() )
  {
    ftsIndexing.stopIndexing();
    ftsIndexing.clearDictionaries();
    // Set muted dictionaries from old groups
    for( int x = 0; x < newCfg.groups.size(); x++ )
    {
      unsigned id = newCfg.groups[ x ].id;
      if( id != Instances::Group::NoGroupId )
      {
        Config::Group const * grp = cfg.getGroup( id );
        if( grp )
        {
          newCfg.groups[ x ].mutedDictionaries = grp->mutedDictionaries;
          newCfg.groups[ x ].popupMutedDictionaries = grp->popupMutedDictionaries;
        }
      }
    }

    cfg = newCfg;

    updateGroupList();

    Config::save( cfg );

    updateSuggestionList();

    for( unsigned x = 0; x < dictionaries.size(); x++ )
    {
      dictionaries[ x ]->setFTSParameters( cfg.preferences.fts );
      dictionaries[ x ]->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
    }

    ftsIndexing.setDictionaries( dictionaries );
    ftsIndexing.doIndexing();
  }

  }

  scanPopup->refresh();
  installHotKeys();


}

void MainWindow::editCurrentGroup()
{
  editDictionaries( groupList->getCurrentGroup() );
}

void MainWindow::editPreferences()
{
  hotkeyWrapper.reset(); // So we could use the keys it hooks
  closeHeadwordsDialog();
  closeFullTextSearchDialog();

  ftsIndexing.stopIndexing();
  ftsIndexing.clearDictionaries();

  Preferences preferences( this, cfg );

  preferences.show();

  if ( preferences.exec() == QDialog::Accepted )
  {
    Config::Preferences p = preferences.getPreferences();

    // These parameters are not set in dialog
    p.zoomFactor = cfg.preferences.zoomFactor;
    p.helpZoomFactor = cfg.preferences.helpZoomFactor;
    p.wordsZoomLevel = cfg.preferences.wordsZoomLevel;
    p.hideMenubar = cfg.preferences.hideMenubar;
    p.searchInDock = cfg.preferences.searchInDock;
    p.alwaysOnTop = cfg.preferences.alwaysOnTop;

    p.proxyServer.systemProxyUser = cfg.preferences.proxyServer.systemProxyUser;
    p.proxyServer.systemProxyPassword = cfg.preferences.proxyServer.systemProxyPassword;

    p.fts.dialogGeometry = cfg.preferences.fts.dialogGeometry;
    p.fts.matchCase = cfg.preferences.fts.matchCase;
    p.fts.maxArticlesPerDictionary = cfg.preferences.fts.maxArticlesPerDictionary;
    p.fts.maxDistanceBetweenWords = cfg.preferences.fts.maxDistanceBetweenWords;
    p.fts.searchMode = cfg.preferences.fts.searchMode;
    p.fts.useMaxArticlesPerDictionary = cfg.preferences.fts.useMaxArticlesPerDictionary;
    p.fts.useMaxDistanceBetweenWords = cfg.preferences.fts.useMaxDistanceBetweenWords;
    p.fts.ignoreWordsOrder = cfg.preferences.fts.ignoreWordsOrder;
    p.fts.ignoreDiacritics = cfg.preferences.fts.ignoreDiacritics;

    // See if we need to reapply Qt stylesheets
    if( cfg.preferences.displayStyle != p.displayStyle ||
      cfg.preferences.darkMode != p.darkMode )
    {
      applyQtStyleSheet( p.addonStyle, p.displayStyle, p.darkMode );
    }

    if( cfg.preferences.historyStoreInterval != p.historyStoreInterval )
      history.setSaveInterval( p.historyStoreInterval );

    if( cfg.preferences.favoritesStoreInterval != p.favoritesStoreInterval )
      ui.favoritesPaneWidget->setSaveInterval( p.favoritesStoreInterval );

    if( cfg.preferences.maxNetworkCacheSize != p.maxNetworkCacheSize )
      setupNetworkCache( p.maxNetworkCacheSize );

    bool needReload =
      ( cfg.preferences.displayStyle != p.displayStyle
        || cfg.preferences.addonStyle != p.addonStyle
        || cfg.preferences.darkReaderMode != p.darkReaderMode
        || cfg.preferences.collapseBigArticles != p.collapseBigArticles
        || cfg.preferences.articleSizeLimit != p.articleSizeLimit
        || cfg.preferences.alwaysExpandOptionalParts != p.alwaysExpandOptionalParts // DSL format's special feature
      );

    // This line must be here because the components below require cfg's value to reconfigure
    // After this point, p must not be accessed.
    cfg.preferences = p;

    // Loop through all tabs and reload pages due to ArticleMaker's change.
    for( int x = 0; x < ui.tabWidget->count(); ++x )
    {
      ArticleView & view =
        dynamic_cast< ArticleView & >( *( ui.tabWidget->widget( x ) ) );

      view.setSelectionBySingleClick( p.selectWordBySingleClick );

      if( needReload ) {
        view.reload();
      }
    }

    audioPlayerFactory.setPreferences( cfg.preferences );

    updateTrayIcon();
    applyProxySettings();

    ui.tabWidget->setHideSingleTab(cfg.preferences.hideSingleTab);

    setAutostart( cfg.preferences.autoStart );

    prepareNewReleaseChecks();

    history.enableAdd( cfg.preferences.storeHistory );
    history.setMaxSize( cfg.preferences.maxStringsInHistory );
    ui.historyPaneWidget->updateHistoryCounts();

    for( unsigned x = 0; x < dictionaries.size(); x++ )
    {
      dictionaries[ x ]->setFTSParameters( cfg.preferences.fts );
      dictionaries[ x ]->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
    }

    ui.fullTextSearchAction->setEnabled( cfg.preferences.fts.enabled );

    Config::save( cfg );
  }

  scanPopup->refresh();
  installHotKeys();

  ftsIndexing.setDictionaries( dictionaries );
  ftsIndexing.doIndexing();
}

void MainWindow::currentGroupChanged( int )
{
  cfg.lastMainGroupId = groupList->getCurrentGroup();
  Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if( cfg.lastMainGroupId == Instances::Group::AllGroupId )
  {
    if( igrp )
      igrp->checkMutedDictionaries( &cfg.mutedDictionaries );
    dictionaryBar.setMutedDictionaries( &cfg.mutedDictionaries );
  }
  else
  {
    Config::Group * grp = cfg.getGroup( cfg.lastMainGroupId );
    if( grp )
    {
      if( igrp )
        igrp->checkMutedDictionaries( &grp->mutedDictionaries );
      dictionaryBar.setMutedDictionaries( &grp->mutedDictionaries );
    }
    else
      dictionaryBar.setMutedDictionaries( 0 );
  }

  if(igrp){
    GlobalBroadcaster::instance()->currentGroupId = cfg.lastMainGroupId;
  }

  updateDictionaryBar();

  // Update word search results
  translateBox->setPopupEnabled( false );
  updateSuggestionList();
  translateInputFinished( false );

  updateCurrentGroupProperty();

  if( ftsDlg )
    ftsDlg->setCurrentGroup( cfg.lastMainGroupId );
}

void MainWindow::updateCurrentGroupProperty()
{
  // We maintain currentGroup property so styles could use that to change
  // fonts based on group names
  Instances::Group * grp =
      groupInstances.findGroup( groupList->getCurrentGroup() );

  if ( grp && translateLine->property( "currentGroup" ).toString() !=
       grp->name )
  {
    translateLine->setProperty( "currentGroup", grp->name );
    wordList->setProperty( "currentGroup", grp->name );
    QString ss = styleSheet();

    // Only update stylesheet if it mentions currentGroup, as updating the
    // stylesheet is a slow operation
    if ( ss.contains("currentGroup") )
      setStyleSheet( ss );
  }
}

void MainWindow::translateInputChanged( QString const & newValue )
{
  updateSuggestionList( newValue );
  translateBoxSuffix = QString();
  // Save translate line text. Later it can be passed to external applications.
  GlobalBroadcaster::instance()->translateLineText = newValue;
}

void MainWindow::updateSuggestionList()
{
  updateSuggestionList( translateLine->text() );
}

void MainWindow::updateSuggestionList( QString const & newValue )
{
  // If there's some status bar message present, clear it since it may be
  // about the previous search that has failed.
  if ( !mainStatusBar->currentMessage().isEmpty() )
  {
    mainStatusBar->clearMessage();
  }

  // If some word is selected in the word list, unselect it. This prevents
  // triggering a set of spurious activation signals when the list changes.

  if ( wordList->selectionModel()->hasSelection() )
    wordList->setCurrentItem( 0, QItemSelectionModel::Clear );

  QString req = newValue.trimmed();

  if ( !req.size() )
  {
    // An empty request always results in an empty result
    wordFinder.cancel();
    wordList->clear();
    wordList->unsetCursor();

    // Reset the noResults mark if it's on right now
    if ( translateLine->property( "noResults" ).toBool() )
    {
      translateLine->setProperty( "noResults", false );

      Utils::Widget::setNoResultColor( translateLine, false );
    }
    return;
  }

  wordList->setCursor( Qt::WaitCursor );

  wordFinder.prefixMatch( req, getActiveDicts() );
}

void MainWindow::translateInputFinished( bool checkModifiers )
{
  QString word = Folding::unescapeWildcardSymbols( translateLine->text() );
  respondToTranslationRequest( Config::InputPhrase( word, translateBoxSuffix ), checkModifiers );
}

void MainWindow::respondToTranslationRequest( Config::InputPhrase const & phrase,
                                              bool checkModifiers, QString const & scrollTo )
{
  if ( phrase.isValid() )
  {
    Qt::KeyboardModifiers mods = QApplication::keyboardModifiers();
    if ( checkModifiers && ( mods & (Qt::ControlModifier | Qt::ShiftModifier) ) )
      addNewTab();

    showTranslationFor( phrase, 0, scrollTo );

    if ( cfg.preferences.searchInDock )
    {
      if ( ui.searchPane->isFloating() )
        activateWindow();
    }

    getCurrentArticleView()->focus();
  }
}

void MainWindow::setTranslateBoxTextAndKeepSuffix( QString text, WildcardPolicy wildcardPolicy,
                                                   TranslateBoxPopup popupAction )
{
  if( wildcardPolicy == EscapeWildcards )
    text = Folding::escapeWildcardSymbols( text );

  if( popupAction == NoPopupChange || cfg.preferences.searchInDock )
    translateLine->setText( text );
  else
    translateBox->setText( text, popupAction == EnablePopup );
}

void MainWindow::setTranslateBoxTextAndClearSuffix( QString const & text, WildcardPolicy wildcardPolicy,
                                                    TranslateBoxPopup popupAction )
{
  setTranslateBoxTextAndKeepSuffix( text, wildcardPolicy, popupAction );
  translateBoxSuffix = QString();
}

void MainWindow::handleEsc()
{
  ArticleView *view = getCurrentArticleView();
  if ( view && view->closeSearch() )
    return;

  if( cfg.preferences.escKeyHidesMainWindow )
  {
    toggleMainWindow();
  }
  else
    focusTranslateLine();
}

void MainWindow::focusTranslateLine()
{
  if ( cfg.preferences.searchInDock )
  {
    if ( ui.searchPane->isFloating() )
      ui.searchPane->activateWindow();
  }
  else
  {
    if ( !isActiveWindow() )
      activateWindow();
  }

  translateLine->clearFocus();
  translateLine->setFocus();
  translateLine->selectAll();
}

void MainWindow::applyMutedDictionariesState()
{
  translateBox->setPopupEnabled( false );

  updateSuggestionList();

  ArticleView *view = getCurrentArticleView();

  if ( view )
  {
    // Update active article view
    view->updateMutedContents();
  }
}

bool MainWindow::handleBackForwardMouseButtons ( QMouseEvent * event) {
  if ( event->button() == Qt::XButton1 ) {
    backClicked();
    return true;
  }
  else
  if ( event->button() == Qt::XButton2 ) {
    forwardClicked();
    return true;
  }
  else
    return false;
}

bool MainWindow::eventFilter( QObject * obj, QEvent * ev )
{
  if ( ev->type() == QEvent::ShortcutOverride
       || ev->type() == QEvent::KeyPress )
  {
    QKeyEvent * ke = static_cast<QKeyEvent*>( ev );

    // Handle F3/Shift+F3 shortcuts
    if ( ke->key() == Qt::Key_F3 )
    {
      ArticleView  * view = getCurrentArticleView();
      if ( view  && view->handleF3( obj, ev ) )
        return true;
    }
  }

  // when the main window is moved or resized, hide the word list suggestions
  if ( obj == this && ( ev->type() == QEvent::Move || ev->type() == QEvent::Resize ) )
  {
    if ( !cfg.preferences.searchInDock )
    {
        translateBox->setPopupEnabled( false );
        return false;
    }
  }

  if ( obj == this && ev->type() == QEvent::WindowStateChange )
  {
    QWindowStateChangeEvent *stev = static_cast< QWindowStateChangeEvent *>( ev );
    wasMaximized = ( stev->oldState() == Qt::WindowMaximized && isMinimized() );
  }

  if ( ev->type() == QEvent::MouseButtonPress ) {
    QMouseEvent * event = static_cast< QMouseEvent * >( ev );

    // clicks outside of the word list should hide it.
    if (obj != translateBox->wordList() && obj != translateBox->wordList()->viewport()) {
      translateBox->setPopupEnabled( false );
    }

    return handleBackForwardMouseButtons( event );
  }

  if (ev->type() == QEvent::KeyPress)
  {
    QKeyEvent *keyevent = static_cast<QKeyEvent*>(ev);

    bool handleCtrlTab = ( obj == translateLine
                           || obj == wordList
                           || obj == ui.historyList
                           || obj == ui.favoritesTree
                           || obj == ui.dictsList
                           || obj == groupList );

    if (keyevent->modifiers() == Qt::ControlModifier && keyevent->key() == Qt::Key_Tab)
    {
      if (cfg.preferences.mruTabOrder)
      {
        ctrlTabPressed();
        return true;
      }
      else if( handleCtrlTab )
      {
        QApplication::sendEvent( ui.tabWidget, ev );
        return true;
      }
      return false;
    }
    if( handleCtrlTab && keyevent->matches( QKeySequence::PreviousChild ) ) // Handle only Ctrl+Shist+Tab here because Ctrl+Tab was already handled before
    {
      QApplication::sendEvent( ui.tabWidget, ev );
      return true;
    }
  }

  if ( obj == translateLine )
  {
    if ( ev->type() == QEvent::KeyPress )
    {
      QKeyEvent * keyEvent = static_cast< QKeyEvent * >( ev );

      if ( cfg.preferences.searchInDock )
      {
        if ( keyEvent->matches( QKeySequence::MoveToNextLine ) && wordList->count() )
        {
          wordList->setFocus( Qt::ShortcutFocusReason );
          wordList->setCurrentRow( 0, QItemSelectionModel::ClearAndSelect );
          return true;
        }
      }

    }

    if ( ev->type() == QEvent::FocusIn ) {
      QFocusEvent * focusEvent = static_cast< QFocusEvent * >( ev );

      // select all on mouse click
      if ( focusEvent->reason() == Qt::MouseFocusReason ) {
        QTimer::singleShot( 0, this, &MainWindow::focusTranslateLine );
      }
      return false;
    }

    if ( ev->type() == QEvent::Resize ) {
      QResizeEvent * resizeEvent = static_cast< QResizeEvent * >( ev );
      groupList->setFixedHeight( resizeEvent->size().height() );
      return false;
    }
  }
  else
  if ( obj == wordList )
  {
    if (ev->type() == QEvent::KeyPress || ev->type() == QEvent::ShortcutOverride) 
    {
      QKeyEvent * keyEvent = static_cast< QKeyEvent * >( ev );

      if ( keyEvent->matches( QKeySequence::MoveToPreviousLine ) &&
           !wordList->currentRow() )
      {
        wordList->setCurrentRow( 0, QItemSelectionModel::Clear );
        translateLine->setFocus( Qt::ShortcutFocusReason );
        return true;
      }

      if ( keyEvent->matches( QKeySequence::InsertParagraphSeparator ) &&
           wordList->selectedItems().size() )
      {
        if ( cfg.preferences.searchInDock )
        {
          if ( ui.searchPane->isFloating() )
            activateWindow();
        }

        getCurrentArticleView()->focus();

        return cfg.preferences.searchInDock;
      }

      // Handle typing events used to initiate new lookups
      // TODO: refactor to eliminate duplication (see below)

      if ( keyEvent->modifiers() &
           ( Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier ) )
        return false; // A non-typing modifier is pressed

      if (  Utils::ignoreKeyEvent(keyEvent))
        return false; // Those key have other uses than to start typing
                      // or don't make sense

      QString text = keyEvent->text();

      if ( text.size() )
      {
        typingEvent( text );
        return true;
      }
    }
  }
  else
  if (obj == ui.dictsList) {
    if ( ev->type() == QEvent::KeyPress || ev->type() == QEvent::ShortcutOverride)
    {
      QKeyEvent * keyEvent = static_cast< QKeyEvent * >( ev );

      // Handle typing events used to initiate new lookups
      // TODO: refactor to eliminate duplication (see above)

      if ( keyEvent->modifiers() &
           ( Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier ) )
        return false; // A non-typing modifier is pressed

      if ( Utils::ignoreKeyEvent(keyEvent))
        return false; // Those key have other uses than to start typing
                      // or don't make sense

      QString text = keyEvent->text();

      if ( text.size() )
      {
        typingEvent( text );
        return true;
      }
    }
  }
    return QMainWindow::eventFilter( obj, ev );

  return false;
}

void MainWindow::wordListItemActivated( QListWidgetItem * item )
{
  if( wordListSelChanged )
    wordListSelChanged = false;
  else {
    // TODO: code duplication with translateInputFinished!

    Qt::KeyboardModifiers mods = QApplication::keyboardModifiers();
    if ( mods & (Qt::ControlModifier | Qt::ShiftModifier) )
      addNewTab();

    showTranslationFor( item->text() );
    getCurrentArticleView()->focus();
  }
}

void MainWindow::wordListSelectionChanged()
{
  QList< QListWidgetItem * > selected = wordList->selectedItems();

  if ( selected.size() )
  {
    wordListSelChanged = true;
    showTranslationFor( selected.front()->text() );
  }
}

void MainWindow::dictsListItemActivated( QListWidgetItem * item )
{
  jumpToDictionary( item, true );
}

void MainWindow::dictsListSelectionChanged()
{
  QList< QListWidgetItem * > selected = ui.dictsList->selectedItems();
  if( selected.size() )
  {
    ArticleView * view = getCurrentArticleView();
    if( view )
    {
      QString dictId = ui.dictsList->selectedItems().at( 0 )->data( Qt::UserRole ).toString();
      view->setActiveArticleId( dictId );
    }
    // selection change ,no need to jump to article ,if jump to article ,the position in webview would be changed
    // when click the dictionary in the html.

    // jumpToDictionary( selected.front() );
  }
}

void MainWindow::jumpToDictionary( QListWidgetItem * item, bool force )
{
  ArticleView * view = getCurrentArticleView();
  if ( view )
  {
    view->jumpToDictionary( item->data( Qt::UserRole ).toString(), force );
  }
}

void MainWindow::openLinkInNewTab( QUrl const & url,
                                   QUrl const & referrer,
                                   QString const & fromArticle,
                                   Contexts const & contexts )
{
  createNewTab( !cfg.preferences.newTabsOpenInBackground, "" )->
      openLink( url, referrer, fromArticle, contexts );
}

void MainWindow::showDefinitionInNewTab( QString const & word,
                                         unsigned group,
                                         QString const & fromArticle,
                                         Contexts const & contexts )
{
  createNewTab( !cfg.preferences.newTabsOpenInBackground, word )->
      showDefinition( word, group, fromArticle, contexts );
}

void MainWindow::activeArticleChanged( ArticleView const * view, QString const & id )
{
  if( view != getCurrentArticleView() )
    return; // It was background action

  // select the row with the corresponding id
  for (int i = 0; i < ui.dictsList->count(); ++i) {
    QListWidgetItem * w = ui.dictsList->item( i );
    QString dictId = w->data( Qt::UserRole ).toString();

    if ( dictId == id )
    {
      // update the current row, but only if necessary
      if ( i != ui.dictsList->currentRow() )
      {
        ui.dictsList->setCurrentRow(i);
      }
      return;
    }
  }
}

void MainWindow::typingEvent( QString const & t )
{
  if ( t == "\n" || t == "\r" )
  {
    if( translateLine->isEnabled() )
      focusTranslateLine();
  }
  else
  {
    if ( ( cfg.preferences.searchInDock && ui.searchPane->isFloating() ) || ui.dictsPane->isFloating() )
      ui.searchPane->activateWindow();

    if( translateLine->isEnabled() )
    {
        translateLine->clear();
        translateLine->setFocus();
        // Escaping the typed-in characters is the user's responsibility.
        //      setTranslateBoxTextAndClearSuffix( t, WildcardsAreAlreadyEscaped, EnablePopup );
        //      translateLine->setCursorPosition( t.size() );
    }
  }
}

void MainWindow::mutedDictionariesChanged()
{
  if ( dictionaryBar.toggleViewAction()->isChecked() )
    applyMutedDictionariesState();
}

void MainWindow::showHistoryItem( QString const & word )
{
  // qDebug() << "Showing history item" << word;

  history.enableAdd( false );

  setTranslateBoxTextAndClearSuffix( word, EscapeWildcards, DisablePopup );
  showTranslationFor( word );

  history.enableAdd( cfg.preferences.storeHistory );
}

void MainWindow::showTranslationFor( Config::InputPhrase const & phrase,
                                     unsigned inGroup,
                                     QString const & scrollTo )
{
  ArticleView *view = getCurrentArticleView();

  navPronounce->setEnabled( false );

  unsigned group = inGroup ? inGroup :
                   ( groupInstances.empty() ? 0 :
                        groupInstances[ groupList->currentIndex() ].id );

  view->showDefinition( phrase, group, scrollTo );


  //ui.tabWidget->setTabText( ui.tabWidget->indexOf(ui.tab), inWord.trimmed() );
}

void MainWindow::showTranslationFor( QString const & word )
{
  showTranslationFor( Config::InputPhrase::fromPhrase( word ) );
}

void MainWindow::showTranslationFor( QString const & inWord,
                                     QStringList const & dictIDs,
                                     QRegExp const & searchRegExp,
                                     bool ignoreDiacritics )
{
  ArticleView *view = getCurrentArticleView();

  navPronounce->setEnabled( false );

  view->showDefinition( inWord, dictIDs, searchRegExp,
                        groupInstances[ groupList->currentIndex() ].id,
                        ignoreDiacritics );
}

#ifdef HAVE_X11
void MainWindow::toggleMainWindow( bool onlyShow, bool byIconClick )
#else
void MainWindow::toggleMainWindow( bool onlyShow )
#endif
{
  bool shown = false;

  if( !cfg.preferences.searchInDock )
    translateBox->setPopupEnabled( false );

  if ( !isVisible() )
  {
    show();

    qApp->setActiveWindow( this );
    activateWindow();
    raise();
    shown = true;
  }
  else
  if ( isMinimized() )
  {
    if( wasMaximized )
      showMaximized();
    else
      showNormal();
    activateWindow();
    raise();
    shown = true;
  }
  else if( !isActiveWindow() ) {
    qApp->setActiveWindow( this );
    if( cfg.preferences.raiseWindowOnSearch ) {
      raise();
      activateWindow();
    }
    shown = true;
  }
  else
  if ( !onlyShow )
  {

// On Windows and Linux, a hidden window won't show a task bar icon
// When trayicon is enabled, the duplication is unneeded

// On macOS, a hidden window will still show on the Dock,
// but click it won't bring it back, thus we can only minimize it.

#ifdef Q_OS_MAC
      if (cfg.preferences.enableTrayIcon)
          showMinimized();
#else
    if (cfg.preferences.enableTrayIcon)
        hide();
    else
        showMinimized();
#endif


    if( headwordsDlg )
      headwordsDlg->hide();

    if( ftsDlg )
      ftsDlg->hide();

  }

  if ( shown )
  {
    if( headwordsDlg )
      headwordsDlg->show();

    if( ftsDlg )
      ftsDlg->show();

    focusTranslateLine();

  }
}

void MainWindow::installHotKeys()
{
  hotkeyWrapper.reset(); // Remove the old one

  if ( cfg.preferences.enableMainWindowHotkey ||
       cfg.preferences.enableClipboardHotkey )
  {
    try
    {
      hotkeyWrapper =  std::make_shared<HotkeyWrapper>( this );
    }
    catch( HotkeyWrapper::exInit & )
    {
      QMessageBox::critical( this, "GoldenDict",
        tr( "Failed to initialize hotkeys monitoring mechanism.<br>"
            "Make sure your XServer has RECORD extension turned on." ) );

      return;
    }

    if ( cfg.preferences.enableMainWindowHotkey )
      hotkeyWrapper->setGlobalKey( cfg.preferences.mainWindowHotkey,0 );

    if ( cfg.preferences.enableClipboardHotkey && !enableScanningAction->isChecked() )
    {
      hotkeyWrapper->setGlobalKey( cfg.preferences.clipboardHotkey,1 );
    }

    connect( hotkeyWrapper.get(),
      &HotkeyWrapper::hotkeyActivated,
      this,
      &MainWindow::hotKeyActivated,
      Qt::AutoConnection );
  }
}

void MainWindow::hotKeyActivated( int hk )
{
  if ( !hk )
    toggleMainWindow();
  else
  if ( scanPopup )
  {
#ifdef HAVE_X11
    // When the user requests translation with the Ctrl+C+C hotkey in certain apps
    // on some GNU/Linux systems, GoldenDict appears to handle Ctrl+C+C before the
    // active application finishes handling Ctrl+C. As a result, GoldenDict finds
    // the clipboard empty, silently cancels the translation request, and users report
    // that Ctrl+C+C is broken in these apps. Slightly delay handling the clipboard
    // hotkey to give the active application more time and thus work around the issue.
    QTimer::singleShot( 10, scanPopup, SLOT( translateWordFromClipboard() ) );
#else
    scanPopup->translateWordFromClipboard();
#endif
  }
}

void MainWindow::prepareNewReleaseChecks()
{
  if ( cfg.preferences.checkForNewReleases )
  {
    QDateTime now = QDateTime::currentDateTime();

    if ( !cfg.timeForNewReleaseCheck.isValid() ||
         now.daysTo( cfg.timeForNewReleaseCheck ) > 2 )
    {
      // The date is invalid, or the check is set to happen more than 2 days
      // in the future -- fix that.
      cfg.timeForNewReleaseCheck = now.addDays( 2 );
    }

    int secsToCheck = now.secsTo( cfg.timeForNewReleaseCheck );

    if ( secsToCheck < 1 )
      secsToCheck = 1;

    newReleaseCheckTimer.setSingleShot( true );
    newReleaseCheckTimer.start( secsToCheck * 1000 );
  }
  else
    newReleaseCheckTimer.stop(); // In case it was started before
}

void MainWindow::checkForNewRelease()
{
  if( latestReleaseReply )
  {
    disconnect( latestReleaseReply, 0, 0, 0 );
    latestReleaseReply->deleteLater();
  }
  latestReleaseReply = 0;

  QNetworkRequest req(
    QUrl( "https://github.com/xiaoyifang/goldendict/releases") );

  latestReleaseReply = articleNetMgr.get( req );

  connect( latestReleaseReply,
    &QNetworkReply::finished,
    this,
    &MainWindow::latestReleaseReplyReady,
    Qt::QueuedConnection );
}

void MainWindow::latestReleaseReplyReady()
{
  if ( !latestReleaseReply )
    return; // Some stray signal

  bool success = false;
  QString latestVersion, downloadUrl;

  // See if we succeeded

  if ( latestReleaseReply->error() == QNetworkReply::NoError )
  {
    QString latestReleaseInfo = QString::fromUtf8( latestReleaseReply->readAll() );
    QRegularExpression firstReleaseAnchor (R"(<a\s+[^>]*?class=\"Link--primary\"[^>]*?>[^<]*?<\/a>)",QRegularExpression::DotMatchesEverythingOption|QRegularExpression::CaseInsensitiveOption);
    auto match = firstReleaseAnchor.match(latestReleaseInfo);
    if(match.hasMatch()){
      auto releaseAnchor = match.captured();
      QRegularExpression extractReleaseRx (R"(<a\s+.*?href=\"([^\"]*)\".*?>(.*?)<\/a>)",QRegularExpression::DotMatchesEverythingOption|QRegularExpression::CaseInsensitiveOption);
      auto matchParts = extractReleaseRx.match(releaseAnchor);
      if(matchParts.hasMatch()){
        latestVersion = matchParts.captured(2);
        QString prefix("GoldenDict-ng-v");
        if(latestVersion.startsWith(prefix)){
          latestVersion = latestVersion.mid(prefix.length());
        }
        downloadUrl = matchParts.captured(1);
        if(downloadUrl.startsWith("/")){
          downloadUrl = latestReleaseReply->request().url().host() + downloadUrl;
        }
        success = true;
      }
    }
  }

  disconnect( latestReleaseReply, 0, 0, 0 );
  latestReleaseReply->deleteLater();
  latestReleaseReply = 0;

  if ( !success )
  {
    // Failed -- reschedule to check in two hours
    newReleaseCheckTimer.start( 2 * 60 * 60 * 1000 );

    GD_DPRINTF( "Failed to check program version, retry in two hours\n" );
  }
  else
  {
    // Success -- reschedule for a normal check and save config
    cfg.timeForNewReleaseCheck = QDateTime();

    prepareNewReleaseChecks();

    Config::save( cfg );

    GD_DPRINTF( "Program version's check successful, current version is %ls\n",
                latestVersion.toStdWString().c_str() );
  }

  if ( success && latestVersion > PROGRAM_VERSION && latestVersion != cfg.skippedRelease )
  {
    QMessageBox msg( QMessageBox::Information,
                     tr( "New Release Available" ),
                     tr( "Version <b>%1</b> of GoldenDict is now available for download.<br>"
                         "Click <b>Download</b> to get to the download page." ).arg( latestVersion ),
                     QMessageBox::NoButton,
                     this );

    QPushButton * dload = msg.addButton( tr( "Download" ), QMessageBox::AcceptRole );
    QPushButton * skip = msg.addButton( tr( "Skip This Release" ), QMessageBox::DestructiveRole );
    msg.addButton( QMessageBox::Cancel );

    msg.exec();

    if ( msg.clickedButton() == dload )
      QDesktopServices::openUrl( QUrl( downloadUrl ) );
    else
    if ( msg.clickedButton() == skip )
    {
      cfg.skippedRelease = latestVersion;
      Config::save( cfg );
    }
  }
}

void MainWindow::trayIconActivated( QSystemTrayIcon::ActivationReason r )
{
  switch(r) {
    case QSystemTrayIcon::Trigger:
      // Left click toggles the visibility of main window
#ifdef HAVE_X11
      toggleMainWindow( false, true );
#else
      toggleMainWindow();
#endif
      break;

    case QSystemTrayIcon::MiddleClick:
      // Middle mouse click on Tray translates selection
      // it is functional like as stardict
        scanPopup->translateWordFromSelection();
      break;
    default:
      break;

  }
}

void MainWindow::showMainWindow()
{
  toggleMainWindow( true );
}

void MainWindow::visitHomepage()
{
  QDesktopServices::openUrl( QApplication::organizationDomain() );
}

void MainWindow::openConfigFolder()
{
  QDesktopServices::openUrl( QUrl::fromLocalFile( Config::getConfigDir() ) );
}

void MainWindow::visitForum()
{
  QDesktopServices::openUrl( QUrl( "https://github.com/xiaoyifang/goldendict/discussions" ) );
}

void MainWindow::showAbout()
{
  About about(this, &dictionaries);

  about.show();
  about.exec();
}

void MainWindow::showDictBarNamesTriggered()
{
  bool show = showDictBarNamesAction.isChecked();

  dictionaryBar.setToolButtonStyle( show ? Qt::ToolButtonTextBesideIcon :
                                           Qt::ToolButtonIconOnly );
  cfg.showingDictBarNames = show;
}

void MainWindow::useSmallIconsInToolbarsTriggered()
{
  bool useSmallIcons = useSmallIconsInToolbarsAction.isChecked();

  int extent = useSmallIcons ? QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize) :
                               QApplication::style()->pixelMetric(QStyle::PM_ToolBarIconSize);

  navToolbar->setIconSize( QSize( extent, extent ) );

  // additional fix for #176
  menuButton->setIconSize( QSize( extent, extent ) );

  updateDictionaryBar();

  cfg.usingSmallIconsInToolbars = useSmallIcons;

  scanPopup->setDictionaryIconSize();
}

void MainWindow::toggleMenuBarTriggered(bool announce)
{
  cfg.preferences.hideMenubar = !toggleMenuBarAction.isChecked();

  if ( announce )
  {
    if ( cfg.preferences.hideMenubar )
    {
      mainStatusBar->showMessage(
            tr( "You have chosen to hide a menubar. Use %1 to show it back." )
            .arg( QString( "<b>%1</b>" ) ).arg( tr( "Ctrl+M" ) ),
            10000,
            QPixmap( ":/icons/warning.png" ) );
    }
    else
    {
      mainStatusBar->clearMessage();
    }
  }

  // Obtain from the menubar all the actions with shortcuts
  // and either add them to the main window or remove them,
  // depending on the menubar state.

  QList<QMenu *> allMenus = menuBar()->findChildren<QMenu *>();
  QListIterator<QMenu *> menuIter( allMenus );
  while( menuIter.hasNext() )
  {
    QMenu * menu = menuIter.next();
    QList<QAction *> allMenuActions = menu->actions();
    QListIterator<QAction *> actionsIter( allMenuActions );
    while( actionsIter.hasNext() )
    {
      QAction * action = actionsIter.next();
      if ( !action->shortcut().isEmpty() )
      {
        if ( cfg.preferences.hideMenubar )
        {
          // add all menubar actions to the main window,
          // before we hide the menubar
          addAction( action );
        }
        else
        {
          // remove all menubar actions from the main window
          removeAction( action );
        }
      }
    }
  }

  menuBar()->setVisible( !cfg.preferences.hideMenubar );
  beforeOptionsSeparator->setVisible( cfg.preferences.hideMenubar);
  menuButtonAction->setVisible( cfg.preferences.hideMenubar );
}

void MainWindow::on_clearHistory_triggered()
{
  history.clear();
  history.save();
}

void MainWindow::on_newTab_triggered()
{
  addNewTab();
}

void MainWindow::setAutostart(bool autostart)
{
#if defined Q_OS_WIN32
    QSettings reg("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
                  QSettings::NativeFormat);
    if (autostart) {
        QString app_fname = QString("\"%1\"").arg( QCoreApplication::applicationFilePath() );
        app_fname.replace("/", "\\");
        reg.setValue(QCoreApplication::applicationName(), app_fname);
    }
    else {
        reg.remove(QCoreApplication::applicationName());
    }
    reg.sync();
#elif defined HAVE_X11
  const QString destinationPath = QDir::homePath() + "/.config/autostart/goldendict-owned-by-preferences.desktop";
  if( autostart == QFile::exists( destinationPath ) )
    return; // Nothing to do.
  if( autostart )
  {
    const QString sourcePath = Config::getProgramDataDir() + "../applications/org.goldendict.GoldenDict.desktop";
    QFile::copy( sourcePath, destinationPath );
  }
  else
    QFile::remove( destinationPath );
#endif
}

void MainWindow::on_actionCloseToTray_triggered()
{
  toggleMainWindow( !cfg.preferences.enableTrayIcon );
}

void MainWindow::on_pageSetup_triggered()
{
  if ( getPrinter().isValid() )
  {
    QPageSetupDialog dialog( &getPrinter(), this );

    dialog.exec();
  }
  else
    QMessageBox::critical( this, tr( "Page Setup" ),
                           tr( "No printer is available. Please install one first." ) );
}

void MainWindow::on_printPreview_triggered()
{
  QPrinter printer;
  QPrintPreviewDialog dialog(  &printer, this );
  auto combox = dialog.findChild< QComboBox *>();
  int index=combox->findText( "100%" );
  combox->setCurrentIndex( index );

  connect( &dialog, &QPrintPreviewDialog::paintRequested, this, &MainWindow::printPreviewPaintRequested );

  dialog.showMaximized();
  dialog.exec();
}

void MainWindow::on_print_triggered()
{
  QPrintDialog dialog( &getPrinter(), this );

  dialog.setWindowTitle( tr( "Print Article") );

  if ( dialog.exec() != QDialog::Accepted )
   return;

  ArticleView *view = getCurrentArticleView();

  view->print( &getPrinter() );
}

void MainWindow::printPreviewPaintRequested( QPrinter * printer )
{
  ArticleView *view = getCurrentArticleView();

  view->print( printer );
}

static void filterAndCollectResources( QString & html, QRegExp & rx, const QString & sep,
                                       const QString & folder, set< QByteArray > & resourceIncluded,
                                       vector< pair< QUrl, QString > > & downloadResources )
{
  int pos = 0;

  while ( ( pos = rx.indexIn( html, pos ) ) != -1 )
  {
    QUrl url( rx.cap( 1 ) );
    QString host = url.host();
    QString resourcePath = Utils::Url::path( url );

    if ( !host.startsWith( '/' ) )
      host.insert( 0, '/' );
    if ( !resourcePath.startsWith( '/' ) )
      resourcePath.insert( 0, '/' );

    QCryptographicHash hash( QCryptographicHash::Md5 );
    hash.addData( rx.cap().toUtf8() );

    if ( resourceIncluded.insert( hash.result() ).second )
    {
      // Gather resource information (url, filename) to be download later
      downloadResources.push_back( pair<QUrl, QString>( url, folder + host + resourcePath ) );
    }

    // Modify original url, set to the native one
    resourcePath = QString::fromLatin1( QUrl::toPercentEncoding( resourcePath, "/" ) );
    QString newUrl = sep + QDir( folder ).dirName() + host + resourcePath + sep;
    html.replace( pos, rx.cap().length(), newUrl );
    pos += newUrl.length();
  }
}

void MainWindow::on_saveArticle_triggered()
{
  ArticleView * view = getCurrentArticleView();

  QString fileName = view->getTitle().simplified();

  // Replace reserved filename characters
  QRegularExpression rxName( R"([/\\\?\*:\|<>])" );
  fileName.replace( rxName, "_" );

  fileName += ".html";
  QString savePath;

  if( cfg.articleSavePath.isEmpty() )
    savePath = QDir::homePath();
  else
  {
    savePath = QDir::fromNativeSeparators( cfg.articleSavePath );
    if( !QDir( savePath ).exists() )
      savePath = QDir::homePath();
  }

  QFileDialog::Options options = QFileDialog::HideNameFilterDetails;
  QString selectedFilter;
  QStringList filters;
  filters.push_back( tr( "Article, Complete (*.html)" ) );
  filters.push_back( tr( "Article, HTML Only (*.html)" ) );

  fileName = savePath + "/" + fileName;
  fileName = QFileDialog::getSaveFileName( this,
                                           tr( "Save Article As" ),
                                           fileName,
                                           filters.join( ";;" ),
                                           &selectedFilter,
                                           options );

  // The " (*.html)" part of filters[i] is absent from selectedFilter in Qt 5.
  bool const complete = filters.at( 0 ).startsWith( selectedFilter );

  if( fileName.isEmpty() )
    return;

  view->toHtml(
    [ = ]( QString & html ) mutable
    {
      QFile file( fileName );
      if( !file.open( QIODevice::WriteOnly ) )
      {
        QMessageBox::critical( this, tr( "Error" ), tr( "Can't save article: %1" ).arg( file.errorString() ) );
      }
      else
      {
        QFileInfo fi( fileName );
        cfg.articleSavePath = QDir::toNativeSeparators( fi.absoluteDir().absolutePath() );

        // Convert internal links

        QRegExp rx3( "href=\"(bword:|gdlookup://localhost/)([^\"]+)\"" );
        int pos = 0;
        QRegularExpression anchorRx( "(g[0-9a-f]{32}_)[0-9a-f]+_" );
        while( ( pos = rx3.indexIn( html, pos ) ) != -1 )
        {
          QString name = QUrl::fromPercentEncoding( rx3.cap( 2 ).simplified().toLatin1() );
          QString anchor;
          name.replace( "?gdanchor=", "#" );
          int n = name.indexOf( '#' );
          if( n > 0 )
          {
            anchor = name.mid( n );
            name.truncate( n );
            anchor.replace( anchorRx, "\\1" ); // MDict anchors
          }
          name.replace( rxName, "_" );
          name = QString( "href=\"" ) + QUrl::toPercentEncoding( name ) + ".html" + anchor + "\"";
          html.replace( pos, rx3.cap().length(), name );
          pos += name.length();
        }

        // MDict anchors
        QRegularExpression anchorLinkRe(
          R"((<\s*a\s+[^>]*\b(?:name|id)\b\s*=\s*["']*g[0-9a-f]{32}_)([0-9a-f]+_)(?=[^"']))",
          QRegularExpression::PatternOption::CaseInsensitiveOption|QRegularExpression::UseUnicodePropertiesOption );
        html.replace( anchorLinkRe, "\\1" );

        if( complete )
        {
          QString folder = fi.absoluteDir().absolutePath() + "/" + fi.baseName() + "_files";
          QRegExp rx1( "\"((?:bres|gico|gdau|qrcx|gdvideo)://[^\"]+)\"" );
          QRegExp rx2( "'((?:bres|gico|gdau|qrcx|gdvideo)://[^']+)'" );
          set< QByteArray > resourceIncluded;
          vector< pair< QUrl, QString > > downloadResources;

          filterAndCollectResources( html, rx1, "\"", folder, resourceIncluded, downloadResources );
          filterAndCollectResources( html, rx2, "'", folder, resourceIncluded, downloadResources );

          ArticleSaveProgressDialog * progressDialog = new ArticleSaveProgressDialog( this );
          // reserve '1' for saving main html file
          int maxVal                                 = 1;

          // Pull and save resources to files
          for( vector< pair< QUrl, QString > >::const_iterator i = downloadResources.begin();
               i != downloadResources.end();
               ++i )
          {
            ResourceToSaveHandler * handler = view->saveResource( i->first, i->second );
            if( !handler->isEmpty() )
            {
              maxVal += 1;
              connect( handler, &ResourceToSaveHandler::done, progressDialog, &ArticleSaveProgressDialog::perform );
            }
          }

          progressDialog->setLabelText( tr( "Saving article..." ) );
          progressDialog->setRange( 0, maxVal );
          progressDialog->setValue( 0 );
          progressDialog->show();

          file.write( html.toUtf8() );
          progressDialog->perform();
        }
        else
        {
          file.write( html.toUtf8() );
        }
      }
    } );
}

void MainWindow::on_rescanFiles_triggered()
{
  hotkeyWrapper.reset(); // No hotkeys while we're editing dictionaries
  closeHeadwordsDialog();
  closeFullTextSearchDialog();

  ftsIndexing.stopIndexing();
  ftsIndexing.clearDictionaries();

  groupInstances.clear(); // Release all the dictionaries they hold
  dictionaries.clear();
  dictionariesUnmuted.clear();
  dictionaryBar.setDictionaries( dictionaries );

  loadDictionaries( this, true, cfg, dictionaries, dictNetMgr );
  dictMap = Dictionary::dictToMap(dictionaries);

  for( unsigned x = 0; x < dictionaries.size(); x++ )
  {
    dictionaries[ x ]->setFTSParameters( cfg.preferences.fts );
    dictionaries[ x ]->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
  }

  ftsIndexing.setDictionaries( dictionaries );
  ftsIndexing.doIndexing();

  updateGroupList();


  scanPopup->refresh();
  installHotKeys();

  updateSuggestionList();
}

void MainWindow::on_alwaysOnTop_triggered( bool checked )
{
    cfg.preferences.alwaysOnTop = checked;

    bool wasVisible = isVisible();

    Qt::WindowFlags flags = this->windowFlags();
    if (checked)
    {
        setWindowFlags(flags | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint);
        mainStatusBar->showMessage(
              tr( "The main window is set to be always on top." ),
              10000,
              QPixmap( ":/icons/warning.png" ) );
    }
    else
    {
        setWindowFlags(flags ^ (Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint));
        mainStatusBar->clearMessage();
    }

    if ( wasVisible )
    {
      show();
    }

    installHotKeys();
}

void MainWindow::zoomin()
{
  cfg.preferences.zoomFactor += 0.1;
  applyZoomFactor();
}

void MainWindow::zoomout()
{
  cfg.preferences.zoomFactor -= 0.1;
  applyZoomFactor();
}

void MainWindow::unzoom()
{
  cfg.preferences.zoomFactor = 1;
  applyZoomFactor();
}

void MainWindow::applyZoomFactor()
{
  // Always call this function synchronously to potentially disable a zoom action,
  // which is being repeatedly triggered. When the action is disabled, its
  // triggered() signal is no longer emitted, which in turn improves performance.
  adjustCurrentZoomFactor();

  // Scaling article views asynchronously dramatically improves performance when
  // a zoom action is triggered repeatedly while many or large articles are open
  // in the main window or in scan popup.
  // Multiple zoom action signals are processed before (often slow) article view
  // scaling is requested. Multiple scaling requests then ask for the same zoom factor,
  // so all of them except for the first one don't change anything and run very fast.
  // In effect, some intermediate zoom factors are skipped when scaling is slow.
  // The slower the scaling, the more steps are skipped.
  QTimer::singleShot( 0, this, &MainWindow::scaleArticlesByCurrentZoomFactor );
}

void MainWindow::adjustCurrentZoomFactor()
{
  if ( cfg.preferences.zoomFactor >= 5 )
    cfg.preferences.zoomFactor = 5;
  else if ( cfg.preferences.zoomFactor <= 0.1 )
    cfg.preferences.zoomFactor = 0.1;

  zoomIn->setEnabled( cfg.preferences.zoomFactor < 5 );
  zoomOut->setEnabled( cfg.preferences.zoomFactor > 0.1 );
  zoomBase->setEnabled( !qFuzzyCompare( cfg.preferences.zoomFactor, 1.0 ) );
}

void MainWindow::scaleArticlesByCurrentZoomFactor()
{
  for ( int i = 0; i < ui.tabWidget->count(); i++ )
  {
    ArticleView & view =
      dynamic_cast< ArticleView & >( *( ui.tabWidget->widget(i) ) );
    view.setZoomFactor( cfg.preferences.zoomFactor );
  }

  scanPopup->applyZoomFactor();
}

void MainWindow::doWordsZoomIn()
{
  ++cfg.preferences.wordsZoomLevel;

  applyWordsZoomLevel();
}

void MainWindow::doWordsZoomOut()
{
  --cfg.preferences.wordsZoomLevel;

  applyWordsZoomLevel();
}

void MainWindow::doWordsZoomBase()
{
  cfg.preferences.wordsZoomLevel = 0;

  applyWordsZoomLevel();
}

void MainWindow::applyWordsZoomLevel()
{
  QFont font( wordListDefaultFont );

  int ps = font.pointSize();

  if ( cfg.preferences.wordsZoomLevel != 0 )
  {
    ps += cfg.preferences.wordsZoomLevel;

    if ( ps < 1 )
      ps = 1;

    font.setPointSize( ps );
  }

  if ( wordList->font().pointSize() != ps )
    wordList->setFont( font );

  font = translateLineDefaultFont;

  ps = font.pointSize();

  if ( cfg.preferences.wordsZoomLevel != 0 )
  {
    ps += cfg.preferences.wordsZoomLevel;

    if ( ps < 1 )
      ps = 1;

    font.setPointSize( ps );
  }

  if ( translateLine->font().pointSize() != ps )
    translateLine->setFont( font );

  font = groupListDefaultFont;

  ps = font.pointSize();

  if ( cfg.preferences.wordsZoomLevel != 0 )
  {
    ps += cfg.preferences.wordsZoomLevel;

    if ( ps < 1 )
      ps = 1;

    font.setPointSize( ps );
  }

  if ( groupList->font().pointSize() != ps )
  {
    disconnect( groupList, &GroupComboBox::currentIndexChanged,
                this, &MainWindow::currentGroupChanged );
    int n = groupList->currentIndex();
    groupList->clear();
    groupList->setFont( font );
    groupList->fill( groupInstances );
    groupList->setCurrentIndex( n );
    connect( groupList, &GroupComboBox::currentIndexChanged,
                this, &MainWindow::currentGroupChanged );
  }

  wordsZoomBase->setEnabled( cfg.preferences.wordsZoomLevel != 0 );

  if( !cfg.preferences.searchInDock )
  {
    // Invalidating navToolbar's layout displays translateBoxWidget w/o the need to press the toolbar
    // extension button when Words Zoom level decreases enough for translateBoxWidget to fit in the toolbar.
    navToolbar->layout()->invalidate();
  }

  scanPopup->applyWordsZoomLevel();
}

void MainWindow::messageFromAnotherInstanceReceived( QString const & message )
{
  if ( message == "bringToFront" )
  {
    toggleMainWindow( true );
    return;
  }
  if( message.left( 15 ) == "translateWord: " )
  {
    if( scanPopup )
      scanPopup->translateWord( message.mid( 15 ) );
    else
      wordReceived( message.mid( 15 ) );
  }
  else
  if( message.left( 10 ) == "setGroup: " )
  {
    setGroupByName( message.mid( 10 ), true );
  }
  else
  if( message.left( 15 ) == "setPopupGroup: " )
  {
    setGroupByName( message.mid( 15 ), false );
  }
  else
    qWarning() << "Unknown message received from another instance: " << message;
}

ArticleView * MainWindow::getCurrentArticleView()
{
  if ( QWidget * cw = ui.tabWidget->currentWidget() )
  {
    return dynamic_cast< ArticleView * >( cw );
  }
  return 0;
}

void MainWindow::phraseReceived( Config::InputPhrase const & phrase )
{
  toggleMainWindow( true );
  setTranslateBoxTextAndKeepSuffix( phrase.phrase, EscapeWildcards, NoPopupChange );
  translateBoxSuffix = phrase.punctuationSuffix;
  respondToTranslationRequest( phrase, false );
}

void MainWindow::wordReceived( const QString & word)
{
  phraseReceived( Config::InputPhrase::fromPhrase( word ) );
}

void MainWindow::headwordReceived( const QString & word, const QString & ID )
{
  toggleMainWindow( true );
  setTranslateBoxTextAndClearSuffix( word, EscapeWildcards, NoPopupChange );
  respondToTranslationRequest( Config::InputPhrase::fromPhrase( word ),
                               false, ArticleView::scrollToFromDictionaryId( ID ) );
}

void MainWindow::updateFavoritesMenu()
{
  if ( ui.favoritesPane->isVisible() )
  {
    ui.showHideFavorites->setText( tr( "&Hide" ) );
  }
  else
  {
    ui.showHideFavorites->setText( tr( "&Show" ) );
  }
}

void MainWindow::updateHistoryMenu()
{
  if( ui.historyPane->isVisible() )
  {
    ui.showHideHistory->setText( tr( "&Hide" ) );
  }
  else
  {
    ui.showHideHistory->setText( tr( "&Show" ) );
  }
}

void MainWindow::toggle_favoritesPane()
{
  if( ui.favoritesPane->isVisible() )
  {
    ui.favoritesPane->hide();
  }
  else
  {
    ui.favoritesPane->show();
    ui.favoritesPane->raise(); // useful when the Pane is tabbed.
  }
}

void MainWindow::toggle_historyPane()
{
  if( ui.historyPane->isVisible() )
  {
    ui.historyPane->hide();
  }
  else
  {
    ui.historyPane->show();
    ui.historyPane->raise();
  }
}

void MainWindow::on_exportHistory_triggered()
{
    QString exportPath;
    if( cfg.historyExportPath.isEmpty() )
        exportPath = QDir::homePath();
    else
    {
        exportPath = QDir::fromNativeSeparators( cfg.historyExportPath );
        if( !QDir( exportPath ).exists() )
            exportPath = QDir::homePath();
    }

    QString fileName = QFileDialog::getSaveFileName( this, tr( "Export history to file" ),
                                                     exportPath,
                                                     tr( "Text files (*.txt);;All files (*.*)" ) );
    if( fileName.size() == 0)
        return;

    cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() );
    QFile file( fileName );

    for(;;)
    {
        if ( !file.open( QFile::WriteOnly | QIODevice::Text ) )
          break;

        // Write UTF-8 BOM
        QByteArray line;
        line.append( 0xEF ).append( 0xBB ).append( 0xBF );
        if ( file.write( line ) != line.size() )
          break;

        // Write history
        QList< History::Item > const & items = history.getItems();

        QList< History::Item >::const_iterator i;
        for( i = items.constBegin(); i != items.constEnd(); ++i )
        {
          line = i->word.toUtf8();

          line.replace( '\n', ' ' );
          line.replace( '\r', ' ' );

          line += "\n";

          if ( file.write( line ) != line.size() )
            break;
        }

        if( i != items.constEnd() )
          break;

        file.close();
        mainStatusBar->showMessage( tr( "History export complete" ), 5000 );
        return;
    }
    QString errStr = QString( tr( "Export error: " ) ) + file.errorString();
    file.close();
    mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
}

// TODO: consider moving parts of this method into History class.
void MainWindow::on_importHistory_triggered()
{
    QString importPath;
    if( cfg.historyExportPath.isEmpty() )
        importPath = QDir::homePath();
    else
    {
        importPath = QDir::fromNativeSeparators( cfg.historyExportPath );
        if( !QDir( importPath ).exists() )
            importPath = QDir::homePath();
    }

    QString fileName = QFileDialog::getOpenFileName( this, tr( "Import history from file" ),
                                                     importPath,
                                                     tr( "Text files (*.txt);;All files (*.*)" ) );
    if( fileName.size() == 0)
        return;

    QFileInfo fileInfo( fileName );
    cfg.historyExportPath = QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() );
    QString errStr;
    QFile file( fileName );

    for(;;)
    {
        if ( !file.open( QFile::ReadOnly | QIODevice::Text ) )
          break;

        QTextStream fileStream( & file );
        QString itemStr, trimmedStr;
        QList< QString > itemList;

        history.clear();

        do
        {
            itemStr = fileStream.readLine();
            if( fileStream.status() >= QTextStream::ReadCorruptData )
                break;

            trimmedStr = itemStr.trimmed();
            if( trimmedStr.isEmpty() )
                continue;

            if( (unsigned)trimmedStr.size() <= history.getMaxItemLength( ) )
                itemList.prepend( trimmedStr );

        } while( !fileStream.atEnd() && itemList.size() < (int)history.getMaxSize() );

        history.enableAdd( true );

        for( QList< QString >::const_iterator i = itemList.constBegin(); i != itemList.constEnd(); ++i )
            history.addItem( History::Item( 1, *i ) );

        history.enableAdd( cfg.preferences.storeHistory );

        if( file.error() != QFile::NoError )
            break;

        if( fileStream.status() >= QTextStream::ReadCorruptData )
        {
            errStr = QString ( tr( "Import error: invalid data in file" ) );
            mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
        }
        else
            mainStatusBar->showMessage( tr( "History import complete" ), 5000 );
        return;
    }
    errStr = QString( tr( "Import error: " ) ) + file.errorString();
    file.close();
    mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
}

void MainWindow::on_exportFavorites_triggered()
{
  QString exportPath;
  if( cfg.historyExportPath.isEmpty() )
    exportPath = QDir::homePath();
  else
  {
    exportPath = QDir::fromNativeSeparators( cfg.historyExportPath );
    if( !QDir( exportPath ).exists() )
      exportPath = QDir::homePath();
  }

  QString fileName = QFileDialog::getSaveFileName( this, tr( "Export Favorites to file" ),
                                                   exportPath,
                                                   tr( "XML files (*.xml);;All files (*.*)" ) );
  if( fileName.size() == 0)
    return;

  cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() );
  QFile file( fileName );

  for(;;)
  {
    if ( !file.open( QFile::WriteOnly | QIODevice::Text ) )
      break;

    QByteArray data;
    ui.favoritesPaneWidget->getDataInXml( data );

    if( file.write( data ) != data.size() )
      break;

    file.close();
    mainStatusBar->showMessage( tr( "Favorites export complete" ), 5000 );
    return;
  }
  QString errStr = QString( tr( "Export error: " ) ) + file.errorString();
  file.close();
  mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
}

void MainWindow::on_ExportFavoritesToList_triggered()
{
  QString exportPath;
  if( cfg.historyExportPath.isEmpty() )
    exportPath = QDir::homePath();
  else
  {
    exportPath = QDir::fromNativeSeparators( cfg.historyExportPath );
    if( !QDir( exportPath ).exists() )
      exportPath = QDir::homePath();
  }

  QString fileName = QFileDialog::getSaveFileName( this, tr( "Export Favorites to file as plain list" ),
                                                   exportPath,
                                                   tr( "Text files (*.txt);;All files (*.*)" ) );
  if( fileName.size() == 0)
    return;

  cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() );
  QFile file( fileName );

  for(;;)
  {
    if ( !file.open( QFile::WriteOnly | QIODevice::Text ) )
      break;

    // Write UTF-8 BOM
    QByteArray line;
    line.append( 0xEF ).append( 0xBB ).append( 0xBF );
    if ( file.write( line ) != line.size() )
      break;

    // Write Favorites
    QString data;
    ui.favoritesPaneWidget->getDataInPlainText( data );

    line = data.toUtf8();
    if( file.write( line ) != line.size() )
      break;

    file.close();
    mainStatusBar->showMessage( tr( "Favorites export complete" ), 5000 );
    return;
  }
  QString errStr = QString( tr( "Export error: " ) ) + file.errorString();
  file.close();
  mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
}

void MainWindow::on_importFavorites_triggered()
{
  QString importPath;
  if( cfg.historyExportPath.isEmpty() )
    importPath = QDir::homePath();
  else
  {
    importPath = QDir::fromNativeSeparators( cfg.historyExportPath );
    if( !QDir( importPath ).exists() )
      importPath = QDir::homePath();
  }

  QString fileName = QFileDialog::getOpenFileName( this, tr( "Import Favorites from file" ),
                                                   importPath,
                                                   tr( "XML files (*.xml);;Txt files (*.txt);;All files (*.*)" ) );
  if( fileName.size() == 0)
    return;

  QFileInfo fileInfo( fileName );
  cfg.historyExportPath = QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() );
  QString errStr;
  QFile file( fileName );

  for(;;)
  {
    if ( !file.open( QFile::ReadOnly | QIODevice::Text ) )
      break;

    if( file.error() != QFile::NoError )
        break;

    QByteArray data = file.readAll();

    if(fileInfo.suffix().compare("txt",Qt::CaseInsensitive)==0){
      if( !ui.favoritesPaneWidget->setDataFromTxt( QString::fromUtf8( data.data(), data.size() ) ) )
        break;
    }
    else{
      if( !ui.favoritesPaneWidget->setDataFromXml( QString::fromUtf8( data.data(), data.size() ) ) )
        break;
    }

    file.close();
    mainStatusBar->showMessage( tr( "Favorites import complete" ), 5000 );
    return;
  }
  if( file.error() != QFile::NoError )
    errStr = QString( tr( "Import error: " ) ) + file.errorString();
  else
    errStr = QString( tr( "Data parsing error" ) );

  file.close();
  mainStatusBar->showMessage( errStr, 10000, QPixmap( ":/icons/error.svg" ) );
}

void MainWindow::fillWordListFromHistory()
{
    ui.wordList->setUpdatesEnabled( false );
    ui.wordList->clear();

    QList< History::Item > const & items = history.getItems();
    for( int x = 0; x < items.size(); ++x )
    {
      History::Item const * i = &items[ x ];
      QListWidgetItem * s = new QListWidgetItem( i->word, ui.wordList );
      if (s->text().at(0).direction() == QChar::DirR)
          s->setTextAlignment(Qt::AlignRight);
      if (s->text().at(0).direction() == QChar::DirL)
          s->setTextAlignment(Qt::AlignLeft);
      ui.wordList->addItem( s );
    }

    ui.wordList->setUpdatesEnabled( true );
}

void MainWindow::focusWordList()
{
    if( ui.wordList->count() > 0 )
        ui.wordList->setFocus();
}

void MainWindow::addWordToHistory( const QString & word )
{
    if(QRegularExpressionMatch m = RX::Epwing::refWord.match( word ); m.hasMatch() )
        return;
    history.addItem( History::Item( 1, word.trimmed() ) );
}

void MainWindow::forceAddWordToHistory( const QString & word )
{
    history.enableAdd( true );
    history.addItem( History::Item( 1, word.trimmed() ) );
    history.enableAdd( cfg.preferences.storeHistory );
}

void MainWindow::foundDictsPaneClicked( QListWidgetItem * item )
{
  Qt::KeyboardModifiers m = QApplication::keyboardModifiers();
  if ( ( m & ( Qt::ControlModifier | Qt::ShiftModifier ) )
       || ( m == Qt::AltModifier ) )
  {
    QString id = item->data( Qt::UserRole ).toString();
    emit clickOnDictPane( id );
  }

   jumpToDictionary( item,true);
}

void MainWindow::showDictionaryInfo( const QString & id )
{
  for( unsigned x = 0; x < dictionaries.size(); x++ )
  {
    if( dictionaries[ x ]->getId() == id.toUtf8().data() )
    {
      DictInfo infoMsg( cfg, this );
      infoMsg.showInfo( dictionaries[ x ] );
      int result = infoMsg.exec();

      if ( result == DictInfo::OPEN_FOLDER )
      {
        openDictionaryFolder( id );
      }
      else if ( result == DictInfo::EDIT_DICTIONARY)
      {
        editDictionary( dictionaries[x].get() );
      }
      else if( result == DictInfo::SHOW_HEADWORDS )
      {
        showDictionaryHeadwords( dictionaries[x].get() );
      }

      break;
    }
  }
}

void MainWindow::showDictionaryHeadwords( Dictionary::Class * dict )
{

  QWidget * owner = qobject_cast< QWidget * >( sender() );

  // DictHeadwords internally check parent== mainwindow to know why it is requested.
  // If the DictHeadwords is requested by Edit->Dictionaries->ShowHeadWords, (owner = "EditDictionaries")
  // it will be a modal dialog. When click a word, that word will NOT be queried.
  // In all other cases, just set owner = mainwindow(this),

  if ( owner->objectName() == "EditDictionaries" ) {
    DictHeadwords headwords( owner, cfg, dict );
    headwords.exec();
  }
  else {
    if ( headwordsDlg == nullptr ) {
      headwordsDlg = new DictHeadwords( this, cfg, dict );
      addGlobalActionsToDialog( headwordsDlg );
      addGroupComboBoxActionsToDialog( headwordsDlg, groupList );
      connect( headwordsDlg, &DictHeadwords::headwordSelected, this, &MainWindow::headwordReceived );
      connect( headwordsDlg, &DictHeadwords::closeDialog,
               this, &MainWindow::closeHeadwordsDialog, Qt::QueuedConnection );
    }
    else{
      headwordsDlg->setup( dict );
    }
    headwordsDlg->show();
  }
}


void MainWindow::closeHeadwordsDialog()
{
  if( headwordsDlg )
  {
    delete headwordsDlg;
    headwordsDlg = NULL;
  }
}

void MainWindow::focusHeadwordsDialog()
{
  if( headwordsDlg )
  {
    headwordsDlg->activateWindow();
    if ( ftsDlg )
      ftsDlg->lower();
  }
}

void MainWindow::focusArticleView()
{
  ArticleView * view = getCurrentArticleView();
  if ( view )
  {
    if ( !isActiveWindow() )
      activateWindow();
    view->focus();
  }
}

void MainWindow::editDictionary( Dictionary::Class * dict )
{
  QString dictFilename = dict->getMainFilename();
  if( !cfg.editDictionaryCommandLine.isEmpty() && !dictFilename.isEmpty() )
  {
    QString command( cfg.editDictionaryCommandLine );
    command.replace( "%GDDICT%", "\"" + dictFilename + "\"" );
    if( command.contains( "%GDWORD%" ) )
    {
      QString headword = unescapeTabHeader( ui.tabWidget->tabText( ui.tabWidget->currentIndex() ) );
      command.replace( "%GDWORD%", headword );
    }
    if( !QProcess::startDetached( command,QStringList() ) )
      QApplication::beep();
  }
}

void MainWindow::openDictionaryFolder( const QString & id )
{
  for( unsigned x = 0; x < dictionaries.size(); x++ )
  {
    if( dictionaries[ x ]->getId() == id.toUtf8().data() )
    {
      if ( !dictionaries[ x ]->getDictionaryFilenames().empty() ) {
        QDesktopServices::openUrl( QUrl::fromLocalFile( dictionaries[ x ]->getContainingFolder() ) );
      }
      break;
    }
  }
}

void MainWindow::foundDictsContextMenuRequested( const QPoint &pos )
{
  QListWidgetItem *item = ui.dictsList->itemAt( pos );
  if( item )
  {
    QString id = item->data( Qt::UserRole ).toString();
    Dictionary::Class *pDict = NULL;

    for( unsigned i = 0; i < dictionaries.size(); i++ )
    {
      if( id.compare( dictionaries[ i ]->getId().c_str() ) == 0 )
      {
        pDict = dictionaries[ i ].get();
        break;
      }
    }

    if( pDict == NULL )
      return;

    if( !pDict->isLocalDictionary() )
    {
      if ( scanPopup )
        scanPopup->blockSignals( true );
      showDictionaryInfo( id );
      if ( scanPopup )
        scanPopup->blockSignals( false );
    }
    else
    {
      QMenu menu( ui.dictsList );
      QAction * infoAction = menu.addAction( tr( "Dictionary info" ) );

      QAction * headwordsAction = NULL;
      if( pDict->getWordCount() > 0 )
        headwordsAction = menu.addAction( tr( "Dictionary headwords" ) );

      QAction * openDictFolderAction = menu.addAction( tr( "Open dictionary folder" ) );

      QAction * editAction = NULL;

      QString dictFilename = pDict->getMainFilename();
      if( !cfg.editDictionaryCommandLine.isEmpty() && !dictFilename.isEmpty() )
        editAction = menu.addAction( tr( "Edit dictionary" ) );

      QAction * result = menu.exec( ui.dictsList->mapToGlobal( pos ) );

      if( result && result == infoAction )
      {
        if ( scanPopup )
          scanPopup->blockSignals( true );
        showDictionaryInfo( id );
        if ( scanPopup )
          scanPopup->blockSignals( false );
      }
      else
      if( result && result == headwordsAction )
      {
        if ( scanPopup )
          scanPopup->blockSignals( true );
        showDictionaryHeadwords( pDict );
        if ( scanPopup )
          scanPopup->blockSignals( false );
      }
      else
      if( result && result == openDictFolderAction )
      {
        openDictionaryFolder( id );
      }
      else
      if( result && result == editAction )
      {
        editDictionary( pDict );
      }
    }
  }
}

void MainWindow::sendWordToInputLine( const QString & word )
{
  setTranslateBoxTextAndClearSuffix( word, EscapeWildcards, NoPopupChange );
}

void MainWindow::storeResourceSavePath( const QString & newPath )
{
  cfg.resourceSavePath = newPath;
}

void MainWindow::proxyAuthentication( const QNetworkProxy &,
                                      QAuthenticator * authenticator )
{
  QNetworkProxy proxy = QNetworkProxy::applicationProxy();

  QString * userStr, * passwordStr;
  if( cfg.preferences.proxyServer.useSystemProxy )
  {
    userStr = &cfg.preferences.proxyServer.systemProxyUser;
    passwordStr = &cfg.preferences.proxyServer.systemProxyPassword;
  }
  else
  {
    userStr = &cfg.preferences.proxyServer.user;
    passwordStr = &cfg.preferences.proxyServer.password;
  }

  if( proxy.user().isEmpty() && !userStr->isEmpty() )
  {
    authenticator->setUser( *userStr );
    authenticator->setPassword( *passwordStr );

    proxy.setUser( *userStr );
    proxy.setPassword( *passwordStr );
    QNetworkProxy::setApplicationProxy( proxy );
  }
  else
  {
    QDialog dlg;
    Ui::Dialog ui;
    ui.setupUi( &dlg );
    dlg.adjustSize();

    ui.userEdit->setText( *userStr );
    ui.passwordEdit->setText( *passwordStr );

    if ( dlg.exec() == QDialog::Accepted )
    {
      *userStr = ui.userEdit->text();
      *passwordStr = ui.passwordEdit->text();

      authenticator->setUser( *userStr );
      authenticator->setPassword( *passwordStr );

      proxy.setUser( *userStr );
      proxy.setPassword( *passwordStr );
      QNetworkProxy::setApplicationProxy( proxy );
    }
  }
}

void MainWindow::showFullTextSearchDialog()
{
  if( !ftsDlg )
  {
    ftsDlg = new FTS::FullTextSearchDialog( this, cfg, dictionaries, groupInstances, ftsIndexing );
    addGlobalActionsToDialog( ftsDlg );
    addGroupComboBoxActionsToDialog( ftsDlg, groupList );

    connect( ftsDlg, SIGNAL( showTranslationFor( QString, QStringList, QRegExp, bool ) ),
             this, SLOT( showTranslationFor( QString, QStringList, QRegExp, bool ) ) );
    connect( ftsDlg,
      &FTS::FullTextSearchDialog::closeDialog,
      this,
      &MainWindow::closeFullTextSearchDialog,
      Qt::QueuedConnection );
    connect( &configEvents, SIGNAL( mutedDictionariesChanged() ),
             ftsDlg, SLOT( updateDictionaries() ) );

    unsigned group = groupInstances.empty() ? 0
                                            : groupInstances[ groupList->currentIndex() ].id;
    ftsDlg->setCurrentGroup( group );
  }

  if( !ftsDlg ->isVisible() )
    ftsDlg->show();
  else
  {
    ftsDlg->activateWindow();
    if ( headwordsDlg )
      headwordsDlg->lower();
  }
}

void MainWindow::closeFullTextSearchDialog()
{
  if( ftsDlg )
  {
    ftsDlg->stopSearch();
    delete ftsDlg;
    ftsDlg = 0;
  }
}

void MainWindow::showFTSIndexingName( QString const & name )
{
  if( name.isEmpty() )
    mainStatusBar->setBackgroundMessage( QString() );
  else
    mainStatusBar->setBackgroundMessage( tr( "Now indexing for full-text search: " ) + name );
}

QString MainWindow::unescapeTabHeader(QString const & header )
{
  // Reset table header to original headword

  QString escaped = header;
  escaped.replace( "&&", "&" );
  if( escaped.startsWith( QChar( 0x202E ) ) )
    escaped = escaped.mid( 1 );
  if( escaped.endsWith( QChar( 0x202C ) ) )
    escaped.chop( 1 );

  return escaped;
}

void MainWindow::addCurrentTabToFavorites()
{
  QString folder;
  Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if( igrp )
    folder = igrp->favoritesFolder;

  QString headword = ui.tabWidget->tabText( ui.tabWidget->currentIndex() );

  ui.favoritesPaneWidget->addHeadword( folder, unescapeTabHeader( headword ) );

  addToFavorites->setIcon( blueStarIcon );
  addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
}

void MainWindow::handleAddToFavoritesButton()
{
  QString folder;
  Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if( igrp )
    folder = igrp->favoritesFolder;
  QString headword = unescapeTabHeader( ui.tabWidget->tabText( ui.tabWidget->currentIndex() ) );

  if( ui.favoritesPaneWidget->isHeadwordPresent( folder, headword ) )
  {
    QMessageBox mb( QMessageBox::Question, "GoldenDict", tr( "Remove headword \"%1\" from Favorites?" ).arg( headword ),
                    QMessageBox::Yes | QMessageBox::No, this );
    if( mb.exec() == QMessageBox::Yes )
    {
      if( ui.favoritesPaneWidget->removeHeadword( folder, headword ) )
      {
        addToFavorites->setIcon( starIcon );
        addToFavorites->setToolTip( tr( "Add current tab to Favorites" ) );
      }
    }
  }
  else
  {
    ui.favoritesPaneWidget->addHeadword( folder, headword );
    addToFavorites->setIcon( blueStarIcon );
    addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
  }
}

void MainWindow::addWordToFavorites( QString const & word, unsigned groupId )
{
  QString folder;
  Instances::Group const * igrp = groupInstances.findGroup( groupId );
  if( igrp )
    folder = igrp->favoritesFolder;

  ui.favoritesPaneWidget->addHeadword( folder, word );
}

void MainWindow::addBookmarkToFavorite( QString const & text )
{
  // get current tab word.
  QString word        = unescapeTabHeader( ui.tabWidget->tabText( ui.tabWidget->currentIndex() ) );
  const auto bookmark = QString( "%1~~~%2" ).arg( word, text );

  ui.favoritesPaneWidget->addHeadword( nullptr, bookmark );
}

void MainWindow::addAllTabsToFavorites()
{
  QString folder;
  Instances::Group const * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if( igrp )
    folder = igrp->favoritesFolder;

  for( int i = 0; i < ui.tabWidget->count(); i++ )
  {
    QString headword = ui.tabWidget->tabText( i );
    ui.favoritesPaneWidget->addHeadword( folder, unescapeTabHeader( headword ) );
  }
  addToFavorites->setIcon( blueStarIcon );
  addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
}

bool MainWindow::isWordPresentedInFavorites( QString const & word, unsigned groupId )
{
  QString folder;
  Instances::Group const * igrp = groupInstances.findGroup( groupId );
  if( igrp )
    folder = igrp->favoritesFolder;

  return ui.favoritesPaneWidget->isHeadwordPresent( folder, word );
}

void MainWindow::setGroupByName( QString const & name, bool main_window )
{
  if( main_window )
  {
    int i;
    for( i = 0; i < groupList->count(); i++ )
    {
      if( groupList->itemText( i ) == name )
      {
        groupList->setCurrentIndex( i );
        break;
      }
    }
    if( i >= groupList->count() )
      gdWarning( "Group \"%s\" for main window is not found\n", name.toUtf8().data() );
  }
  else
  {
    emit setPopupGroupByName( name );
  }
}

void MainWindow::headwordFromFavorites( QString const & headword,
                                        QString const & favoritesFolder )
{
  if( !favoritesFolder.isEmpty() )
  {
    // Find group by it Favorites folder
    for( Instances::Groups::size_type i = 0; i < groupInstances.size(); i++ )
    {
      if( groupInstances[ i ].favoritesFolder == favoritesFolder )
      {
        // Group found. Select it and stop search.
        if( groupList->currentIndex() != (int)i )
        {
          groupList->setCurrentIndex( i );

          // Restore focus on Favorites tree
          ui.favoritesPaneWidget->setFocusOnTree();
        }
        break;
      }
    }
  }

  // Show headword without lost of focus on Favorites tree
  // bookmark cases:   the favorite item may like this   "word~~~selectedtext"
  auto words = headword.split( "~~~" );

  setTranslateBoxTextAndClearSuffix( words[0], EscapeWildcards, DisablePopup );

  //must be a bookmark.
  if(words.size()>1)
  {
    auto view = getCurrentArticleView();
    if(view)
    {
      view->setDelayedHighlightText(words[1]);// findText( words[ 1 ], QWebEnginePage::FindCaseSensitively );
    }
  }

  showTranslationFor( words[ 0 ] );
}