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

#ifdef EPWING_SUPPORT
  #include "dict/epwing_book.hh"
#endif

#include "mainwindow.hh"
#include "logger.hh"
#include <QWebEngineProfile>
#include "edit_dictionaries.hh"
#include "dict/loaddictionaries.hh"
#include "preferences.hh"
#include "globalregex.hh"
#include "about.hh"
#include "mruqmenu.hh"
#include "gestures.hh"
#include "dictheadwords.hh"
#include <QTextStream>
#include <QDir>
#include <QUrl>
#include <QMessageBox>
#include <QIcon>
#include <QPixmap>
#include <QList>
#include <QToolBar>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QCryptographicHash>
#include <QFileDialog>
#include <QPrinter>
#include <QPageSetupDialog>
#include <QPrintPreviewDialog>
#include <QPrintDialog>
#include <QRunnable>
#include <QThreadPool>
#include <QSslConfiguration>
#include <QStyleFactory>
#include <QStyleHints>
#include <QNetworkProxyFactory>

#include "weburlrequestinterceptor.hh"
#include "folding.hh"
#include "articlesaver.hh"

#include <set>
#include <map>

#include "dictinfo.hh"
#include "historypanewidget.hh"
#include "utils.hh"
#include "help.hh"
#include "resourceschemehandler.hh"
#include <QListWidgetItem>
#include "config.hh"
#include "globalregex.hh"

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

#ifdef Q_OS_WIN32
  #include <windows.h>
#endif

#include <QGuiApplication>
#include <QWebEngineSettings>
#include <QProxyStyle>

#ifdef WITH_X11
  #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

namespace {
QString ApplicationSettingName = "GoldenDict";
}

void MainWindow::changeWebEngineViewFont() const
{
  if ( cfg.preferences.customFonts.standard.isEmpty() ) {
    QWebEngineProfile::defaultProfile()->settings()->resetFontFamily( QWebEngineSettings::StandardFont );
  }
  else {
    QWebEngineProfile::defaultProfile()->settings()->setFontFamily( QWebEngineSettings::StandardFont,
                                                                    cfg.preferences.customFonts.standard );
  }

  if ( cfg.preferences.customFonts.serif.isEmpty() ) {
    QWebEngineProfile::defaultProfile()->settings()->resetFontFamily( QWebEngineSettings::SerifFont );
  }
  else {
    QWebEngineProfile::defaultProfile()->settings()->setFontFamily( QWebEngineSettings::SerifFont,
                                                                    cfg.preferences.customFonts.serif );
  }

  if ( cfg.preferences.customFonts.sansSerif.isEmpty() ) {
    QWebEngineProfile::defaultProfile()->settings()->resetFontFamily( QWebEngineSettings::SansSerifFont );
  }
  else {
    QWebEngineProfile::defaultProfile()->settings()->setFontFamily( QWebEngineSettings::SansSerifFont,
                                                                    cfg.preferences.customFonts.sansSerif );
  }

  if ( cfg.preferences.customFonts.monospace.isEmpty() ) {
    QWebEngineProfile::defaultProfile()->settings()->resetFontFamily( QWebEngineSettings::FixedFont );
  }
  else {
    QWebEngineProfile::defaultProfile()->settings()->setFontFamily( QWebEngineSettings::FixedFont,
                                                                    cfg.preferences.customFonts.monospace );
  }
}

MainWindow::MainWindow( Config::Class & cfg_ ):
  trayIcon( nullptr ),
  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 ),
  toggleMenuBarAction( tr( "&Menubar" ), this ),
  lockPanelsAction( tr( "Lock Panels" ), this ),
  focusHeadwordsDlgAction( this ),
  focusArticleViewAction( this ),
  addAllTabToFavoritesAction( this ),
  useSmallIconsInToolbarsAction( tr( "Show &Small Icons in Toolbars" ), this ),
  useLargeIconsInToolbarsAction( tr( "Show &Large Icons in Toolbars" ), this ),
  useNormalIconsInToolbarsAction( tr( "Show &Normal Icons in Toolbars" ), this ),
  stopAudioAction( this ),
  trayIconMenu( this ),
  addTab( this ),
  cfg( cfg_ ),
  history( cfg_.preferences.maxStringsInHistory, cfg_.maxHeadwordSize ),
  dictionaryBar( this, cfg.preferences.maxDictionaryRefsInContextMenu ),
  articleMaker( dictionaries, groupInstances, cfg.preferences ),
  articleNetMgr( this, dictionaries, articleMaker, cfg.preferences.disallowContentFromOtherSites ),
  dictNetMgr( this ),
  audioPlayerFactory(
    cfg.preferences.useInternalPlayer, cfg.preferences.internalPlayerBackend, cfg.preferences.audioPlaybackProgram ),
  wordFinder( this ),
  wordListSelChanged( false ),
  wasMaximized( false ),
  headwordsDlg( nullptr ),
  ftsIndexing( dictionaries ),
  ftsDlg( nullptr ),
  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()->setConfig( &cfg );
  GlobalBroadcaster::instance()->setAudioPlayer( &audioPlayerFactory.player() );
  GlobalBroadcaster::instance()->setAllDictionaries( &dictionaries );
  GlobalBroadcaster::instance()->setGroups( &groupInstances );

  localSchemeHandler     = new LocalSchemeHandler( articleNetMgr, this );
  QStringList htmlScheme = { "gdlookup", "bword", "entry", "gdinternal" };
  for ( const auto & localScheme : htmlScheme ) {
    QWebEngineProfile::defaultProfile()->installUrlSchemeHandler( localScheme.toLatin1(), localSchemeHandler );
  }


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

  QWebEngineProfile::defaultProfile()->setUrlRequestInterceptor( new WebUrlRequestInterceptor( this ) );
  connect( QWebEngineProfile::defaultProfile(),
           &QWebEngineProfile::downloadRequested,
           this,
           &MainWindow::handleDownloadRequested );
  // Identify as GoldenDict, but avoid standard "QtWebEngine/..." identifier which some sites might block
  QString userAgent = QWebEngineProfile::defaultProfile()->httpUserAgent();
  userAgent.replace( RX::qtWebEngineUserAgent, "" );
  QWebEngineProfile::defaultProfile()->setHttpUserAgent( userAgent );
#ifdef 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( nullptr );
  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->setObjectName( "groupListToolbar" );
  groupListInToolbar->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::MinimumExpanding );
  groupListInToolbar->setSizeAdjustPolicy( QComboBox::AdjustToContents );
  translateBoxLayout->addWidget( groupListInToolbar, 0 );

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

  // popup
  navToolbar->addSeparator();

  enableScanningAction = navToolbar->addAction( QIcon( ":/icons/wizard.svg" ), tr( "Toggle clipboard monitoring" ) );
  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, &QAction::triggered, this, [ this ]() {
    getCurrentArticleView()->playSound();
  } );

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

  ui.rescanFiles->setShortcuts( QList< QKeySequence >() << QKeySequence( "Ctrl+F5" ) << QKeySequence( "F5" ) );

  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( true );

  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( 3, 5, 3, 5 );
  //  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;
  }
  else {
    groupList     = groupListInToolbar;
    translateLine = translateBox->translateLine();
  }
  connect( &wordFinder, &WordFinder::updated, this, &MainWindow::prefixMatchUpdated );
  connect( &wordFinder, &WordFinder::finished, this, &MainWindow::prefixMatchFinished );


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

  wordListDefaultFont      = ui.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, &QDockWidget::visibilityChanged, this, &MainWindow::dictsPaneVisibilityChanged );

  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();

// tray icon
#ifndef Q_OS_MACOS // macOS uses the dock menu instead of the tray icon
  connect( trayIconMenu.addAction( tr( "Show &Main Window" ) ), &QAction::triggered, this, [ this ] {
    this->toggleMainWindow( true );
  } );
#endif
  trayIconMenu.addAction( enableScanningAction );

#ifndef Q_OS_MACOS // macOS uses the dock menu instead of the tray icon
  trayIconMenu.addSeparator();
  connect( trayIconMenu.addAction( tr( "&Quit" ) ), &QAction::triggered, this, &MainWindow::quitApp );
#endif

  addGlobalAction( &escAction, [ this ]() {
    handleEsc();
  } );
  escAction.setShortcut( QKeySequence( "Esc" ) );

  addGlobalAction( &focusTranslateLineAction, [ this ]() {
    focusTranslateLine();
  } );
  focusTranslateLineAction.setShortcuts( QList< QKeySequence >()
                                         << QKeySequence( "Alt+D" ) << QKeySequence( "Ctrl+L" ) );

  addGlobalAction( &focusHeadwordsDlgAction, [ this ]() {
    focusHeadwordsDialog();
  } );
  focusHeadwordsDlgAction.setShortcut( QKeySequence( "Ctrl+D" ) );

  addGlobalAction( &focusArticleViewAction, [ this ]() {
    focusArticleView();
  } );
  focusArticleViewAction.setShortcut( QKeySequence( "Ctrl+N" ) );

  addGlobalAction( ui.fullTextSearchAction, [ this ]() {
    showFullTextSearchDialog();
  } );

  addGlobalAction( &stopAudioAction, [ this ]() {
    stopAudio();
  } );
  stopAudioAction.setShortcut( QKeySequence( "Ctrl+Shift+S" ) );

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

  useSmallIconsInToolbarsAction.setCheckable( true );
  useSmallIconsInToolbarsAction.setChecked( cfg.usingToolbarsIconSize == Config::ToolbarsIconSize::Small );
  useLargeIconsInToolbarsAction.setCheckable( true );
  useLargeIconsInToolbarsAction.setChecked( cfg.usingToolbarsIconSize == Config::ToolbarsIconSize::Large );
  useNormalIconsInToolbarsAction.setCheckable( true );
  useNormalIconsInToolbarsAction.setChecked( cfg.usingToolbarsIconSize == Config::ToolbarsIconSize::Normal );

  // icon action group,default exclusive option.
  smallLargeIconGroup->addAction( &useLargeIconsInToolbarsAction );
  smallLargeIconGroup->addAction( &useSmallIconsInToolbarsAction );
  smallLargeIconGroup->addAction( &useNormalIconsInToolbarsAction );

  connect( smallLargeIconGroup, &QActionGroup::triggered, this, &MainWindow::iconSizeActionTriggered );

  // 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->addSeparator();
  ui.menuView->addAction( &useSmallIconsInToolbarsAction );
  ui.menuView->addAction( &useNormalIconsInToolbarsAction );
  ui.menuView->addAction( &useLargeIconsInToolbarsAction );
  ui.menuView->addSeparator();
  ui.alwaysOnTop->setChecked( cfg.preferences.alwaysOnTop );
  lockPanelsAction.setCheckable( true );
  lockPanelsAction.setChecked( cfg.preferences.panelsLocked );
  connect( &lockPanelsAction, &QAction::toggled, this, &MainWindow::onLockPanelsToggled );
  ui.menuView->addAction( &lockPanelsAction );
  ui.menuView->addAction( ui.alwaysOnTop );

  // Dictionary bar

  const Instances::Group * igrp = groupInstances.findGroup( cfg.lastMainGroupId );
  if ( cfg.lastMainGroupId == GroupId::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 : nullptr );
  }
  GlobalBroadcaster::instance()->currentGroupId = cfg.lastMainGroupId;

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

  connect( this, &MainWindow::clickOnDictPane, &dictionaryBar, &DictionaryBar::dictsPaneClicked );

  addToolBarBreak();
  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 );
  // Use lambda to adapt the signal (2 parameters) to the slot (3 parameters with default)
  connect( &dictionaryBar,
           &DictionaryBar::showStatusBarMessage,
           this,
           [ this ]( const QString & message, int timeout ) {
             showStatusBarMessage( message, timeout );
           } );

  // Favorites

  ui.favoritesPaneWidget->setUp( &cfg, { ui.showHideFavorites, ui.importFavorites, ui.exportFavorites } );
  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 );

  connect( ui.favoritesPaneWidget, &FavoritesPaneWidget::activeFavChange, this, &MainWindow::updateFavIconSlot );

  GlobalBroadcaster::instance()->isWordPresentedInFavorites = [ this ]( const QString & word ) {
    return this->ui.favoritesPaneWidget->isWordPresentInActiveFolder( word );
  };

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

  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::tabBarDoubleClicked, this, [ this ]( const int index ) {
    if ( -1 == index ) { // empty space at tabbar clicked.
      this->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, this, []() {
    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::textEdited, this, &MainWindow::translateInputChanged );

  connect( ui.translateLine, &QLineEdit::returnPressed, this, [ this ]() {
    translateInputFinished( true );
  } );
  connect( translateBox, &TranslateBox::returnPressed, this, [ this ]() {
    translateInputFinished( true );
  } );

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

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

  // Only keep itemClicked signal connection, remove itemSelectionChanged and itemDoubleClicked connections
  // connect( ui.dictsList, &QListWidget::itemSelectionChanged, this, &MainWindow::dictsListSelectionChanged );
  // connect( ui.dictsList, &QListWidget::itemDoubleClicked, this, &MainWindow::dictsListItemActivated );

  connect( GlobalBroadcaster::instance(),
           &GlobalBroadcaster::mutedDictionariesChanged,
           this,
           &MainWindow::mutedDictionariesChanged );

  this->installEventFilter( this );

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

  ui.wordList->installEventFilter( this );

  ui.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 );
  connect( GlobalBroadcaster::instance(),
           &GlobalBroadcaster::indexingDictionary,
           this,
           &MainWindow::showFTSIndexingName );
  connect( GlobalBroadcaster::instance(), &GlobalBroadcaster::ftsStateChanged, this, [ this ]() {
    ftsIndexing.stopIndexing();
    ftsIndexing.doIndexing();
  } );
  connect( GlobalBroadcaster::instance(),
           &GlobalBroadcaster::websiteDictionarySignal,
           this,
           &MainWindow::openWebsiteInNewTab );

  connect( &GlobalBroadcaster::instance()->pronounce_engine,
           &PronounceEngine::emitAudio,
           this,
           [ this ]( auto audioUrl ) {
             auto view = getCurrentArticleView();
             if ( view == nullptr ) {
               return;
             }
             view->setAudioLink( audioUrl );
             if ( !isActiveWindow() ) {
               return;
             }
             if ( cfg.preferences.pronounceOnLoadMain ) {
               // Use a small delay to avoid audio clipping on Windows during window activation/rendering
               QTimer::singleShot( 150, view, [ view, audioUrl ]() {
                 view->playAudio( QUrl::fromEncoded( audioUrl.toUtf8() ) );
               } );
             }
           } );
  applyProxySettings();

  //set  webengineview font
  changeWebEngineViewFont();

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

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

  makeDictionaries();

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

  setWindowTitle( "GoldenDict-ng" );

  // Create tab list menu
  createTabList();


#if defined( Q_OS_LINUX )
  defaultInterfaceStyle = QApplication::style()->name();
#elif defined( Q_OS_MAC )
  defaultInterfaceStyle = "Fusion";
#endif

  updateAppearances( cfg.preferences.addonStyle,
                     cfg.preferences.displayStyle,
                     cfg.preferences.darkMode
#if !defined( Q_OS_WIN )
                     ,
                     cfg.preferences.interfaceStyle
#endif
  );

  // Create and show the initial welcome tab
  history.enableAdd( false );
  createNewTab( true, ArticleMaker::welcomeWord() )->load( QUrl( "gdinternal://welcome-page" ) );
  history.enableAdd( cfg.preferences.storeHistory );

  // restore should be called after all UI initialized but not necessarily after show()
  // This must be called before show() as of Qt6.5 on Windows, not sure if it is a bug
  // Due to a bug of WebEngine, this also must be called after WebEngine has a view loaded https://bugreports.qt.io/browse/QTBUG-115074
  if ( cfg.mainWindowState.size() && !cfg.resetState ) {
    restoreState( cfg.mainWindowState );
  }
  if ( cfg.mainWindowGeometry.size() ) {
    restoreGeometry( cfg.mainWindowGeometry );
  }

  // Show window unless it is configured not to
  if ( !cfg.preferences.enableTrayIcon || !cfg.preferences.startToTray ) {
    show();
    focusTranslateLine();
  }

  // Scanpopup related
  // Deferred initialization until first use or if scanning is enabled
  // Use a delayed call to avoid blocking the main window's initial show-up
  if ( cfg.preferences.startWithScanPopupOn ) {
    QTimer::singleShot( 1000, this, &MainWindow::ensureScanPopup );
  }

  clipboardListener = clipboardListener::get_impl( this );
  connect( clipboardListener, &BaseClipboardListener::changed, this, &MainWindow::clipboardChange );

  connect( enableScanningAction, &QAction::toggled, this, [ 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" ) );
    }
#endif


    if ( on ) {
      clipboardListener->start();
    }
    else {
      clipboardListener->stop();
    }

    installHotKeys();
    trayIconUpdateOrInit();
  } );

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

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

  QTimer::singleShot( 1000, this, &MainWindow::trayIconUpdateOrInit );

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

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

  // Initialize global hotkeys
  QTimer::singleShot( 2000, this, &MainWindow::installHotKeys );

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

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

  // makeDictionaries() didn't do deferred init - we do it here, at the end.
  // Use a delay to let the UI breathe first
  QTimer::singleShot( 3000, this, [ this ]() {
    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();

#ifndef Q_OS_MACOS
  ui.centralWidget->grabGesture( Gestures::GDPinchGestureType );
  ui.centralWidget->grabGesture( Gestures::GDSwipeGestureType );
#endif

  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 ) ); // Moved to lazy initialization in inspectElement()

#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

  iconSizeActionTriggered( nullptr );

  if ( cfg.preferences.checkForNewReleases ) {
    QTimer::singleShot( 10000, this, &MainWindow::checkNewRelease );
  }

#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
  if ( cfg.preferences.darkMode == Config::Dark::Auto ) {
    connect( QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, &MainWindow::refreshAppearances );
  }
#endif
}


void MainWindow::ensureScanPopup()
{
  if ( scanPopup ) {
    return;
  }

  if ( !GlobalBroadcaster::instance()->getAllDictionaries() || !GlobalBroadcaster::instance()->getGroups() ) {
    return;
  }

  // Scanpopup related
  scanPopup = new ScanPopup( nullptr, cfg, articleNetMgr, history );

  scanPopup->setStyleSheet( styleSheet() );

  connect( scanPopup, &ScanPopup::editGroupRequest, this, &MainWindow::editDictionaries, Qt::QueuedConnection );

  connect( scanPopup, &ScanPopup::sendPhraseToMainWindow, this, [ this ]( const QString & word ) {
    wordReceived( word );
  } );

  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,
           ui.favoritesPaneWidget,
           &FavoritesPaneWidget::addRemoveWordInActiveFav );
}

void MainWindow::prefixMatchUpdated()
{
  updateMatchResults( false );
}

void MainWindow::prefixMatchFinished()
{
  updateMatchResults( true );
}

void MainWindow::updateMatchResults( bool finished )
{
  const WordFinder::SearchResults & results = wordFinder.getResults();

  if ( cfg.preferences.searchInDock ) {
    ui.wordList->setUpdatesEnabled( false );
    //clear all existed items
    ui.wordList->clear();

    for ( const auto & result : results ) {
      auto i = new QListWidgetItem( result.first, ui.wordList );
      i->setToolTip( result.first );

      if ( result.second ) {
        QFont f = i->font();
        f.setItalic( true );
        i->setFont( f );
      }

      i->setTextAlignment( Qt::AlignLeft );
    }

    if ( ui.wordList->count() ) {
      ui.wordList->scrollToItem( ui.wordList->item( 0 ), QAbstractItemView::PositionAtTop );
      ui.wordList->setCurrentItem( nullptr, QItemSelectionModel::Clear );
    }

    ui.wordList->setUpdatesEnabled( true );
    ui.wordList->unsetCursor();
  }
  else {

    QStringList _results;
    _results.clear();

    for ( const auto & result : results ) {
      _results << result.first;
    }
    translateBox->setModel( _results );
  }

  if ( finished ) {

    refreshTranslateLine();

    if ( !wordFinder.getErrorString().isEmpty() ) {
      emit showStatusBarMessage( tr( "WARNING: %1" ).arg( wordFinder.getErrorString() ),
                                 20000,
                                 QPixmap( ":/icons/error.svg" ) );
    }
  }
}

void MainWindow::refreshTranslateLine()
{
  if ( !translateLine ) {
    return;
  }

  // Visually mark the input line to mark if there's no results
  bool setMark = wordFinder.getResults().empty() && !wordFinder.wasSearchUncertain();
  Utils::Widget::setNoResultColor( translateLine, setMark );
  if ( !cfg.preferences.searchInDock ) {
    translateBox->setNoResults( setMark );
  }
}

void MainWindow::clipboardChange( QClipboard::Mode m )
{
  ensureScanPopup();

  if ( !scanPopup ) {
    return;
  }

#if defined( WITH_X11 )
  if ( m == QClipboard::Clipboard ) {
    if ( !cfg.preferences.trackClipboardScan )
      return;
    scanPopup->translateWordFromPrimaryClipboard();
    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( clipboardListener->text() );
#else
  scanPopup->translateWordFromPrimaryClipboard();
#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;

    translateBoxToolBarAction->setVisible( false );

    translateBox->setPopupEnabled( 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();

    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;

  updateGroupList( false );

  setInputLineText( text, WildcardPolicy::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 );
  setInputLineText( str, WildcardPolicy::EscapeWildcards, NoPopupChange );

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

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

  ftsIndexing.stopIndexing();
#ifndef Q_OS_MACOS
  ui.centralWidget->ungrabGesture( Gestures::GDPinchGestureType );
  ui.centralWidget->ungrabGesture( Gestures::GDSwipeGestureType );
#endif
  //  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;
  }

  if ( scanPopup ) {
    delete scanPopup;
    scanPopup = nullptr;
  }

#ifdef EPWING_SUPPORT
  Epwing::finalize();
#endif
  delete ui.historyPaneWidget; // This should be deleted before shared History Object.
}

void MainWindow::addGlobalAction( QAction * action, const std::function< void() > & slotFunc )
{
  action->setShortcutContext( Qt::WidgetWithChildrenShortcut );
  connect( action, &QAction::triggered, this, slotFunc );

  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 );
  dialog->addAction( ui.quit );
}

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()
{
  //if the dictionaries is empty ,large chance that the config has corrupt.
  if ( cfg.preferences.removeInvalidIndexOnExit && !dictMap.isEmpty() ) {
    const QDir dir( Config::getIndexDir() );

    const QFileInfoList entries = dir.entryInfoList( QDir::Files | QDir::NoDotAndDotDot );

    for ( auto & file : entries ) {
      const QString fileName = file.fileName();

      if ( dictMap.contains( fileName.toStdString() ) ) {
        continue;
      }
      //remove both normal index and fts index.
      auto filePath = file.absoluteFilePath();
      qDebug() << "remove invalid index files & fts dirs";

      QFile::remove( filePath );
      QDir d( filePath + "_FTS_x" );
      if ( d.exists() ) {
        d.removeRecursively();
      }
      //temp dir
      QDir dtemp( filePath + "_FTS_x_temp" );
      if ( dtemp.exists() ) {
        dtemp.removeRecursively();
      }
    }


    //remove temp directories.
    const QFileInfoList dirs = dir.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot );

    for ( auto & file : dirs ) {
      const QString fileName = file.fileName();

      if ( !fileName.endsWith( "_temp" ) ) {
        continue;
      }

      const QFileInfo info( fileName );
      const QDateTime lastModified = info.lastModified();

      //if the temp directory has not been modified within 7 days,remove the temp directory.
      if ( lastModified.addDays( 7 ) > QDateTime::currentDateTime() ) {
        continue;
      }
      QDir d( fileName );
      d.removeRecursively();
    }
  }


  try {
    // Save MainWindow state and geometry
    cfg.mainWindowState    = saveState();
    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 ) {
      qWarning( "Configuration saving failed, error: %s", e.what() );
    }

    // Save history
    history.save();

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

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

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

  return *printer;
}

void MainWindow::updateAppearances( const QString & addonStyle,
                                    const QString & displayStyle,
                                    Config::Dark darkMode
#if !defined( Q_OS_WIN )
                                    ,
                                    const QString & interfaceStyle
#endif
)
{
#ifdef Q_OS_WIN32
  if ( darkMode == Config::Dark::On ) {
    //https://forum.qt.io/topic/101391/windows-10-dark-theme

    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 );
    qApp->setStyle( "Fusion" );
  }
  else {
    qApp->setStyle( "WindowsVista" );
    qApp->setPalette( QPalette() );
  }

#endif

#if !defined( Q_OS_WIN )
  if ( interfaceStyle == "Default" ) {
    QApplication::setStyle( QStyleFactory::create( defaultInterfaceStyle ) );
  }
  else {
    if ( QStyleFactory::keys().contains( interfaceStyle ) ) {
      QApplication::setStyle( QStyleFactory::create( interfaceStyle ) );
    }
  }
#endif

  // Re-apply the user-configured font after resetting the palette or style
  if ( cfg.preferences.enableInterfaceFont && !cfg.preferences.interfaceFont.isEmpty() ) {
    QFont font = QApplication::font();
    font.setFamily( cfg.preferences.interfaceFont );
    if ( cfg.preferences.interfaceFontSize > 0 ) {
      font.setPixelSize( cfg.preferences.interfaceFontSize );
    }
    QApplication::setFont( font );
  }


  QByteArray css{};

  QFile commonCssFile( ":qt-common.css" );
  if ( commonCssFile.open( QFile::ReadOnly ) ) {
    css += commonCssFile.readAll();
  }

#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 == Config::Dark::Off ) {
    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 == Config::Dark::On ) {
    css += "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }";
  }
#endif

  if ( !css.isEmpty() ) {
    setStyleSheet( css );
    if ( scanPopup ) {
      scanPopup->setStyleSheet( css );
    }
  }
}

void MainWindow::refreshAppearances()
{
  updateAppearances( cfg.preferences.addonStyle,
                     cfg.preferences.displayStyle,
                     cfg.preferences.darkMode
#if !defined( Q_OS_WIN )
                     ,
                     cfg.preferences.interfaceStyle
#endif
  );
}

void MainWindow::trayIconUpdateOrInit()
{
#ifdef Q_OS_MACOS
  trayIconMenu.setAsDockMenu();
  ui.actionCloseToTray->setVisible( false );
#else

  if ( !cfg.preferences.enableTrayIcon ) {
    if ( trayIcon ) {
      delete trayIcon;
      trayIcon = nullptr;
    }
  }
  else {
    // Update the icon to reflect the scanning mode
    QIcon icon = enableScanningAction->isChecked() ?
      QIcon::fromTheme( "goldendict-scan-tray", QIcon( ":/icons/programicon_scan.png" ) ) :
      QIcon::fromTheme( "goldendict-tray", QIcon( ":/icons/programicon_old.png" ) );

    if ( !trayIcon ) {
      trayIcon = new QSystemTrayIcon( this );
      trayIcon->setContextMenu( &trayIconMenu );
      trayIcon->setToolTip( QApplication::applicationName() );
      trayIcon->setIcon( icon );
      connect( trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated );
      trayIcon->show();
    }
    else {
      // Update existing tray icon
      trayIcon->setIcon( icon );
    }
  }

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

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 tray icon is disabled or closing to tray is not enabled, quit the application
  if ( !cfg.preferences.enableTrayIcon || !cfg.preferences.closeToTray ) {
    ev->accept();
    quitApp();
    return;
  }

  // Hide translation popup if necessary
  if ( !cfg.preferences.searchInDock ) {
    translateBox->setPopupEnabled( false );
  }

#ifdef Q_OS_MACOS
  // On macOS, ignore non-spontaneous events or already hidden windows to avoid redundant logic
  if ( !ev->spontaneous() || !isVisible() ) {
    return;
  }
#endif

  // Handle window hiding based on platform characteristics
#if defined( Q_OS_UNIX ) && !defined( Q_OS_MACOS )
  // On Linux/Unix, do not call ignore() as it blocks session logout.
  // The window is hidden instead of closed because quitOnLastWindowClosed is false.
  Q_ASSERT( !QApplication::quitOnLastWindowClosed() );
  Q_ASSERT( !testAttribute( Qt::WA_DeleteOnClose ) );
#else
  // On Windows and macOS (manual close), ignore the event and hide the window.
  // This ensures global hotkey hooks remain valid on Windows.
  ev->ignore();
  hide();
#endif
}

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

  for ( auto viewer : findChildren< QMainWindow * >( "ResourceViewer" ) ) {
    viewer->close();
  }

  commitData();
  qApp->quit();
}

void MainWindow::applyProxySettings()
{
  if ( cfg.preferences.proxyServer.enabled && cfg.preferences.proxyServer.useSystemProxy ) {
    QNetworkProxyFactory::setUseSystemConfiguration( true );
    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::makeDictionaries()
{

  wordFinder.clear();

  dictionariesUnmuted.clear();

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

  loadDictionaries( this, 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( false );
}

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 reload )
{
  bool haveGroups = !cfg.groups.empty();

  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   = GroupId::AllGroupId;
    g.icon = "folder.png";

    groupInstances.push_back( g );
  }

  for ( auto & group : cfg.groups ) {
    groupInstances.push_back( Instances::Group( group, 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 );

  dictionaryBar.updateToGroup( groupInstances.findGroup( groupList->getCurrentGroup() ), &cfg.mutedDictionaries, cfg );

  if ( reload ) {
    qDebug() << "Reloading all the tabs...";

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

      view.reload();
    }
  }

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

const vector< sptr< Dictionary::Class > > & 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;
  }

  const QSet< QString > * mutedDictionaries = dictionaryBar.getMutedDictionaries();
  if ( !dictionaryBar.toggleViewAction()->isChecked() || mutedDictionaries == nullptr ) {
    return groupInstances[ current ].dictionaries;
  }
  else {
    const vector< sptr< Dictionary::Class > > & 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 );
      }
    }
  }
}

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

void MainWindow::addNewTab()
{
  createNewTab( true, tr( "(untitled)" ) )->load( QUrl( "gdinternal://untitle-page" ) );
}

ArticleView * MainWindow::createNewTab( bool switchToIt, const QString & name )
{
  ArticleView * view = new ArticleView( this,
                                        articleNetMgr,
                                        false,
                                        cfg,
                                        translateLine,
                                        dictionaryBar.toggleViewAction(),
                                        groupList->getCurrentGroup() );

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

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

  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 );
  connect( view, &ArticleView::translateSelectedText, this, &MainWindow::handleTranslateSelectedText );

  connect( ui.searchInPageAction, &QAction::triggered, view, [ this, view ]() {
#ifdef Q_OS_MACOS
    //workaround to fix macos popup page search Ctrl + F
    if ( scanPopup && scanPopup->isActiveWindow() ) {
      scanPopup->openSearch();
      return;
    }
#endif
    view->openSearch();
  } );

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

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

  QString escaped = Utils::escapeAmps( name );

  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 )
{
  if ( !inspector ) {
    inspector.reset( new ArticleInspector( this ) );
  }
  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.empty() ) {
    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()
{
  ArticleView * view = getCurrentArticleView();

  view->back();
}

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

  view->forward();
}

void MainWindow::titleChanged( ArticleView * view, const QString & 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 = Utils::escapeAmps( escaped );

  // Truncate long titles to make tab labels more readable
  const int maxTabTitleLength = 30;
  escaped                     = Utils::ellipsizeString( escaped, maxTabTitleLength );

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

  if ( index == ui.tabWidget->currentIndex() ) {
    updateFavIcon( title );

    updateWindowTitle();
  }
}

void MainWindow::iconChanged( ArticleView * view, const QIcon & 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() ) {
      setWindowTitle( QString( "%1 - %2" ).arg( str, "GoldenDict-ng" ) );
    }
  }
}

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

  updateBackForwardButtons();
  updatePronounceAvailability();
}

void MainWindow::showStatusBarMessage( const QString & message, int timeout, const QPixmap & 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
  auto view = getCurrentArticleView();
  QString headword;
  if ( view ) {
    headword = view->getCurrentWord();
  }

  updateFavIcon( headword );

  if ( view ) {
    groupList->setCurrentGroup( view->getCurrentGroupId() );
  }
}

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

  dictionaryBar.updateToGroup( groupInstances.findGroup( groupList->getCurrentGroup() ), &cfg.mutedDictionaries, cfg );
  dictionaryBar.setDictionaryIconSize( this->getIconSizeLogical() );

  applyMutedDictionariesState(); // Visibility change affects searches and results
}

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;
  }

  // Get the current view to check if it's a website
  ArticleView * view = getCurrentArticleView();

  if ( !view ) {
    return;
  }

  // If current view is a website, don't rebuild the list, just select the corresponding item
  if ( view->isWebsite() ) {
    QString websiteDictId = view->getActiveArticleId();

    if ( !websiteDictId.isEmpty() ) {
      // Find and select the corresponding item in the existing list
      for ( int i = 0; i < ui.dictsList->count(); ++i ) {
        QListWidgetItem * item = ui.dictsList->item( i );
        if ( item && item->data( Qt::UserRole ).toString() == websiteDictId ) {
          ui.dictsList->setCurrentItem( item );
          return; // No need to proceed further
        }
      }
    }
    return;
  }

  ui.dictsList->clear();

  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() );
        auto * 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;
      }
    }
  }
}

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

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

void MainWindow::updatePronounceAvailability()
{
  if ( ui.tabWidget->count() > 0 ) {
    ArticleView * pView = getCurrentArticleView();
    if ( pView == nullptr ) {
      return;
    }

    pView->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 != GroupId::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 ( auto & group : newCfg.groups ) {
        unsigned id = group.id;
        if ( id != GroupId::NoGroupId ) {
          const Config::Group * grp = cfg.getGroup( id );
          if ( grp ) {
            group.mutedDictionaries      = grp->mutedDictionaries;
            group.popupMutedDictionaries = grp->popupMutedDictionaries;
          }
        }
      }

      cfg = newCfg;

      dictMap = Dictionary::dictToMap( dictionaries );

      updateGroupList();

      Config::save( cfg );

      updateSuggestionList();

      for ( auto & dictionary : dictionaries ) {
        dictionary->setFTSParameters( cfg.preferences.fts );
        dictionary->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
      }

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

  if ( scanPopup ) {
    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.hideMenubar    = cfg.preferences.hideMenubar;
    p.searchInDock   = cfg.preferences.searchInDock;
    p.alwaysOnTop    = cfg.preferences.alwaysOnTop;

    p.fts.dialogGeometry = cfg.preferences.fts.dialogGeometry;

    p.fts.searchMode = cfg.preferences.fts.searchMode;

    // See if we need to update Appearances
    if ( cfg.preferences.displayStyle != p.displayStyle || cfg.preferences.darkMode != p.darkMode
#if !defined( Q_OS_WIN )
         || cfg.preferences.interfaceStyle != p.interfaceStyle
#endif
    ) {
      updateAppearances( p.addonStyle,
                         p.displayStyle,
                         p.darkMode
#if !defined( Q_OS_WIN )
                         ,
                         p.interfaceStyle
#endif
      );
    }

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

    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
        || p.darkReaderMode == Config::Dark::Auto // We cannot know if a reload is needed, just do it regardless.
      );

    cfg.preferences = p;

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

      view.setSelectionBySingleClick( p.selectWordBySingleClick );
      view.syncBackgroundColorWithCfgDarkReader();
      if ( needReload ) {
        view.reload();
      }

#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
      if ( cfg.preferences.darkReaderMode == Config::Dark::Auto ) {
        connect( QGuiApplication::styleHints(),
                 &QStyleHints::colorSchemeChanged,
                 &view,
                 &ArticleView::reload,
                 Qt::UniqueConnection );
      }
      else {
        disconnect( QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, &view, &ArticleView::reload );
      }
#endif
    }

#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
    if ( cfg.preferences.darkMode == Config::Dark::Auto ) {
      connect( QGuiApplication::styleHints(),
               &QStyleHints::colorSchemeChanged,
               this,
               &MainWindow::refreshAppearances,
               Qt::UniqueConnection );
    }
    else {
      disconnect( QGuiApplication::styleHints(),
                  &QStyleHints::colorSchemeChanged,
                  this,
                  &MainWindow::refreshAppearances );
    }
#endif

    audioPlayerFactory.setPreferences( cfg.preferences.useInternalPlayer,
                                       cfg.preferences.internalPlayerBackend,
                                       cfg.preferences.audioPlaybackProgram );

    trayIconUpdateOrInit();
    applyProxySettings();

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

    setAutostart( cfg.preferences.autoStart );

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

    for ( const auto & dictionarie : dictionaries ) {
      dictionarie->setFTSParameters( cfg.preferences.fts );
      dictionarie->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
    }

    Logger::switchLoggingMethod( cfg.preferences.enableApplicationLog );

    Config::save( cfg );
  }

  if ( scanPopup ) {
    scanPopup->refresh();
  }
  installHotKeys();

  ftsIndexing.setDictionaries( dictionaries );
  // Delay indexing to avoid high IO load during startup
  QTimer::singleShot( 5000, this, [ this ]() {
    ftsIndexing.doIndexing();
  } );
}

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

  if ( igrp ) {
    GlobalBroadcaster::instance()->currentGroupId = grg_id;
    ui.tabWidget->setTabIcon( ui.tabWidget->currentIndex(), igrp->makeIcon() );
  }
  else {
    ui.tabWidget->setTabIcon( ui.tabWidget->currentIndex(), QIcon() );
  }

  dictionaryBar.updateToGroup( groupInstances.findGroup( groupList->getCurrentGroup() ), &cfg.mutedDictionaries, cfg );

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

  if ( auto view = getCurrentArticleView() ) {
    if ( view->getCurrentGroupId() != grg_id ) {
      // If the current view is the "Welcome!" page, don't trigger a new lookup.
      // Just update its group ID so that if the user types a new word,
      // it will be looked up in the correct new group.
      // We check the isWelcomePage() flag instead of the word to allow users
      // to actually look up the word "Welcome!".
      if ( view->isInternalPage() ) {
        view->setCurrentGroupId( grg_id );
        return;
      }
      view->setCurrentGroupId( grg_id );
      QString word = Folding::unescapeWildcardSymbols( view->getWord() );
      respondToTranslationRequest( word, false );
    }
  }

  if ( ftsDlg ) {
    ftsDlg->setCurrentGroup( grg_id );
  }

  if ( igrp ) {
    ui.favoritesPaneWidget->trySetCurrentActiveFav( igrp->favoritesFolder.split( '/' ) );
  }
}

void MainWindow::translateInputChanged( const QString & newValue )
{
  updateSuggestionList( newValue );
  // 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( const QString & 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 ( ui.wordList->selectionModel()->hasSelection() ) {
    ui.wordList->setCurrentItem( nullptr, QItemSelectionModel::Clear );
  }

  QString req = newValue.trimmed();

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

    // Reset the noResults mark if it's on right now
    Utils::Widget::setNoResultColor( translateLine, false );
    if ( !cfg.preferences.searchInDock ) {
      translateBox->setNoResults( false );
    }
    return;
  }

  ui.wordList->setCursor( Qt::WaitCursor );
  wordFinder.prefixMatch( req, getActiveDicts() );
}

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

void MainWindow::respondToTranslationRequest( const QString & word,
                                              bool checkModifiers,
                                              const QString & scrollTo,
                                              bool focus )
{
  if ( !word.isEmpty() ) {
    Qt::KeyboardModifiers mods = QApplication::keyboardModifiers();
    if ( checkModifiers && ( mods & ( Qt::ControlModifier | Qt::ShiftModifier ) ) ) {
      addNewTab();
    }

    showTranslationFor( word, 0, scrollTo );

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

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

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

  updateSuggestionList();
  GlobalBroadcaster::instance()->translateLineText = text;
}

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

  if ( cfg.preferences.escKeyHidesMainWindow ) {
    toggleMainWindow( false );
  }
  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 ) {
    auto * ke = dynamic_cast< QKeyEvent * >( ev );
    // Handle F3/Shift+F3 shortcuts
    const int key = ke->key();
    if ( key == Qt::Key_F3 ) {
      ArticleView * view = getCurrentArticleView();
      if ( view && view->handleF3( obj, ev ) ) {
        return true;
      }
    }

    //workaround to fix #660
    if ( obj == this && ev->type() == QEvent::KeyPress
         && ( key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_Space || key == Qt::Key_PageUp
              || key == Qt::Key_PageDown ) ) {
      ArticleView * view = getCurrentArticleView();
      if ( view ) {
        view->focus();
        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 ) {
    auto stev    = dynamic_cast< QWindowStateChangeEvent * >( ev );
    wasMaximized = ( stev->oldState() == Qt::WindowMaximized && isMinimized() );
  }

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

    return handleBackForwardMouseButtons( event );
  }

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

    const bool handleCtrlTab = ( obj == translateLine || obj == ui.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;
    }

    // Handle only Ctrl+Shift+Tab here because Ctrl+Tab was already handled before
    if ( handleCtrlTab && keyevent->matches( QKeySequence::PreviousChild ) ) {
      QApplication::sendEvent( ui.tabWidget, ev );
      return true;
    }
  }

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

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

    if ( ev->type() == QEvent::FocusIn ) {

      // select all on mouse click
      if ( const auto focusEvent = dynamic_cast< QFocusEvent * >( ev ); focusEvent->reason() == Qt::MouseFocusReason ) {
        QTimer::singleShot( 0, this, SLOT( focusTranslateLine() ) );
      }
      return false;
    }
  }
  else if ( obj == ui.wordList ) {
    if ( ev->type() == QEvent::KeyPress ) {
      QKeyEvent * keyEvent = dynamic_cast< QKeyEvent * >( ev );

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

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

        focusArticleView();

        return cfg.preferences.searchInDock;
      }
    }
  }
  else if ( obj == ui.dictsList ) {
    if ( ev->type() == QEvent::KeyPress ) {
      QKeyEvent * keyEvent = dynamic_cast< QKeyEvent * >( ev );

      // Handle up/down arrow key navigation
      if ( keyEvent->matches( QKeySequence::MoveToNextLine ) && ui.dictsList->count() > 0 ) {
        int currentRow = ui.dictsList->currentRow();
        if ( currentRow < ui.dictsList->count() - 1 ) {
          ui.dictsList->setCurrentRow( currentRow + 1, QItemSelectionModel::ClearAndSelect );
          jumpToDictionary( ui.dictsList->item( currentRow + 1 ), true );
          return true;
        }
      }
      else if ( keyEvent->matches( QKeySequence::MoveToPreviousLine ) && ui.dictsList->count() > 0 ) {
        int currentRow = ui.dictsList->currentRow();
        if ( currentRow > 0 ) {
          ui.dictsList->setCurrentRow( currentRow - 1, QItemSelectionModel::ClearAndSelect );
          jumpToDictionary( ui.dictsList->item( currentRow - 1 ), true );
          return true;
        }
      }
    }
  }

  if ( ev->type() == QEvent::KeyPress && obj != translateLine ) {

    if ( const auto key_event = dynamic_cast< QKeyEvent * >( ev );
         key_event->modifiers() == Qt::NoModifier || key_event->modifiers() == Qt::ShiftModifier ) {
      const QString text = key_event->text();

      if ( Utils::ignoreKeyEvent( key_event ) || key_event->key() == Qt::Key_Return
           || key_event->key() == Qt::Key_Enter ) {
        return false; // Those key have other uses than to start typing
      }
      // or don't make sense
      if ( !text.isEmpty() ) {
        typingEvent( text );
        return true;
      }
    }
  }

  return QMainWindow::eventFilter( obj, ev );
}

void MainWindow::wordListItemActivated( QListWidgetItem * item )
{
  if ( wordListSelChanged ) {
    wordListSelChanged = false;
  }
  else {
    respondToTranslationRequest( item->text(), true );
  }
}

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

  if ( !selected.empty() ) {
    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() ) {
    jumpToDictionary( selected.front() );
  }
}

void MainWindow::jumpToDictionary( QListWidgetItem * item, bool force )
{
  auto dictId = item->data( Qt::UserRole ).toString();

  // If openWebsiteInNewTab is configured, try to find existing tab first
  if ( GlobalBroadcaster::instance()->getPreference()->openWebsiteInNewTab ) {
    if ( ArticleView * view = findArticleViewByDictId( dictId ) ) {
      // Switch to the found tab
      ui.tabWidget->setCurrentWidget( view );
      return;
    }
  }

  if ( ArticleView * view = getFirstNonWebSiteArticleView() ) {
    // Switch to the found tab
    ui.tabWidget->setCurrentWidget( view );
    view->jumpToDictionary( dictId, force );
  }
}

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

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

void MainWindow::handleTranslateSelectedText( const QString & word, const QUrl & url, const QString & currentArticle )
{
  // Initiate translation
  Qt::KeyboardModifiers kmod = QApplication::keyboardModifiers();
  if ( kmod & ( Qt::ControlModifier | Qt::ShiftModifier ) ) { // open in new tab
    // Create a new tab and show definition there
    ArticleView * newView = createNewTab( !cfg.preferences.newTabsOpenInBackground, word );
    auto groupId          = newView->getGroup( url );
    if ( groupId == GroupId::NoGroupId ) {
      groupId = groupList->getCurrentGroup();
    }
    newView->showDefinition( word, groupId, currentArticle, Contexts() );
  }
  else {
    // Get the current active ArticleView
    ArticleView * currentView = getFirstNonWebSiteArticleView();
    if ( currentView ) {
      // Get group ID from the URL or fall back to current group
      auto groupId = currentView->getGroup( url );
      if ( groupId == GroupId::NoGroupId || currentView->isInternalPage() ) {
        groupId = groupList->getCurrentGroup();
      }

      // If the URL has dictionaries query parameter, use those dictionaries
      if ( Utils::Url::hasQueryItem( url, "dictionaries" ) ) {
        QStringList dictsList = Utils::Url::queryItemValue( url, "dictionaries" ).split( ",", Qt::SkipEmptyParts );
        currentView->showDefinition( word, dictsList, groupId, false );
      }
      else {
        // Otherwise show definition in current tab
        currentView->showDefinition( word, groupId, currentArticle );
      }
    }
  }
}

void MainWindow::activeArticleChanged( const ArticleView * view, const QString & id )
{
  if ( view != getCurrentArticleView() || view->isWebsite() ) {
    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( const QString & 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() ) {
      if ( navToolbar->isFloating() ) {
        navToolbar->activateWindow();
      }

      translateLine->clear();
      translateLine->setFocus();
      // Escaping the typed-in characters is the user's responsibility.
      setInputLineText( t, WildcardPolicy::WildcardsAreAlreadyEscaped, EnablePopup );
      translateLine->setCursorPosition( t.size() );
    }
  }
}

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

void MainWindow::showHistoryItem( const QString & word )
{
  history.enableAdd( false );

  setInputLineText( word, WildcardPolicy::EscapeWildcards, DisablePopup );
  showTranslationFor( word );

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

void MainWindow::showTranslationFor( const QString & word, unsigned inGroup, const QString & scrollTo )
{
  GlobalBroadcaster::instance()->is_popup = false;

  ArticleView * view = getFirstNonWebSiteArticleView();

  navPronounce->setEnabled( false );

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

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

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

void MainWindow::showTranslationForDicts( const QString & inWord,
                                          const QStringList & dictIDs,
                                          const QRegularExpression & searchRegExp,
                                          bool ignoreDiacritics )
{
  ArticleView * view = getFirstNonWebSiteArticleView();

  navPronounce->setEnabled( false );

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

void MainWindow::toggleMainWindow( bool ensureShow )
{
  bool shown = false;

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

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

    activateWindow();
    raise();
    shown = true;
  }
  else if ( isMinimized() ) {
    if ( wasMaximized ) {
      showMaximized();
    }
    else {
      showNormal();
    }
    activateWindow();
    raise();
    shown = true;
  }
  else if ( !isActiveWindow() ) {
    activateWindow();
    if ( cfg.preferences.raiseWindowOnSearch ) {
      raise();
    }
    shown = true;
  }
  else if ( !ensureShow ) {

    // 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
    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()
{
#if defined( WITH_X11 )
  if ( QGuiApplication::platformName() != "xcb" ) {
    return;
  }
#endif

  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 ) {
      hotkeyWrapper->setGlobalKey( cfg.preferences.clipboardHotkey, 1 );
    }

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

void MainWindow::hotKeyActivated( int hk )
{
  if ( !hk ) {
    GlobalBroadcaster::instance()->is_popup = false;
    toggleMainWindow( false );
  }
  else {
    ensureScanPopup();
    GlobalBroadcaster::instance()->is_popup = true;
#if defined( Q_OS_UNIX ) && !defined( Q_OS_MACOS )
    // 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.
    if ( scanPopup ) {
      QTimer::singleShot( 10, scanPopup, &ScanPopup::translateWordFromPrimaryClipboard );
    }
#else
    if ( scanPopup ) {
      scanPopup->translateWordFromPrimaryClipboard();
    }
#endif
  }
}


void MainWindow::checkNewRelease()
{
  // Limit release check to 1 per day.
  if ( cfg.timeForNewReleaseCheck < QDateTime::currentDateTime().addDays( 1 ) ) {
    return;
  }

  QNetworkRequest github_release_api;
  github_release_api.setUrl( QUrl( "https://api.github.com/repos/xiaoyifang/goldendict-ng/releases/latest" ) );
  github_release_api.setRawHeader( QByteArray( "Accept" ), QByteArray( "application/vnd.github+json" ) );
  // github_release_api.setRawHeader( QByteArray( "Authorization" ), QByteArray( "" ) );
  github_release_api.setRawHeader( QByteArray( "X-GitHub-Api-Version" ), QByteArray( "2022-11-28" ) );

  auto * github_reply = dictNetMgr.get( github_release_api ); // will be marked as deleteLater when reply finished.

  QObject::connect( github_reply, &QNetworkReply::finished, github_reply, [ github_reply, this ]() {
    if ( github_reply->error() != QNetworkReply::NoError ) {
      qWarning() << "Version check failed: " << github_reply->errorString();
    }
    else {
      auto latest_release = QJsonDocument::fromJson( github_reply->readAll() );
      if ( !latest_release.isNull() ) {
        const QJsonValue tag_name = latest_release[ "tag_name" ];
        const QJsonValue html_url = latest_release[ "html_url" ];

        if ( tag_name.isString() && html_url.isString() ) {
          QString latestVersion = tag_name.toString().mid( 1, 8 );
          QString downloadUrl   = html_url.toString();

          if ( 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;
            }
          }
        }
      }
    }

    cfg.timeForNewReleaseCheck = QDateTime::currentDateTime();

    Config::save( cfg );

    github_reply->deleteLater();
  } );
}

void MainWindow::trayIconActivated( QSystemTrayIcon::ActivationReason r )
{
  switch ( r ) {
    case QSystemTrayIcon::Trigger:
      // Left click toggles the visibility of main window
      toggleMainWindow( false );
      break;

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


void MainWindow::visitHomepage()
{
  QDesktopServices::openUrl( QUrl( "https://xiaoyifang.github.io/goldendict-ng/" ) );
}

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

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

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

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

DictionaryBar::IconSize MainWindow::getIconSizeLogical()
{
  if ( useLargeIconsInToolbarsAction.isChecked() ) {
    return DictionaryBar::IconSize::Large;
  }
  else if ( useSmallIconsInToolbarsAction.isChecked() ) {
    return DictionaryBar::IconSize::Small;
  }
  else {
    return DictionaryBar::IconSize::Normal;
  }
}


int MainWindow::getIconSize()
{
  bool useLargeIcons = useLargeIconsInToolbarsAction.isChecked();
  int extent         = QApplication::style()->pixelMetric( QStyle::PM_ToolBarIconSize );
  if ( useLargeIcons ) {
    extent = QApplication::style()->pixelMetric( QStyle::PM_LargeIconSize );
  }
  else if ( useSmallIconsInToolbarsAction.isChecked() ) {
    extent = QApplication::style()->pixelMetric( QStyle::PM_SmallIconSize );
  }
  else {
    //empty
  }
  return extent;
}

void MainWindow::iconSizeActionTriggered( QAction * /*action*/ )
{
  bool useLargeIcons = useLargeIconsInToolbarsAction.isChecked();
  int extent         = getIconSize();
  if ( useLargeIcons ) {
    cfg.usingToolbarsIconSize = Config::ToolbarsIconSize::Large;
  }
  else if ( useSmallIconsInToolbarsAction.isChecked() ) {
    cfg.usingToolbarsIconSize = Config::ToolbarsIconSize::Small;
  }
  else {
    cfg.usingToolbarsIconSize = Config::ToolbarsIconSize::Normal;
  }

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

  dictionaryBar.setDictionaryIconSize( getIconSizeLogical() );

  if ( scanPopup ) {
    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( QT_TR_NOOP( "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 * >();
  for ( const auto & menu : allMenus ) {
    QList< QAction * > allMenuActions = menu->actions();
    for ( const auto & action : allMenuActions ) {
      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()
{
  QMessageBox::StandardButton reply;
  reply = QMessageBox::question( this,
                                 tr( "Clear History" ),
                                 tr( "Are you sure you want to clear all history items?" ),
                                 QMessageBox::Yes | QMessageBox::No );
  if ( reply == QMessageBox::Yes ) {
    history.clear();
    history.save();
  }
}

void MainWindow::on_clearFavorites_triggered()
{
  ui.favoritesPaneWidget->clearAllItems();
}

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

void MainWindow::setAutostart( bool autostart )
{
#if defined Q_OS_WIN32
  QSettings reg( R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat );
  if ( autostart ) {
    QString app_fname = QString( R"("%1")" ).arg( QCoreApplication::applicationFilePath() );
    app_fname.replace( "/", R"(\)" );
    reg.setValue( ApplicationSettingName, app_fname );
  }
  else {
    reg.remove( ApplicationSettingName );
  }
  reg.sync();
#elif defined( Q_OS_UNIX ) && !defined( Q_OS_MACOS )
  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/io.github.xiaoyifang.goldendict_ng.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 );
}

void MainWindow::on_saveArticle_triggered()
{
  // Delegate to centralized saver object and show status messages on the main status bar
  ArticleView * view = getCurrentArticleView();
  if ( !view ) {
    return;
  }
  auto * saver = new ArticleSaver( this, view, cfg );
  connect( saver, &ArticleSaver::statusMessage, this, [ this ]( const QString & message, int timeout ) {
    mainStatusBar->showMessage( message, timeout );
  } );
  saver->save();
}

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, cfg, dictionaries, dictNetMgr );
  dictMap = Dictionary::dictToMap( dictionaries );

  for ( const auto & dictionarie : dictionaries ) {
    dictionarie->setFTSParameters( cfg.preferences.fts );
    dictionarie->setSynonymSearchEnabled( cfg.preferences.synonymSearchEnabled );
  }

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

  updateGroupList();


  if ( scanPopup ) {
    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::onLockPanelsToggled( bool locked )
{
  cfg.preferences.panelsLocked = locked;

  const QList< QDockWidget * > dockWidgets = findChildren< QDockWidget * >();

  for ( QDockWidget * dockWidget : dockWidgets ) {
    if ( locked ) {
      dockWidget->setFeatures( QDockWidget::NoDockWidgetFeatures );
    }
    else {
      dockWidget->setFeatures( QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable
                               | QDockWidget::DockWidgetFloatable );
    }
  }
}

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 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.25 ) {
    cfg.preferences.zoomFactor = 0.25;
  }

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

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

  if ( scanPopup ) {
    scanPopup->applyZoomFactor();
  }
}

void MainWindow::showTranslation( const QString & word, const QString & windowType )
{
  if ( windowType == "main" ) {
    wordReceived( word );
  }
  else {
    ensureScanPopup();
    if ( scanPopup ) {
      scanPopup->translateWord( word );
    }
  }
}

bool MainWindow::handleStructuredMessage( const QString & message )
{
  if ( !message.startsWith( "action:" ) ) {
    return false;
  }

  QMap< QString, QString > params;
  QStringList parts = message.split( '|' );
  for ( const QString & part : parts ) {
    QStringList keyValue = part.split( ':' );
    if ( keyValue.size() >= 2 ) {
      params[ keyValue[ 0 ] ] = keyValue.mid( 1 ).join( ':' );
    }
  }

  if ( QString action = params.value( "action" ); action == "translate" ) {
    QString windowType = params.value( "window", "popup" );
    QString word       = params.value( "word" );
    QString group      = params.value( "group" );
    QString popupGroup = params.value( "popupGroup" );

    // Handle group settings if specified
    if ( !group.isEmpty() ) {
      setGroupByName( group, true );
    }
    if ( !popupGroup.isEmpty() ) {
      setGroupByName( popupGroup, false );
    }

    // Show translation based on window type
    showTranslation( word, windowType );
  }

  return true;
}


void MainWindow::messageFromAnotherInstanceReceived( const QString & message )
{
  if ( message == "bringToFront" ) {
    toggleMainWindow( true );
    return;
  }

  if ( message == "toggleScanPopup" ) {
    enableScanningAction->trigger();
    return;
  }

  // Handle structured message format
  if ( handleStructuredMessage( message ) ) {
    return;
  }

  // Unknown message
  qWarning() << "Unknown message received from another instance: " << message;
}

ArticleView * MainWindow::getCurrentArticleView()
{
  QWidget * currentWidget   = ui.tabWidget->currentWidget();
  ArticleView * currentView = qobject_cast< ArticleView * >( currentWidget );

  if ( currentView ) {
    return currentView;
  }

  return nullptr;
}

ArticleView * MainWindow::getFirstNonWebSiteArticleView()
{
  QWidget * currentWidget   = ui.tabWidget->currentWidget();
  ArticleView * currentView = qobject_cast< ArticleView * >( currentWidget );

  // First check if "openWebsiteInNewTab" is disabled
  if ( !GlobalBroadcaster::instance()->getPreference()->openWebsiteInNewTab ) {
    if ( currentView ) {
      currentView->setWebsite( false );
      return currentView;
    }
  }

  //the following logic is under the condition that "openWebsiteInNewTab" is enabled.
  // If current view is already a non-website tab, return it directly
  if ( currentView && !currentView->isWebsite() ) {
    return currentView;
  }

  // If current view is not suitable, look for the first non-website tab
  for ( int i = 0; i < ui.tabWidget->count(); i++ ) {
    auto * view = qobject_cast< ArticleView * >( ui.tabWidget->widget( i ) );
    if ( view && !view->isWebsite() ) {
      return view;
    }
  }

  // If no non-website tab found, fall back to current view (if exists)
  if ( currentView ) {
    currentView->setWebsite( false );
    return currentView;
  }

  return nullptr;
}

ArticleView * MainWindow::findArticleViewByDictId( const QString & dictId )
{
  // First check if openWebsiteInNewTab configuration is enabled
  if ( GlobalBroadcaster::instance()->getPreference()->openWebsiteInNewTab ) {
    // Iterate through all tabs
    for ( int i = 0; i < ui.tabWidget->count(); i++ ) {
      auto * view = qobject_cast< ArticleView * >( ui.tabWidget->widget( i ) );
      if ( view && view->isWebsite() ) {
        // Check if current ArticleView's activeDictIds list contains the specified dictId
        QString dictIdActive = view->getActiveArticleId();
        if ( dictIdActive == dictId ) {
          return view;
        }
      }
    }
  }
  qDebug() << "findArticleViewByDictId() return nullptr with dictId:" << dictId;
  // If configuration is not enabled or no matching ArticleView found, return nullptr
  return nullptr;
}

void MainWindow::wordReceived( const QString & word )
{
  const bool shouldFocus = cfg.preferences.raiseWindowOnSearch;
  if ( shouldFocus ) {
    toggleMainWindow( true );
  }
  setInputLineText( word, WildcardPolicy::EscapeWildcards, NoPopupChange );
  respondToTranslationRequest( word, false, QString(), shouldFocus );
}

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


  if ( !file.open( QFile::WriteOnly | QIODevice::Text ) ) {
    errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
    return;
  }

  auto guard = qScopeGuard( [ &file ]() {
    file.close();
  } );
  Q_UNUSED( guard )

  // Write UTF-8 BOM
  QByteArray line;
  line.append( 0xEF ).append( 0xBB ).append( 0xBF );
  if ( file.write( line ) != line.size() ) {
    errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
    return;
  }

  // Write history
  const QList< History::Item > & 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() ) {
      errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
      return;
    }
  }

  mainStatusBar->showMessage( tr( "History export complete" ), 5000 );
}

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

  if ( !file.open( QFile::ReadOnly | QIODevice::Text ) ) {
    errStr = QString( tr( "Import error: " ) ) + file.errorString();
    errorMessageOnStatusBar( errStr );
    return;
  }

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

  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( { *i } );
  }

  history.enableAdd( cfg.preferences.storeHistory );

  if ( file.error() != QFile::NoError ) {
    errStr = QString( tr( "Import error: " ) ) + file.errorString();
    errorMessageOnStatusBar( errStr );
    return;
  }

  if ( fileStream.status() >= QTextStream::ReadCorruptData ) {
    errStr = QString( tr( "Import error: invalid data in file" ) );
    errorMessageOnStatusBar( errStr );
    return;
  }
  //even without this line, the destructor of QFile will close the file as documented.
  file.close();
  mainStatusBar->showMessage( tr( "History import complete" ), 5000 );
}

void MainWindow::errorMessageOnStatusBar( const QString & errStr )
{
  this->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( "Text files (*.txt);;XML files (*.xml)" ) );
  if ( fileName.size() == 0 ) {
    return;
  }
  cfg.historyExportPath = QDir::toNativeSeparators( QFileInfo( fileName ).absoluteDir().absolutePath() );
  QFile file( fileName );
  if ( !file.open( QFile::WriteOnly | QIODevice::Text ) ) {
    errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
    return;
  }
  if ( fileName.endsWith( ".xml", Qt::CaseInsensitive ) ) {
    QByteArray data;
    ui.favoritesPaneWidget->getDataInXml( data );

    if ( file.write( data ) != data.size() ) {
      errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
      return;
    }
  }
  else {
    // Write UTF-8 BOM
    QByteArray line;
    line.append( 0xEF ).append( 0xBB ).append( 0xBF );
    if ( file.write( line ) != line.size() ) {
      errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
      return;
    }

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

    line = data.toUtf8();
    if ( file.write( line ) != line.size() ) {
      errorMessageOnStatusBar( QString( tr( "Export error: " ) ) + file.errorString() );
      return;
    }
  }
  file.close();

  mainStatusBar->showMessage( tr( "Favorites export complete" ), 5000 );
}

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( "Text and XML files (*.txt *.xml);;All files (*.*)" ) );
  if ( fileName.size() == 0 ) {
    return;
  }

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

  if ( !file.open( QFile::ReadOnly | QIODevice::Text ) ) {
    errorMessageOnStatusBar( QString( tr( "Import error: " ) ) + file.errorString() );
    return;
  }

  QByteArray data = file.readAll();

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

  file.close();
  mainStatusBar->showMessage( tr( "Favorites import complete" ), 5000 );
}

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

void MainWindow::addWordToHistory( const QString & word )
{
  //    skip epwing reference link. epwing reference link has the pattern of r%dAt%d
  if ( QRegularExpressionMatch m = RX::Epwing::refWord.match( word ); m.hasMatch() ) {
    return;
  }
  history.addItem( { word.trimmed() } );
}

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

void MainWindow::foundDictsPaneClicked( QListWidgetItem * item )
{
  // Since we only keep this event handler method, no reentrancy flag is needed here

  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::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,
               [ this ]( const QString & headword, const QString & dictID ) {
                 setInputLineText( headword, WildcardPolicy::EscapeWildcards, NoPopupChange );
                 respondToTranslationRequest( headword, false, ArticleView::scrollToFromDictionaryId( dictID ), false );
               } );
      connect( headwordsDlg,
               &DictHeadwords::closeDialog,
               this,
               &MainWindow::closeHeadwordsDialog,
               Qt::QueuedConnection );
    }
    else {
      headwordsDlg->setup( dict );
    }
    headwordsDlg->show();
  }
}


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

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

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

void MainWindow::stopAudio()
{
  if ( ArticleView * view = getCurrentArticleView() ) {
    view->stopSound();
  }
}

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 ) {
    return;
  }

  QString id                = item->data( Qt::UserRole ).toString();
  Dictionary::Class * pDict = nullptr;

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

  if ( pDict == nullptr ) {
    return;
  }

  if ( !pDict->isLocalDictionary() ) {
    withScanPopupSignalBlocked( [ this, id ]() {
      showDictionaryInfo( id );
    } );
    return;
  }

  QMenu menu( ui.dictsList );
  QAction * infoAction = menu.addAction( tr( "Dictionary info" ) );

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

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

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

  if ( !result ) {
    return;
  }

  if ( result == infoAction ) {
    withScanPopupSignalBlocked( [ this, id ]() {
      showDictionaryInfo( id );
    } );
  }
  else if ( result == headwordsAction ) {
    withScanPopupSignalBlocked( [ this, pDict ]() {
      showDictionaryHeadwords( pDict );
    } );
  }
  else if ( result == openDictFolderAction ) {
    openDictionaryFolder( id );
  }
}

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

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

void MainWindow::proxyAuthentication( const QNetworkProxy &, QAuthenticator * authenticator )
{
  qDebug() << "Proxy Authentication Required";
  QNetworkProxy proxy = QNetworkProxy::applicationProxy();

  if ( proxy.type() == QNetworkProxy::DefaultProxy ) {
    qDebug() << "Current proxy is the system proxy.";
  }
  else {
    qDebug() << "Current proxy is not the system proxy.";
  }

  qDebug() << "Proxy Type:" << proxy.type();
  qDebug() << "Proxy Host Name:" << proxy.hostName();
  qDebug() << "Proxy Port:" << proxy.port();
  qDebug() << "Proxy User:" << proxy.user();
  qDebug() << "Proxy Password:" << ( proxy.password().isEmpty() ? "Not set" : "Set" );
}

void MainWindow::showFullTextSearchDialog()
{
  if ( !ftsDlg ) {
    ftsDlg = new FTS::FullTextSearchDialog( this, cfg, dictionaries, groupInstances, ftsIndexing );
    // ftsDlg->setSearchText( translateLine->text() );

    addGlobalActionsToDialog( ftsDlg );
    addGroupComboBoxActionsToDialog( ftsDlg, groupList );

    connect( ftsDlg, &FTS::FullTextSearchDialog::showTranslationFor, this, &MainWindow::showTranslationForDicts );
    connect( ftsDlg,
             &FTS::FullTextSearchDialog::closeDialog,
             this,
             &MainWindow::closeFullTextSearchDialog,
             Qt::QueuedConnection );
    connect( GlobalBroadcaster::instance(),
             &GlobalBroadcaster::ftsStateChanged,
             ftsDlg,
             &FTS::FullTextSearchDialog::updateDictionaries );

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

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

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

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

void MainWindow::openWebsiteInNewTab( QString name, QString url, QString dictId, bool isPopup, QString word )
{
  if ( isPopup ) {
    return;
  }

  auto view = findArticleViewByDictId( dictId );
  if ( view == nullptr ) {
    // Truncate long website names for tab labels
    const int maxTabTitleLength = 30;
    QString truncatedName       = Utils::ellipsizeString( name, maxTabTitleLength );
    view                        = createNewTab( false, truncatedName );
    view->setWebsite( true );
    // Set the dictId for the website view
    view->setActiveArticleId( dictId );
  }

  // Set the current word for the website view
  if ( !word.isEmpty() ) {
    view->setCurrentWord( word );
  }

  view->load( url, name );
}

void MainWindow::addCurrentTabToFavorites()
{
  auto view = getCurrentArticleView();
  if ( !view ) {
    return;
  }
  auto headword = view->getCurrentWord();

  ui.favoritesPaneWidget->addWordToActiveFav( headword );

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

void MainWindow::handleAddToFavoritesButton()
{
  auto view = getCurrentArticleView();
  if ( !view ) {
    return;
  }
  auto headword = view->getCurrentWord();

  if ( ui.favoritesPaneWidget->isWordPresentInActiveFolder( 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->removeWordFromActiveFav( headword ) ) {
        addToFavorites->setIcon( starIcon );
        addToFavorites->setToolTip( tr( "Add current tab to Favorites" ) );
      }
    }
  }
  else {
    ui.favoritesPaneWidget->addWordToActiveFav( headword );
    addToFavorites->setIcon( blueStarIcon );
    addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
  }
}


void MainWindow::addBookmarkToFavorite( const QString & text )
{
  // get current tab word.
  auto view = getCurrentArticleView();
  if ( !view ) {
    return;
  }
  auto word           = view->getCurrentWord();
  const auto bookmark = QString( "%1~~~%2" ).arg( word, text );

  ui.favoritesPaneWidget->addWordToActiveFav( bookmark );
}

void MainWindow::addAllTabsToFavorites()
{
  for ( int i = 0; i < ui.tabWidget->count(); i++ ) {
    auto view = qobject_cast< ArticleView * >( ui.tabWidget->widget( i ) );
    if ( !view ) {
      continue;
    }
    auto headword = view->getCurrentWord();
    ui.favoritesPaneWidget->addWordToActiveFav( headword );
  }
  addToFavorites->setIcon( blueStarIcon );
  addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
}

bool MainWindow::updateFavIcon( const QString & word )
{

  if ( ui.favoritesPaneWidget->isWordPresentInActiveFolder( word ) ) {
    addToFavorites->setIcon( blueStarIcon );
    addToFavorites->setToolTip( tr( "Remove current tab from Favorites" ) );
  }
  else {
    addToFavorites->setIcon( starIcon );
    addToFavorites->setToolTip( tr( "Add current tab to Favorites" ) );
  }

  if ( word.isEmpty() ) {
    return false;
  }

  return ui.favoritesPaneWidget->isWordPresentInActiveFolder( word );
}


void MainWindow::updateFavIconSlot()
{
  updateFavIcon( getCurrentArticleView()->getCurrentWord() );
}

void MainWindow::setGroupByName( const QString & 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() ) {
      qWarning( "Group \"%s\" for main window is not found", name.toUtf8().data() );
    }
  }
  else {
    ensureScanPopup();
    if ( scanPopup ) {
      emit setPopupGroupByName( name );
    }
  }
}

void MainWindow::headwordFromFavorites( const QString & headword, const QString & favFolderFullPath )
{
  if ( !favFolderFullPath.isEmpty() ) {
    // Find group by it Favorites folder
    for ( Instances::Groups::size_type i = 0; i < groupInstances.size(); i++ ) {
      if ( groupInstances[ i ].favoritesFolder == favFolderFullPath ) {
        // 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( "~~~" );

  setInputLineText( words[ 0 ], WildcardPolicy::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 ] );
}

void MainWindow::handleDownloadRequested( QWebEngineDownloadRequest * download )
{
  QString savePath;
  if ( cfg.resourceSavePath.isEmpty() ) {
    savePath = QDir::homePath();
  }
  else {
    savePath = QDir::fromNativeSeparators( cfg.resourceSavePath );
    if ( !QDir( savePath ).exists() ) {
      savePath = QDir::homePath();
    }
  }

  QString fileName =
    QFileDialog::getSaveFileName( this, tr( "Save File" ), savePath + "/" + download->downloadFileName() );

  if ( fileName.isEmpty() ) {
    return;
  }

  QFileInfo fileInfo( fileName );
  cfg.resourceSavePath = QDir::toNativeSeparators( fileInfo.absoluteDir().absolutePath() );

  download->setDownloadDirectory( fileInfo.absolutePath() );
  download->setDownloadFileName( fileInfo.fileName() );
  download->accept();
}
