Use compact format for JSON files
[qBittorrent.git] / src / gui / mainwindow.cpp
blob1dc459fdb11114f6b6713087b1144b29f5093a4a
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "mainwindow.h"
32 #include <QtSystemDetection>
34 #include <algorithm>
35 #include <chrono>
37 #include <QAction>
38 #include <QActionGroup>
39 #include <QClipboard>
40 #include <QCloseEvent>
41 #include <QComboBox>
42 #include <QDebug>
43 #include <QDesktopServices>
44 #include <QFileDialog>
45 #include <QFileSystemWatcher>
46 #include <QLabel>
47 #include <QMenu>
48 #include <QMessageBox>
49 #include <QMetaObject>
50 #include <QMimeData>
51 #include <QProcess>
52 #include <QPushButton>
53 #include <QShortcut>
54 #include <QSplitter>
55 #include <QStatusBar>
56 #include <QString>
57 #include <QTimer>
59 #ifdef Q_OS_WIN
60 #include <QCryptographicHash>
61 #include <QScopeGuard>
62 #endif
64 #include "base/bittorrent/session.h"
65 #include "base/bittorrent/sessionstatus.h"
66 #include "base/global.h"
67 #include "base/net/downloadmanager.h"
68 #include "base/path.h"
69 #include "base/preferences.h"
70 #include "base/rss/rss_folder.h"
71 #include "base/rss/rss_session.h"
72 #include "base/utils/foreignapps.h"
73 #include "base/utils/fs.h"
74 #include "base/utils/misc.h"
75 #include "base/utils/password.h"
76 #include "base/version.h"
77 #include "aboutdialog.h"
78 #include "autoexpandabledialog.h"
79 #include "cookiesdialog.h"
80 #include "desktopintegration.h"
81 #include "downloadfromurldialog.h"
82 #include "executionlogwidget.h"
83 #include "hidabletabwidget.h"
84 #include "interfaces/iguiapplication.h"
85 #include "lineedit.h"
86 #include "optionsdialog.h"
87 #include "powermanagement/powermanagement.h"
88 #include "properties/peerlistwidget.h"
89 #include "properties/propertieswidget.h"
90 #include "properties/proptabbar.h"
91 #include "rss/rsswidget.h"
92 #include "search/searchwidget.h"
93 #include "speedlimitdialog.h"
94 #include "statsdialog.h"
95 #include "statusbar.h"
96 #include "torrentcreatordialog.h"
97 #include "trackerlist/trackerlistwidget.h"
98 #include "transferlistfilterswidget.h"
99 #include "transferlistmodel.h"
100 #include "transferlistwidget.h"
101 #include "ui_mainwindow.h"
102 #include "uithememanager.h"
103 #include "utils.h"
105 #ifdef Q_OS_MACOS
106 #include "macosdockbadge/badger.h"
107 #endif
108 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
109 #include "programupdater.h"
110 #endif
112 using namespace std::chrono_literals;
114 namespace
116 #define SETTINGS_KEY(name) u"GUI/" name
117 #define EXECUTIONLOG_SETTINGS_KEY(name) (SETTINGS_KEY(u"Log/"_s) name)
119 const std::chrono::seconds PREVENT_SUSPEND_INTERVAL {60};
121 #ifdef Q_OS_WIN
122 const QString PYTHON_INSTALLER_URL = u"https://www.python.org/ftp/python/3.13.0/python-3.13.0-amd64.exe"_s;
123 const QByteArray PYTHON_INSTALLER_MD5 = QByteArrayLiteral("f5e5d48ba86586d4bef67bcb3790d339");
124 const QByteArray PYTHON_INSTALLER_SHA3_512 = QByteArrayLiteral("28ed23b82451efa5ec87e5dd18d7dacb9bc4d0a3643047091e5a687439f7e03a1c6e60ec64ee1210a0acaf2e5012504ff342ff27e5db108db05407e62aeff2f1");
125 #endif
128 MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, const QString &titleSuffix)
129 : GUIApplicationComponent(app)
130 , m_ui {new Ui::MainWindow}
131 , m_downloadRate {Utils::Misc::friendlyUnit(0, true)}
132 , m_uploadRate {Utils::Misc::friendlyUnit(0, true)}
133 , m_storeExecutionLogEnabled {EXECUTIONLOG_SETTINGS_KEY(u"Enabled"_s)}
134 , m_storeDownloadTrackerFavicon {SETTINGS_KEY(u"DownloadTrackerFavicon"_s)}
135 , m_storeExecutionLogTypes {EXECUTIONLOG_SETTINGS_KEY(u"Types"_s), Log::MsgType::ALL}
136 #ifdef Q_OS_MACOS
137 , m_badger {std::make_unique<MacUtils::Badger>()}
138 #endif // Q_OS_MACOS
140 m_ui->setupUi(this);
142 Preferences *const pref = Preferences::instance();
143 m_uiLocked = pref->isUILocked();
144 m_displaySpeedInTitle = pref->speedInTitleBar();
145 // Setting icons
146 #ifndef Q_OS_MACOS
147 setWindowIcon(UIThemeManager::instance()->getIcon(u"qbittorrent"_s));
148 #endif // Q_OS_MACOS
150 setTitleSuffix(titleSuffix);
152 #if (defined(Q_OS_UNIX))
153 m_ui->actionOptions->setText(tr("Preferences"));
154 #endif
156 addToolbarContextMenu();
158 m_ui->actionOpen->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_s));
159 m_ui->actionDownloadFromURL->setIcon(UIThemeManager::instance()->getIcon(u"insert-link"_s));
160 m_ui->actionSetGlobalSpeedLimits->setIcon(UIThemeManager::instance()->getIcon(u"speedometer"_s));
161 m_ui->actionCreateTorrent->setIcon(UIThemeManager::instance()->getIcon(u"torrent-creator"_s, u"document-edit"_s));
162 m_ui->actionAbout->setIcon(UIThemeManager::instance()->getIcon(u"help-about"_s));
163 m_ui->actionStatistics->setIcon(UIThemeManager::instance()->getIcon(u"view-statistics"_s));
164 m_ui->actionTopQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-top"_s));
165 m_ui->actionIncreaseQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-up"_s));
166 m_ui->actionDecreaseQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-down"_s));
167 m_ui->actionBottomQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-bottom"_s));
168 m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"list-remove"_s));
169 m_ui->actionDocumentation->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
170 m_ui->actionDonateMoney->setIcon(UIThemeManager::instance()->getIcon(u"wallet-open"_s));
171 m_ui->actionExit->setIcon(UIThemeManager::instance()->getIcon(u"application-exit"_s));
172 m_ui->actionLock->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
173 m_ui->actionOptions->setIcon(UIThemeManager::instance()->getIcon(u"configure"_s, u"preferences-system"_s));
174 m_ui->actionStart->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
175 m_ui->actionStop->setIcon(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s));
176 m_ui->actionPauseSession->setIcon(UIThemeManager::instance()->getIcon(u"pause-session"_s, u"media-playback-pause"_s));
177 m_ui->actionResumeSession->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
178 m_ui->menuAutoShutdownOnDownloadsCompletion->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_s, u"application-exit"_s));
179 m_ui->actionManageCookies->setIcon(UIThemeManager::instance()->getIcon(u"browser-cookies"_s, u"preferences-web-browser-cookies"_s));
180 m_ui->menuLog->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
181 m_ui->actionCheckForUpdates->setIcon(UIThemeManager::instance()->getIcon(u"view-refresh"_s));
183 m_ui->actionPauseSession->setVisible(!BitTorrent::Session::instance()->isPaused());
184 m_ui->actionResumeSession->setVisible(BitTorrent::Session::instance()->isPaused());
185 connect(BitTorrent::Session::instance(), &BitTorrent::Session::paused, this, [this]
187 m_ui->actionPauseSession->setVisible(false);
188 m_ui->actionResumeSession->setVisible(true);
189 refreshWindowTitle();
190 refreshTrayIconTooltip();
192 connect(BitTorrent::Session::instance(), &BitTorrent::Session::resumed, this, [this]
194 m_ui->actionPauseSession->setVisible(true);
195 m_ui->actionResumeSession->setVisible(false);
196 refreshWindowTitle();
197 refreshTrayIconTooltip();
200 auto *lockMenu = new QMenu(m_ui->menuView);
201 lockMenu->addAction(tr("&Set Password"), this, &MainWindow::defineUILockPassword);
202 lockMenu->addAction(tr("&Clear Password"), this, &MainWindow::clearUILockPassword);
203 m_ui->actionLock->setMenu(lockMenu);
205 updateAltSpeedsBtn(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled());
207 connect(BitTorrent::Session::instance(), &BitTorrent::Session::speedLimitModeChanged, this, &MainWindow::updateAltSpeedsBtn);
209 qDebug("create tabWidget");
210 m_tabs = new HidableTabWidget(this);
211 connect(m_tabs.data(), &QTabWidget::currentChanged, this, &MainWindow::tabChanged);
213 m_splitter = new QSplitter(Qt::Horizontal, this);
214 // vSplitter->setChildrenCollapsible(false);
216 auto *hSplitter = new QSplitter(Qt::Vertical, this);
217 hSplitter->setChildrenCollapsible(false);
218 hSplitter->setFrameShape(QFrame::NoFrame);
220 // Torrent filter
221 m_columnFilterEdit = new LineEdit;
222 m_columnFilterEdit->setPlaceholderText(tr("Filter torrents..."));
223 m_columnFilterEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
224 m_columnFilterEdit->setFixedWidth(200);
225 m_columnFilterEdit->setContextMenuPolicy(Qt::CustomContextMenu);
226 connect(m_columnFilterEdit, &QWidget::customContextMenuRequested, this, &MainWindow::showFilterContextMenu);
227 auto *columnFilterLabel = new QLabel(tr("Filter by:"));
228 m_columnFilterComboBox = new QComboBox;
229 QHBoxLayout *columnFilterLayout = new QHBoxLayout(m_columnFilterWidget);
230 columnFilterLayout->setContentsMargins(0, 0, 0, 0);
231 auto *columnFilterSpacer = new QWidget(this);
232 columnFilterSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
233 columnFilterLayout->addWidget(columnFilterSpacer);
234 columnFilterLayout->addWidget(m_columnFilterEdit);
235 columnFilterLayout->addWidget(columnFilterLabel, 0);
236 columnFilterLayout->addWidget(m_columnFilterComboBox, 0);
237 m_columnFilterWidget = new QWidget(this);
238 m_columnFilterWidget->setLayout(columnFilterLayout);
239 m_columnFilterAction = m_ui->toolBar->insertWidget(m_ui->actionLock, m_columnFilterWidget);
241 auto *spacer = new QWidget(this);
242 spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
243 m_ui->toolBar->insertWidget(m_columnFilterAction, spacer);
245 // Transfer List tab
246 m_transferListWidget = new TransferListWidget(app, this);
247 m_propertiesWidget = new PropertiesWidget(hSplitter);
248 connect(m_transferListWidget, &TransferListWidget::currentTorrentChanged, m_propertiesWidget, &PropertiesWidget::loadTorrentInfos);
249 hSplitter->addWidget(m_transferListWidget);
250 hSplitter->addWidget(m_propertiesWidget);
251 m_splitter->addWidget(hSplitter);
252 m_splitter->setCollapsible(0, false);
253 m_tabs->addTab(m_splitter,
254 #ifndef Q_OS_MACOS
255 UIThemeManager::instance()->getIcon(u"folder-remote"_s),
256 #endif
257 tr("Transfers"));
258 // Filter types
259 const QList<TransferListModel::Column> filterTypes = {TransferListModel::Column::TR_NAME, TransferListModel::Column::TR_SAVE_PATH};
260 for (const TransferListModel::Column type : filterTypes)
262 const QString typeName = m_transferListWidget->getSourceModel()->headerData(type, Qt::Horizontal, Qt::DisplayRole).value<QString>();
263 m_columnFilterComboBox->addItem(typeName, type);
265 connect(m_columnFilterComboBox, &QComboBox::currentIndexChanged, this, &MainWindow::applyTransferListFilter);
266 connect(m_columnFilterEdit, &LineEdit::textChanged, this, &MainWindow::applyTransferListFilter);
267 connect(hSplitter, &QSplitter::splitterMoved, this, &MainWindow::saveSettings);
268 connect(m_splitter, &QSplitter::splitterMoved, this, &MainWindow::saveSplitterSettings);
270 #ifdef Q_OS_MACOS
271 // Increase top spacing to avoid tab overlapping
272 m_ui->centralWidgetLayout->addSpacing(8);
273 #endif
275 m_ui->centralWidgetLayout->addWidget(m_tabs);
277 m_queueSeparator = m_ui->toolBar->insertSeparator(m_ui->actionTopQueuePos);
278 m_queueSeparatorMenu = m_ui->menuEdit->insertSeparator(m_ui->actionTopQueuePos);
280 #ifdef Q_OS_MACOS
281 for (QAction *action : asConst(m_ui->toolBar->actions()))
283 if (action->isSeparator())
285 QWidget *spacer = new QWidget(this);
286 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
287 spacer->setMinimumWidth(16);
288 m_ui->toolBar->insertWidget(action, spacer);
289 m_ui->toolBar->removeAction(action);
293 QWidget *spacer = new QWidget(this);
294 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
295 spacer->setMinimumWidth(8);
296 m_ui->toolBar->insertWidget(m_ui->actionDownloadFromURL, spacer);
299 QWidget *spacer = new QWidget(this);
300 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
301 spacer->setMinimumWidth(8);
302 m_ui->toolBar->addWidget(spacer);
304 #endif // Q_OS_MACOS
306 // Transfer list slots
307 connect(m_ui->actionStart, &QAction::triggered, m_transferListWidget, &TransferListWidget::startSelectedTorrents);
308 connect(m_ui->actionStop, &QAction::triggered, m_transferListWidget, &TransferListWidget::stopSelectedTorrents);
309 connect(m_ui->actionPauseSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::pauseSession);
310 connect(m_ui->actionResumeSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::resumeSession);
311 connect(m_ui->actionDelete, &QAction::triggered, m_transferListWidget, &TransferListWidget::softDeleteSelectedTorrents);
312 connect(m_ui->actionTopQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::topQueuePosSelectedTorrents);
313 connect(m_ui->actionIncreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::increaseQueuePosSelectedTorrents);
314 connect(m_ui->actionDecreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::decreaseQueuePosSelectedTorrents);
315 connect(m_ui->actionBottomQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::bottomQueuePosSelectedTorrents);
316 connect(m_ui->actionMinimize, &QAction::triggered, this, &MainWindow::minimizeWindow);
317 connect(m_ui->actionUseAlternativeSpeedLimits, &QAction::triggered, this, &MainWindow::toggleAlternativeSpeeds);
319 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
320 connect(m_ui->actionCheckForUpdates, &QAction::triggered, this, [this]() { checkProgramUpdate(true); });
322 // trigger an early check on startup
323 if (pref->isUpdateCheckEnabled())
324 checkProgramUpdate(false);
325 #else
326 m_ui->actionCheckForUpdates->setVisible(false);
327 #endif
329 // Certain menu items should reside at specific places on macOS.
330 // Qt partially does it on its own, but updates and different languages require tuning.
331 m_ui->actionExit->setMenuRole(QAction::QuitRole);
332 m_ui->actionAbout->setMenuRole(QAction::AboutRole);
333 m_ui->actionCheckForUpdates->setMenuRole(QAction::ApplicationSpecificRole);
334 m_ui->actionOptions->setMenuRole(QAction::PreferencesRole);
336 connect(m_ui->actionManageCookies, &QAction::triggered, this, &MainWindow::manageCookies);
338 // Initialise system sleep inhibition timer
339 m_pwr = new PowerManagement(this);
340 m_preventTimer = new QTimer(this);
341 m_preventTimer->setSingleShot(true);
342 connect(m_preventTimer, &QTimer::timeout, this, &MainWindow::updatePowerManagementState);
343 connect(pref, &Preferences::changed, this, &MainWindow::updatePowerManagementState);
344 updatePowerManagementState();
346 // Configure BT session according to options
347 loadPreferences();
349 connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &MainWindow::loadSessionStats);
350 connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated, this, &MainWindow::reloadTorrentStats);
352 createKeyboardShortcuts();
354 #ifdef Q_OS_MACOS
355 setUnifiedTitleAndToolBarOnMac(true);
356 #endif
358 // View settings
359 m_ui->actionTopToolBar->setChecked(pref->isToolbarDisplayed());
360 m_ui->actionShowStatusbar->setChecked(pref->isStatusbarDisplayed());
361 m_ui->actionSpeedInTitleBar->setChecked(pref->speedInTitleBar());
362 m_ui->actionRSSReader->setChecked(pref->isRSSWidgetEnabled());
363 m_ui->actionSearchWidget->setChecked(pref->isSearchEnabled());
364 m_ui->actionExecutionLogs->setChecked(isExecutionLogEnabled());
366 const Log::MsgTypes flags = executionLogMsgTypes();
367 m_ui->actionNormalMessages->setChecked(flags.testFlag(Log::NORMAL));
368 m_ui->actionInformationMessages->setChecked(flags.testFlag(Log::INFO));
369 m_ui->actionWarningMessages->setChecked(flags.testFlag(Log::WARNING));
370 m_ui->actionCriticalMessages->setChecked(flags.testFlag(Log::CRITICAL));
372 displayRSSTab(m_ui->actionRSSReader->isChecked());
373 on_actionExecutionLogs_triggered(m_ui->actionExecutionLogs->isChecked());
374 on_actionNormalMessages_triggered(m_ui->actionNormalMessages->isChecked());
375 on_actionInformationMessages_triggered(m_ui->actionInformationMessages->isChecked());
376 on_actionWarningMessages_triggered(m_ui->actionWarningMessages->isChecked());
377 on_actionCriticalMessages_triggered(m_ui->actionCriticalMessages->isChecked());
378 if (m_ui->actionSearchWidget->isChecked())
379 QMetaObject::invokeMethod(this, &MainWindow::on_actionSearchWidget_triggered, Qt::QueuedConnection);
380 // Auto shutdown actions
381 auto *autoShutdownGroup = new QActionGroup(this);
382 autoShutdownGroup->setExclusive(true);
383 autoShutdownGroup->addAction(m_ui->actionAutoShutdownDisabled);
384 autoShutdownGroup->addAction(m_ui->actionAutoExit);
385 autoShutdownGroup->addAction(m_ui->actionAutoShutdown);
386 autoShutdownGroup->addAction(m_ui->actionAutoSuspend);
387 autoShutdownGroup->addAction(m_ui->actionAutoHibernate);
388 #if (!defined(Q_OS_UNIX) || defined(Q_OS_MACOS)) || defined(QBT_USES_DBUS)
389 m_ui->actionAutoShutdown->setChecked(pref->shutdownWhenDownloadsComplete());
390 m_ui->actionAutoSuspend->setChecked(pref->suspendWhenDownloadsComplete());
391 m_ui->actionAutoHibernate->setChecked(pref->hibernateWhenDownloadsComplete());
392 #else
393 m_ui->actionAutoShutdown->setDisabled(true);
394 m_ui->actionAutoSuspend->setDisabled(true);
395 m_ui->actionAutoHibernate->setDisabled(true);
396 #endif
397 m_ui->actionAutoExit->setChecked(pref->shutdownqBTWhenDownloadsComplete());
399 if (!autoShutdownGroup->checkedAction())
400 m_ui->actionAutoShutdownDisabled->setChecked(true);
402 // Load Window state and sizes
403 loadSettings();
405 populateDesktopIntegrationMenu();
406 #ifndef Q_OS_MACOS
407 m_ui->actionLock->setVisible(app->desktopIntegration()->isActive());
408 connect(app->desktopIntegration(), &DesktopIntegration::stateChanged, this, [this, app]()
410 m_ui->actionLock->setVisible(app->desktopIntegration()->isActive());
412 #endif
413 connect(app->desktopIntegration(), &DesktopIntegration::notificationClicked, this, &MainWindow::desktopNotificationClicked);
414 connect(app->desktopIntegration(), &DesktopIntegration::activationRequested, this, [this]()
416 #ifdef Q_OS_MACOS
417 if (!isVisible())
418 activate();
419 #else
420 toggleVisibility();
421 #endif
424 #ifdef Q_OS_MACOS
425 if (initialState == WindowState::Normal)
427 show();
428 activateWindow();
429 raise();
431 else
433 // Make sure the Window is visible if we don't have a tray icon
434 showMinimized();
436 #else
437 if (app->desktopIntegration()->isActive())
439 if ((initialState == WindowState::Normal) && !m_uiLocked)
441 show();
442 activateWindow();
443 raise();
445 else if (initialState == WindowState::Minimized)
447 showMinimized();
448 if (pref->minimizeToTray())
450 hide();
451 if (!pref->minimizeToTrayNotified())
453 app->desktopIntegration()->showNotification(tr("qBittorrent is minimized to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
454 pref->setMinimizeToTrayNotified(true);
459 else
461 // Make sure the Window is visible if we don't have a tray icon
462 if (initialState != WindowState::Normal)
464 showMinimized();
466 else
468 show();
469 activateWindow();
470 raise();
473 #endif
475 const bool isFiltersSidebarVisible = pref->isFiltersSidebarVisible();
476 m_ui->actionShowFiltersSidebar->setChecked(isFiltersSidebarVisible);
477 if (isFiltersSidebarVisible)
479 showFiltersSidebar(true);
481 else
483 m_transferListWidget->applyStatusFilter(pref->getTransSelFilter());
484 m_transferListWidget->applyCategoryFilter(QString());
485 m_transferListWidget->applyTagFilter(std::nullopt);
486 m_transferListWidget->applyTrackerFilterAll();
489 // Start watching the executable for updates
490 m_executableWatcher = new QFileSystemWatcher(this);
491 connect(m_executableWatcher, &QFileSystemWatcher::fileChanged, this, &MainWindow::notifyOfUpdate);
492 m_executableWatcher->addPath(qApp->applicationFilePath());
494 m_transferListWidget->setFocus();
496 // Update the number of torrents (tab)
497 updateNbTorrents();
498 connect(m_transferListWidget->getSourceModel(), &QAbstractItemModel::rowsInserted, this, &MainWindow::updateNbTorrents);
499 connect(m_transferListWidget->getSourceModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateNbTorrents);
501 connect(pref, &Preferences::changed, this, &MainWindow::optionsSaved);
503 qDebug("GUI Built");
506 MainWindow::~MainWindow()
508 delete m_ui;
511 bool MainWindow::isExecutionLogEnabled() const
513 return m_storeExecutionLogEnabled;
516 void MainWindow::setExecutionLogEnabled(const bool value)
518 m_storeExecutionLogEnabled = value;
521 Log::MsgTypes MainWindow::executionLogMsgTypes() const
523 return m_storeExecutionLogTypes;
526 void MainWindow::setExecutionLogMsgTypes(const Log::MsgTypes value)
528 m_executionLog->setMessageTypes(value);
529 m_storeExecutionLogTypes = value;
532 bool MainWindow::isDownloadTrackerFavicon() const
534 return m_storeDownloadTrackerFavicon;
537 void MainWindow::setDownloadTrackerFavicon(const bool value)
539 if (m_transferListFiltersWidget)
540 m_transferListFiltersWidget->setDownloadTrackerFavicon(value);
541 m_storeDownloadTrackerFavicon = value;
544 void MainWindow::setTitleSuffix(const QString &suffix)
546 const auto emDash = QChar(0x2014);
547 const QString separator = u' ' + emDash + u' ';
548 m_windowTitle = QStringLiteral("qBittorrent " QBT_VERSION)
549 + (!suffix.isEmpty() ? (separator + suffix) : QString());
551 refreshWindowTitle();
554 void MainWindow::addToolbarContextMenu()
556 const Preferences *const pref = Preferences::instance();
557 m_toolbarMenu = new QMenu(this);
559 m_ui->toolBar->setContextMenuPolicy(Qt::CustomContextMenu);
560 connect(m_ui->toolBar, &QWidget::customContextMenuRequested, this, &MainWindow::toolbarMenuRequested);
562 QAction *iconsOnly = m_toolbarMenu->addAction(tr("Icons Only"), this, &MainWindow::toolbarIconsOnly);
563 QAction *textOnly = m_toolbarMenu->addAction(tr("Text Only"), this, &MainWindow::toolbarTextOnly);
564 QAction *textBesideIcons = m_toolbarMenu->addAction(tr("Text Alongside Icons"), this, &MainWindow::toolbarTextBeside);
565 QAction *textUnderIcons = m_toolbarMenu->addAction(tr("Text Under Icons"), this, &MainWindow::toolbarTextUnder);
566 QAction *followSystemStyle = m_toolbarMenu->addAction(tr("Follow System Style"), this, &MainWindow::toolbarFollowSystem);
568 auto *textPositionGroup = new QActionGroup(m_toolbarMenu);
569 textPositionGroup->addAction(iconsOnly);
570 iconsOnly->setCheckable(true);
571 textPositionGroup->addAction(textOnly);
572 textOnly->setCheckable(true);
573 textPositionGroup->addAction(textBesideIcons);
574 textBesideIcons->setCheckable(true);
575 textPositionGroup->addAction(textUnderIcons);
576 textUnderIcons->setCheckable(true);
577 textPositionGroup->addAction(followSystemStyle);
578 followSystemStyle->setCheckable(true);
580 const auto buttonStyle = static_cast<Qt::ToolButtonStyle>(pref->getToolbarTextPosition());
581 if ((buttonStyle >= Qt::ToolButtonIconOnly) && (buttonStyle <= Qt::ToolButtonFollowStyle))
582 m_ui->toolBar->setToolButtonStyle(buttonStyle);
583 switch (buttonStyle)
585 case Qt::ToolButtonIconOnly:
586 iconsOnly->setChecked(true);
587 break;
588 case Qt::ToolButtonTextOnly:
589 textOnly->setChecked(true);
590 break;
591 case Qt::ToolButtonTextBesideIcon:
592 textBesideIcons->setChecked(true);
593 break;
594 case Qt::ToolButtonTextUnderIcon:
595 textUnderIcons->setChecked(true);
596 break;
597 default:
598 followSystemStyle->setChecked(true);
602 void MainWindow::manageCookies()
604 auto *cookieDialog = new CookiesDialog(this);
605 cookieDialog->setAttribute(Qt::WA_DeleteOnClose);
606 cookieDialog->open();
609 void MainWindow::toolbarMenuRequested()
611 m_toolbarMenu->popup(QCursor::pos());
614 void MainWindow::toolbarIconsOnly()
616 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
617 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonIconOnly);
620 void MainWindow::toolbarTextOnly()
622 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
623 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextOnly);
626 void MainWindow::toolbarTextBeside()
628 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
629 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextBesideIcon);
632 void MainWindow::toolbarTextUnder()
634 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
635 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextUnderIcon);
638 void MainWindow::toolbarFollowSystem()
640 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
641 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonFollowStyle);
644 bool MainWindow::defineUILockPassword()
646 bool ok = false;
647 const QString newPassword = AutoExpandableDialog::getText(this, tr("UI lock password")
648 , tr("Please type the UI lock password:"), QLineEdit::Password, {}, &ok);
649 if (!ok)
650 return false;
652 if (newPassword.size() < 3)
654 QMessageBox::warning(this, tr("Invalid password"), tr("The password must be at least 3 characters long"));
655 return false;
658 Preferences::instance()->setUILockPassword(Utils::Password::PBKDF2::generate(newPassword));
659 return true;
662 void MainWindow::clearUILockPassword()
664 const QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Clear the password")
665 , tr("Are you sure you want to clear the password?"), (QMessageBox::Yes | QMessageBox::No), QMessageBox::No);
666 if (answer == QMessageBox::Yes)
667 Preferences::instance()->setUILockPassword({});
670 void MainWindow::on_actionLock_triggered()
672 Preferences *const pref = Preferences::instance();
674 // Check if there is a password
675 if (pref->getUILockPassword().isEmpty())
677 if (!defineUILockPassword())
678 return;
681 // Lock the interface
682 m_uiLocked = true;
683 pref->setUILocked(true);
684 app()->desktopIntegration()->menu()->setEnabled(false);
685 hide();
688 void MainWindow::handleRSSUnreadCountUpdated(int count)
690 m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count));
693 void MainWindow::displayRSSTab(bool enable)
695 if (enable)
697 // RSS tab
698 if (!m_rssWidget)
700 m_rssWidget = new RSSWidget(app(), m_tabs);
701 connect(m_rssWidget.data(), &RSSWidget::unreadCountUpdated, this, &MainWindow::handleRSSUnreadCountUpdated);
702 #ifdef Q_OS_MACOS
703 m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
704 #else
705 const int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
706 m_tabs->setTabIcon(indexTab, UIThemeManager::instance()->getIcon(u"application-rss"_s));
707 #endif
710 else
712 delete m_rssWidget;
716 void MainWindow::showFilterContextMenu()
718 const Preferences *pref = Preferences::instance();
720 QMenu *menu = m_columnFilterEdit->createStandardContextMenu();
721 menu->setAttribute(Qt::WA_DeleteOnClose);
722 menu->addSeparator();
724 QAction *useRegexAct = menu->addAction(tr("Use regular expressions"));
725 useRegexAct->setCheckable(true);
726 useRegexAct->setChecked(pref->getRegexAsFilteringPatternForTransferList());
727 connect(useRegexAct, &QAction::toggled, pref, &Preferences::setRegexAsFilteringPatternForTransferList);
728 connect(useRegexAct, &QAction::toggled, this, &MainWindow::applyTransferListFilter);
730 menu->popup(QCursor::pos());
733 void MainWindow::displaySearchTab(bool enable)
735 Preferences::instance()->setSearchEnabled(enable);
736 if (enable)
738 // RSS tab
739 if (!m_searchWidget)
741 m_searchWidget = new SearchWidget(app(), this);
742 connect(m_searchWidget, &SearchWidget::searchFinished, this, [this](const bool failed)
744 if (app()->desktopIntegration()->isNotificationsEnabled() && (currentTabWidget() != m_searchWidget))
746 if (failed)
747 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Search has failed"));
748 else
749 app()->desktopIntegration()->showNotification(tr("Search Engine"), tr("Search has finished"));
752 m_tabs->insertTab(1, m_searchWidget,
753 #ifndef Q_OS_MACOS
754 UIThemeManager::instance()->getIcon(u"edit-find"_s),
755 #endif
756 tr("Search"));
759 else
761 delete m_searchWidget;
765 void MainWindow::toggleFocusBetweenLineEdits()
767 if (m_columnFilterEdit->hasFocus() && (m_propertiesWidget->tabBar()->currentIndex() == PropTabBar::FilesTab))
769 m_propertiesWidget->contentFilterLine()->setFocus();
770 m_propertiesWidget->contentFilterLine()->selectAll();
772 else
774 m_columnFilterEdit->setFocus();
775 m_columnFilterEdit->selectAll();
779 void MainWindow::updateNbTorrents()
781 m_tabs->setTabText(0, tr("Transfers (%1)").arg(m_transferListWidget->getSourceModel()->rowCount()));
784 void MainWindow::on_actionDocumentation_triggered() const
786 QDesktopServices::openUrl(QUrl(u"https://doc.qbittorrent.org"_s));
789 void MainWindow::tabChanged([[maybe_unused]] const int newTab)
791 // We cannot rely on the index newTab
792 // because the tab order is undetermined now
793 if (m_tabs->currentWidget() == m_splitter)
795 qDebug("Changed tab to transfer list, refreshing the list");
796 m_propertiesWidget->loadDynamicData();
797 m_columnFilterAction->setVisible(true);
798 return;
800 m_columnFilterAction->setVisible(false);
802 if (m_tabs->currentWidget() == m_searchWidget)
804 qDebug("Changed tab to search engine, giving focus to search input");
805 m_searchWidget->giveFocusToSearchInput();
809 void MainWindow::saveSettings() const
811 auto *pref = Preferences::instance();
812 pref->setMainGeometry(saveGeometry());
813 m_propertiesWidget->saveSettings();
816 void MainWindow::saveSplitterSettings() const
818 if (!m_transferListFiltersWidget)
819 return;
821 auto *pref = Preferences::instance();
822 pref->setFiltersSidebarWidth(m_splitter->sizes()[0]);
825 void MainWindow::cleanup()
827 if (!m_neverShown)
829 saveSettings();
830 saveSplitterSettings();
833 // delete RSSWidget explicitly to avoid crash in
834 // handleRSSUnreadCountUpdated() at application shutdown
835 delete m_rssWidget;
837 delete m_executableWatcher;
839 m_preventTimer->stop();
841 #if (defined(Q_OS_WIN) || defined(Q_OS_MACOS))
842 if (m_programUpdateTimer)
843 m_programUpdateTimer->stop();
844 #endif
846 // remove all child widgets
847 while (auto *w = findChild<QWidget *>())
848 delete w;
851 void MainWindow::loadSettings()
853 const auto *pref = Preferences::instance();
855 if (const QByteArray mainGeo = pref->getMainGeometry();
856 !mainGeo.isEmpty() && restoreGeometry(mainGeo))
858 m_posInitialized = true;
862 void MainWindow::desktopNotificationClicked()
864 if (isHidden())
866 if (m_uiLocked)
868 // Ask for UI lock password
869 if (!unlockUI())
870 return;
872 show();
873 if (isMinimized())
874 showNormal();
877 raise();
878 activateWindow();
881 void MainWindow::createKeyboardShortcuts()
883 m_ui->actionCreateTorrent->setShortcut(QKeySequence::New);
884 m_ui->actionOpen->setShortcut(QKeySequence::Open);
885 m_ui->actionDelete->setShortcut(QKeySequence::Delete);
886 m_ui->actionDelete->setShortcutContext(Qt::WidgetShortcut); // nullify its effect: delete key event is handled by respective widgets, not here
887 m_ui->actionDownloadFromURL->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O);
888 m_ui->actionExit->setShortcut(Qt::CTRL | Qt::Key_Q);
889 #ifdef Q_OS_MACOS
890 m_ui->actionCloseWindow->setShortcut(QKeySequence::Close);
891 #else
892 m_ui->actionCloseWindow->setVisible(false);
893 #endif
895 const auto *switchTransferShortcut = new QShortcut((Qt::ALT | Qt::Key_1), this);
896 connect(switchTransferShortcut, &QShortcut::activated, this, &MainWindow::displayTransferTab);
897 const auto *switchSearchShortcut = new QShortcut((Qt::ALT | Qt::Key_2), this);
898 connect(switchSearchShortcut, &QShortcut::activated, this, qOverload<>(&MainWindow::displaySearchTab));
899 const auto *switchRSSShortcut = new QShortcut((Qt::ALT | Qt::Key_3), this);
900 connect(switchRSSShortcut, &QShortcut::activated, this, qOverload<>(&MainWindow::displayRSSTab));
901 const auto *switchExecutionLogShortcut = new QShortcut((Qt::ALT | Qt::Key_4), this);
902 connect(switchExecutionLogShortcut, &QShortcut::activated, this, &MainWindow::displayExecutionLogTab);
903 const auto *switchSearchFilterShortcut = new QShortcut(QKeySequence::Find, m_transferListWidget);
904 connect(switchSearchFilterShortcut, &QShortcut::activated, this, &MainWindow::toggleFocusBetweenLineEdits);
905 const auto *switchSearchFilterShortcutAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), m_transferListWidget);
906 connect(switchSearchFilterShortcutAlternative, &QShortcut::activated, this, &MainWindow::toggleFocusBetweenLineEdits);
908 m_ui->actionDocumentation->setShortcut(QKeySequence::HelpContents);
909 m_ui->actionOptions->setShortcut(Qt::ALT | Qt::Key_O);
910 m_ui->actionStatistics->setShortcut(Qt::CTRL | Qt::Key_I);
911 m_ui->actionStart->setShortcut(Qt::CTRL | Qt::Key_S);
912 m_ui->actionStop->setShortcut(Qt::CTRL | Qt::Key_P);
913 m_ui->actionPauseSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
914 m_ui->actionResumeSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
915 m_ui->actionBottomQueuePos->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Minus);
916 m_ui->actionDecreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Minus);
917 m_ui->actionIncreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Plus);
918 m_ui->actionTopQueuePos->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Plus);
919 #ifdef Q_OS_MACOS
920 m_ui->actionMinimize->setShortcut(Qt::CTRL | Qt::Key_M);
921 addAction(m_ui->actionMinimize);
922 #endif
925 // Keyboard shortcuts slots
926 void MainWindow::displayTransferTab() const
928 m_tabs->setCurrentWidget(m_splitter);
931 void MainWindow::displaySearchTab()
933 if (!m_searchWidget)
935 m_ui->actionSearchWidget->setChecked(true);
936 displaySearchTab(true);
939 m_tabs->setCurrentWidget(m_searchWidget);
942 void MainWindow::displayRSSTab()
944 if (!m_rssWidget)
946 m_ui->actionRSSReader->setChecked(true);
947 displayRSSTab(true);
950 m_tabs->setCurrentWidget(m_rssWidget);
953 void MainWindow::displayExecutionLogTab()
955 if (!m_executionLog)
957 m_ui->actionExecutionLogs->setChecked(true);
958 on_actionExecutionLogs_triggered(true);
961 m_tabs->setCurrentWidget(m_executionLog);
964 // End of keyboard shortcuts slots
966 void MainWindow::on_actionSetGlobalSpeedLimits_triggered()
968 auto *dialog = new SpeedLimitDialog {this};
969 dialog->setAttribute(Qt::WA_DeleteOnClose);
970 dialog->open();
973 // Necessary if we want to close the window
974 // in one time if "close to systray" is enabled
975 void MainWindow::on_actionExit_triggered()
977 // UI locking enforcement.
978 if (isHidden() && m_uiLocked)
979 // Ask for UI lock password
980 if (!unlockUI()) return;
982 m_forceExit = true;
983 close();
986 #ifdef Q_OS_MACOS
987 void MainWindow::on_actionCloseWindow_triggered()
989 // On macOS window close is basically equivalent to window hide.
990 // If you decide to implement this functionality for other OS,
991 // then you will also need ui lock checks like in actionExit.
992 close();
994 #endif
996 QWidget *MainWindow::currentTabWidget() const
998 if (isMinimized() || !isVisible())
999 return nullptr;
1000 if (m_tabs->currentIndex() == 0)
1001 return m_transferListWidget;
1002 return m_tabs->currentWidget();
1005 TransferListWidget *MainWindow::transferListWidget() const
1007 return m_transferListWidget;
1010 bool MainWindow::unlockUI()
1012 if (m_unlockDlgShowing)
1013 return false;
1015 bool ok = false;
1016 const QString password = AutoExpandableDialog::getText(this, tr("UI lock password")
1017 , tr("Please type the UI lock password:"), QLineEdit::Password, {}, &ok);
1018 if (!ok) return false;
1020 Preferences *const pref = Preferences::instance();
1022 const QByteArray secret = pref->getUILockPassword();
1023 if (!Utils::Password::PBKDF2::verify(secret, password))
1025 QMessageBox::warning(this, tr("Invalid password"), tr("The password is invalid"));
1026 return false;
1029 m_uiLocked = false;
1030 pref->setUILocked(false);
1031 app()->desktopIntegration()->menu()->setEnabled(true);
1032 return true;
1035 void MainWindow::notifyOfUpdate(const QString &)
1037 // Show restart message
1038 m_statusBar->showRestartRequired();
1039 LogMsg(tr("qBittorrent was just updated and needs to be restarted for the changes to be effective.")
1040 , Log::CRITICAL);
1041 // Delete the executable watcher
1042 delete m_executableWatcher;
1043 m_executableWatcher = nullptr;
1046 #ifndef Q_OS_MACOS
1047 // Toggle Main window visibility
1048 void MainWindow::toggleVisibility()
1050 if (isHidden())
1052 if (m_uiLocked && !unlockUI()) // Ask for UI lock password
1053 return;
1055 // Make sure the window is not minimized
1056 setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
1058 // Then show it
1059 show();
1060 raise();
1061 activateWindow();
1063 else
1065 hide();
1068 #endif // Q_OS_MACOS
1070 // Display About Dialog
1071 void MainWindow::on_actionAbout_triggered()
1073 // About dialog
1074 if (m_aboutDlg)
1076 m_aboutDlg->activateWindow();
1078 else
1080 m_aboutDlg = new AboutDialog(this);
1081 m_aboutDlg->setAttribute(Qt::WA_DeleteOnClose);
1082 m_aboutDlg->show();
1086 void MainWindow::on_actionStatistics_triggered()
1088 if (m_statsDlg)
1090 m_statsDlg->activateWindow();
1092 else
1094 m_statsDlg = new StatsDialog(this);
1095 m_statsDlg->setAttribute(Qt::WA_DeleteOnClose);
1096 m_statsDlg->show();
1100 void MainWindow::showEvent(QShowEvent *e)
1102 qDebug("** Show Event **");
1103 e->accept();
1105 if (isVisible())
1107 // preparations before showing the window
1109 if (m_neverShown)
1111 m_propertiesWidget->readSettings();
1112 m_neverShown = false;
1115 if (currentTabWidget() == m_transferListWidget)
1116 m_propertiesWidget->loadDynamicData();
1118 // Make sure the window is initially centered
1119 if (!m_posInitialized)
1121 move(Utils::Gui::screenCenter(this));
1122 m_posInitialized = true;
1125 else
1127 // to avoid blank screen when restoring from tray icon
1128 show();
1132 void MainWindow::keyPressEvent(QKeyEvent *event)
1134 if (event->matches(QKeySequence::Paste))
1136 const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData();
1137 if (mimeData->hasText())
1139 const QStringList lines = mimeData->text().split(u'\n', Qt::SkipEmptyParts);
1140 for (QString line : lines)
1142 line = line.trimmed();
1143 if (!Utils::Misc::isTorrentLink(line))
1144 continue;
1146 app()->addTorrentManager()->addTorrent(line);
1149 return;
1153 QMainWindow::keyPressEvent(event);
1156 // Called when we close the program
1157 void MainWindow::closeEvent(QCloseEvent *e)
1159 Preferences *const pref = Preferences::instance();
1160 #ifdef Q_OS_MACOS
1161 if (!m_forceExit)
1163 hide();
1164 e->accept();
1165 return;
1167 #else
1168 const bool goToSystrayOnExit = pref->closeToTray();
1169 if (!m_forceExit && app()->desktopIntegration()->isActive() && goToSystrayOnExit && !this->isHidden())
1171 e->ignore();
1172 QMetaObject::invokeMethod(this, &QWidget::hide, Qt::QueuedConnection);
1173 if (!pref->closeToTrayNotified())
1175 app()->desktopIntegration()->showNotification(tr("qBittorrent is closed to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
1176 pref->setCloseToTrayNotified(true);
1178 return;
1180 #endif // Q_OS_MACOS
1182 const QList<BitTorrent::Torrent *> allTorrents = BitTorrent::Session::instance()->torrents();
1183 const bool hasActiveTorrents = std::any_of(allTorrents.cbegin(), allTorrents.cend(), [](BitTorrent::Torrent *torrent)
1185 return torrent->isActive();
1187 if (pref->confirmOnExit() && hasActiveTorrents)
1189 if (e->spontaneous() || m_forceExit)
1191 if (!isVisible())
1192 show();
1193 QMessageBox confirmBox(QMessageBox::Question, tr("Exiting qBittorrent"),
1194 // Split it because the last sentence is used in the WebUI
1195 tr("Some files are currently transferring.") + u'\n' + tr("Are you sure you want to quit qBittorrent?"),
1196 QMessageBox::NoButton, this);
1197 QPushButton *noBtn = confirmBox.addButton(tr("&No"), QMessageBox::NoRole);
1198 confirmBox.addButton(tr("&Yes"), QMessageBox::YesRole);
1199 QPushButton *alwaysBtn = confirmBox.addButton(tr("&Always Yes"), QMessageBox::YesRole);
1200 confirmBox.setDefaultButton(noBtn);
1201 confirmBox.exec();
1202 if (!confirmBox.clickedButton() || (confirmBox.clickedButton() == noBtn))
1204 // Cancel exit
1205 e->ignore();
1206 m_forceExit = false;
1207 return;
1209 if (confirmBox.clickedButton() == alwaysBtn)
1210 // Remember choice
1211 Preferences::instance()->setConfirmOnExit(false);
1215 // Accept exit
1216 e->accept();
1217 qApp->exit();
1220 // Display window to create a torrent
1221 void MainWindow::on_actionCreateTorrent_triggered()
1223 createTorrentTriggered({});
1226 void MainWindow::createTorrentTriggered(const Path &path)
1228 if (m_createTorrentDlg)
1230 m_createTorrentDlg->updateInputPath(path);
1231 m_createTorrentDlg->activateWindow();
1233 else
1235 m_createTorrentDlg = new TorrentCreatorDialog(this, path);
1236 m_createTorrentDlg->setAttribute(Qt::WA_DeleteOnClose);
1237 m_createTorrentDlg->show();
1241 bool MainWindow::event(QEvent *e)
1243 #ifndef Q_OS_MACOS
1244 switch (e->type())
1246 case QEvent::WindowStateChange:
1247 qDebug("Window change event");
1248 // Now check to see if the window is minimised
1249 if (isMinimized())
1251 qDebug("minimisation");
1252 Preferences *const pref = Preferences::instance();
1253 if (app()->desktopIntegration()->isActive() && pref->minimizeToTray())
1255 qDebug() << "Has active window:" << (qApp->activeWindow() != nullptr);
1256 // Check if there is a modal window
1257 const QWidgetList allWidgets = QApplication::allWidgets();
1258 const bool hasModalWindow = std::any_of(allWidgets.cbegin(), allWidgets.cend()
1259 , [](const QWidget *widget) { return widget->isModal(); });
1260 // Iconify if there is no modal window
1261 if (!hasModalWindow)
1263 qDebug("Minimize to Tray enabled, hiding!");
1264 e->ignore();
1265 QMetaObject::invokeMethod(this, &QWidget::hide, Qt::QueuedConnection);
1266 if (!pref->minimizeToTrayNotified())
1268 app()->desktopIntegration()->showNotification(tr("qBittorrent is minimized to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
1269 pref->setMinimizeToTrayNotified(true);
1271 return true;
1275 break;
1276 case QEvent::ToolBarChange:
1278 qDebug("MAC: Received a toolbar change event!");
1279 const bool ret = QMainWindow::event(e);
1281 qDebug("MAC: new toolbar visibility is %d", !m_ui->actionTopToolBar->isChecked());
1282 m_ui->actionTopToolBar->toggle();
1283 Preferences::instance()->setToolbarDisplayed(m_ui->actionTopToolBar->isChecked());
1284 return ret;
1286 default:
1287 break;
1289 #endif // Q_OS_MACOS
1291 return QMainWindow::event(e);
1294 // Display a dialog to allow user to add
1295 // torrents to download list
1296 void MainWindow::on_actionOpen_triggered()
1298 Preferences *const pref = Preferences::instance();
1299 // Open File Open Dialog
1300 // Note: it is possible to select more than one file
1301 const QStringList pathsList = QFileDialog::getOpenFileNames(this, tr("Open Torrent Files")
1302 , pref->getMainLastDir().data(), tr("Torrent Files") + u" (*" + TORRENT_FILE_EXTENSION + u')');
1304 if (pathsList.isEmpty())
1305 return;
1307 for (const QString &file : pathsList)
1308 app()->addTorrentManager()->addTorrent(file);
1310 // Save last dir to remember it
1311 const Path topDir {pathsList.at(0)};
1312 const Path parentDir = topDir.parentPath();
1313 pref->setMainLastDir(parentDir.isEmpty() ? topDir : parentDir);
1316 void MainWindow::activate()
1318 if (!m_uiLocked || unlockUI())
1320 show();
1321 activateWindow();
1322 raise();
1326 void MainWindow::optionsSaved()
1328 LogMsg(tr("Options saved."));
1329 loadPreferences();
1332 void MainWindow::showStatusBar(bool show)
1334 if (!show)
1336 // Remove status bar
1337 setStatusBar(nullptr);
1339 else if (!m_statusBar)
1341 // Create status bar
1342 m_statusBar = new StatusBar;
1343 connect(m_statusBar.data(), &StatusBar::connectionButtonClicked, this, &MainWindow::showConnectionSettings);
1344 connect(m_statusBar.data(), &StatusBar::alternativeSpeedsButtonClicked, this, &MainWindow::toggleAlternativeSpeeds);
1345 setStatusBar(m_statusBar);
1349 void MainWindow::showFiltersSidebar(const bool show)
1351 if (show && !m_transferListFiltersWidget)
1353 m_transferListFiltersWidget = new TransferListFiltersWidget(m_splitter, m_transferListWidget, isDownloadTrackerFavicon());
1354 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersAdded, m_transferListFiltersWidget, &TransferListFiltersWidget::addTrackers);
1355 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersRemoved, m_transferListFiltersWidget, &TransferListFiltersWidget::removeTrackers);
1356 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_transferListFiltersWidget, &TransferListFiltersWidget::refreshTrackers);
1357 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntryStatusesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntryStatusesUpdated);
1359 m_splitter->insertWidget(0, m_transferListFiltersWidget);
1360 m_splitter->setCollapsible(0, true);
1361 // From https://doc.qt.io/qt-5/qsplitter.html#setSizes:
1362 // Instead, any additional/missing space is distributed amongst the widgets
1363 // according to the relative weight of the sizes.
1364 m_splitter->setStretchFactor(0, 0);
1365 m_splitter->setStretchFactor(1, 1);
1366 m_splitter->setSizes({Preferences::instance()->getFiltersSidebarWidth()});
1368 else if (!show && m_transferListFiltersWidget)
1370 saveSplitterSettings();
1371 delete m_transferListFiltersWidget;
1372 m_transferListFiltersWidget = nullptr;
1376 void MainWindow::loadPreferences()
1378 const Preferences *pref = Preferences::instance();
1380 // General
1381 if (pref->isToolbarDisplayed())
1383 m_ui->toolBar->setVisible(true);
1385 else
1387 // Clear search filter before hiding the top toolbar
1388 m_columnFilterEdit->clear();
1389 m_ui->toolBar->setVisible(false);
1392 showStatusBar(pref->isStatusbarDisplayed());
1394 m_transferListWidget->setAlternatingRowColors(pref->useAlternatingRowColors());
1395 m_propertiesWidget->getFilesList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1396 m_propertiesWidget->getTrackerList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1397 m_propertiesWidget->getPeerList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1399 // Queueing System
1400 if (BitTorrent::Session::instance()->isQueueingSystemEnabled())
1402 if (!m_ui->actionDecreaseQueuePos->isVisible())
1404 m_transferListWidget->hideQueuePosColumn(false);
1405 m_ui->actionDecreaseQueuePos->setVisible(true);
1406 m_ui->actionIncreaseQueuePos->setVisible(true);
1407 m_ui->actionTopQueuePos->setVisible(true);
1408 m_ui->actionBottomQueuePos->setVisible(true);
1409 #ifndef Q_OS_MACOS
1410 m_queueSeparator->setVisible(true);
1411 #endif
1412 m_queueSeparatorMenu->setVisible(true);
1415 else
1417 if (m_ui->actionDecreaseQueuePos->isVisible())
1419 m_transferListWidget->hideQueuePosColumn(true);
1420 m_ui->actionDecreaseQueuePos->setVisible(false);
1421 m_ui->actionIncreaseQueuePos->setVisible(false);
1422 m_ui->actionTopQueuePos->setVisible(false);
1423 m_ui->actionBottomQueuePos->setVisible(false);
1424 #ifndef Q_OS_MACOS
1425 m_queueSeparator->setVisible(false);
1426 #endif
1427 m_queueSeparatorMenu->setVisible(false);
1431 // Torrent properties
1432 m_propertiesWidget->reloadPreferences();
1434 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1435 if (pref->isUpdateCheckEnabled())
1437 if (!m_programUpdateTimer)
1439 m_programUpdateTimer = new QTimer(this);
1440 m_programUpdateTimer->setInterval(24h);
1441 m_programUpdateTimer->setSingleShot(true);
1442 connect(m_programUpdateTimer, &QTimer::timeout, this, [this]() { checkProgramUpdate(false); });
1443 m_programUpdateTimer->start();
1446 else
1448 delete m_programUpdateTimer;
1449 m_programUpdateTimer = nullptr;
1451 #endif
1453 qDebug("GUI settings loaded");
1456 void MainWindow::loadSessionStats()
1458 const auto *btSession = BitTorrent::Session::instance();
1459 const BitTorrent::SessionStatus &status = btSession->status();
1460 m_downloadRate = Utils::Misc::friendlyUnit(status.payloadDownloadRate, true);
1461 m_uploadRate = Utils::Misc::friendlyUnit(status.payloadUploadRate, true);
1463 // update global information
1464 #ifdef Q_OS_MACOS
1465 m_badger->updateSpeed(status.payloadDownloadRate, status.payloadUploadRate);
1466 #else
1467 refreshTrayIconTooltip();
1468 #endif // Q_OS_MACOS
1470 refreshWindowTitle();
1473 void MainWindow::reloadTorrentStats(const QList<BitTorrent::Torrent *> &torrents)
1475 if (currentTabWidget() == m_transferListWidget)
1477 if (torrents.contains(m_propertiesWidget->getCurrentTorrent()))
1478 m_propertiesWidget->loadDynamicData();
1482 void MainWindow::downloadFromURLList(const QStringList &urlList)
1484 for (const QString &url : urlList)
1485 app()->addTorrentManager()->addTorrent(url);
1488 void MainWindow::populateDesktopIntegrationMenu()
1490 auto *menu = app()->desktopIntegration()->menu();
1491 menu->clear();
1493 #ifndef Q_OS_MACOS
1494 connect(menu, &QMenu::aboutToShow, this, [this]()
1496 m_ui->actionToggleVisibility->setText(isVisible() ? tr("Hide") : tr("Show"));
1498 connect(m_ui->actionToggleVisibility, &QAction::triggered, this, &MainWindow::toggleVisibility);
1500 menu->addAction(m_ui->actionToggleVisibility);
1501 menu->addSeparator();
1502 #endif
1504 menu->addAction(m_ui->actionOpen);
1505 menu->addAction(m_ui->actionDownloadFromURL);
1506 menu->addSeparator();
1508 menu->addAction(m_ui->actionUseAlternativeSpeedLimits);
1509 menu->addAction(m_ui->actionSetGlobalSpeedLimits);
1510 menu->addSeparator();
1512 menu->addAction(m_ui->actionResumeSession);
1513 menu->addAction(m_ui->actionPauseSession);
1515 #ifndef Q_OS_MACOS
1516 menu->addSeparator();
1517 menu->addAction(m_ui->actionExit);
1518 #endif
1520 if (m_uiLocked)
1521 menu->setEnabled(false);
1524 void MainWindow::updateAltSpeedsBtn(const bool alternative)
1526 m_ui->actionUseAlternativeSpeedLimits->setChecked(alternative);
1529 PropertiesWidget *MainWindow::propertiesWidget() const
1531 return m_propertiesWidget;
1534 // Display Program Options
1535 void MainWindow::on_actionOptions_triggered()
1537 if (m_options)
1539 m_options->activateWindow();
1541 else
1543 m_options = new OptionsDialog(app(), this);
1544 m_options->setAttribute(Qt::WA_DeleteOnClose);
1545 m_options->open();
1549 void MainWindow::on_actionTopToolBar_triggered()
1551 const bool isVisible = static_cast<QAction *>(sender())->isChecked();
1552 m_ui->toolBar->setVisible(isVisible);
1553 Preferences::instance()->setToolbarDisplayed(isVisible);
1556 void MainWindow::on_actionShowStatusbar_triggered()
1558 const bool isVisible = static_cast<QAction *>(sender())->isChecked();
1559 Preferences::instance()->setStatusbarDisplayed(isVisible);
1560 showStatusBar(isVisible);
1563 void MainWindow::on_actionShowFiltersSidebar_triggered(const bool checked)
1565 Preferences *const pref = Preferences::instance();
1566 pref->setFiltersSidebarVisible(checked);
1567 showFiltersSidebar(checked);
1570 void MainWindow::on_actionSpeedInTitleBar_triggered()
1572 m_displaySpeedInTitle = static_cast<QAction *>(sender())->isChecked();
1573 Preferences::instance()->showSpeedInTitleBar(m_displaySpeedInTitle);
1574 refreshWindowTitle();
1577 void MainWindow::on_actionRSSReader_triggered()
1579 Preferences::instance()->setRSSWidgetVisible(m_ui->actionRSSReader->isChecked());
1580 displayRSSTab(m_ui->actionRSSReader->isChecked());
1583 void MainWindow::on_actionSearchWidget_triggered()
1585 if (m_ui->actionSearchWidget->isChecked())
1587 const Utils::ForeignApps::PythonInfo pyInfo = Utils::ForeignApps::pythonInfo();
1589 // Not found
1590 if (!pyInfo.isValid())
1592 m_ui->actionSearchWidget->setChecked(false);
1593 Preferences::instance()->setSearchEnabled(false);
1595 #ifdef Q_OS_WIN
1596 const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Missing Python Runtime")
1597 , tr("Python is required to use the search engine but it does not seem to be installed.\nDo you want to install it now?")
1598 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
1599 if (buttonPressed == QMessageBox::Yes)
1600 installPython();
1601 #else
1602 QMessageBox::information(this, tr("Missing Python Runtime")
1603 , tr("Python is required to use the search engine but it does not seem to be installed."));
1604 #endif
1605 return;
1608 // Check version requirement
1609 if (!pyInfo.isSupportedVersion())
1611 m_ui->actionSearchWidget->setChecked(false);
1612 Preferences::instance()->setSearchEnabled(false);
1614 #ifdef Q_OS_WIN
1615 const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
1616 , tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
1617 .arg(pyInfo.version.toString(), u"3.9.0")
1618 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
1619 if (buttonPressed == QMessageBox::Yes)
1620 installPython();
1621 #else
1622 QMessageBox::information(this, tr("Old Python Runtime")
1623 , tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
1624 .arg(pyInfo.version.toString(), u"3.9.0"));
1625 #endif
1626 return;
1629 m_ui->actionSearchWidget->setChecked(true);
1630 Preferences::instance()->setSearchEnabled(true);
1633 displaySearchTab(m_ui->actionSearchWidget->isChecked());
1636 // Display an input dialog to prompt user for
1637 // an url
1638 void MainWindow::on_actionDownloadFromURL_triggered()
1640 if (!m_downloadFromURLDialog)
1642 m_downloadFromURLDialog = new DownloadFromURLDialog(this);
1643 m_downloadFromURLDialog->setAttribute(Qt::WA_DeleteOnClose);
1644 connect(m_downloadFromURLDialog.data(), &DownloadFromURLDialog::urlsReadyToBeDownloaded, this, &MainWindow::downloadFromURLList);
1645 m_downloadFromURLDialog->open();
1649 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1650 void MainWindow::handleUpdateCheckFinished(ProgramUpdater *updater, const bool invokedByUser)
1652 m_ui->actionCheckForUpdates->setEnabled(true);
1653 m_ui->actionCheckForUpdates->setText(tr("&Check for Updates"));
1654 m_ui->actionCheckForUpdates->setToolTip(tr("Check for program updates"));
1656 const auto cleanup = [this, updater]()
1658 if (m_programUpdateTimer)
1659 m_programUpdateTimer->start();
1660 updater->deleteLater();
1663 const QString newVersion = updater->getNewVersion();
1664 if (!newVersion.isEmpty())
1666 const QString msg {tr("A new version is available.") + u"<br/>"
1667 + tr("Do you want to download %1?").arg(newVersion) + u"<br/><br/>"
1668 + u"<a href=\"https://www.qbittorrent.org/news\">%1</a>"_s.arg(tr("Open changelog..."))};
1669 auto *msgBox = new QMessageBox {QMessageBox::Question, tr("qBittorrent Update Available"), msg
1670 , (QMessageBox::Yes | QMessageBox::No), this};
1671 msgBox->setAttribute(Qt::WA_DeleteOnClose);
1672 msgBox->setAttribute(Qt::WA_ShowWithoutActivating);
1673 msgBox->setDefaultButton(QMessageBox::Yes);
1674 msgBox->setWindowModality(Qt::NonModal);
1675 connect(msgBox, &QMessageBox::buttonClicked, this, [msgBox, updater](QAbstractButton *button)
1677 if (msgBox->buttonRole(button) == QMessageBox::YesRole)
1679 updater->updateProgram();
1682 connect(msgBox, &QDialog::finished, this, cleanup);
1683 msgBox->show();
1685 else
1687 if (invokedByUser)
1689 auto *msgBox = new QMessageBox {QMessageBox::Information, u"qBittorrent"_s
1690 , tr("No updates available.\nYou are already using the latest version.")
1691 , QMessageBox::Ok, this};
1692 msgBox->setAttribute(Qt::WA_DeleteOnClose);
1693 msgBox->setWindowModality(Qt::NonModal);
1694 connect(msgBox, &QDialog::finished, this, cleanup);
1695 msgBox->show();
1697 else
1699 cleanup();
1703 #endif
1705 void MainWindow::toggleAlternativeSpeeds()
1707 BitTorrent::Session *const session = BitTorrent::Session::instance();
1708 session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled());
1711 void MainWindow::on_actionDonateMoney_triggered()
1713 QDesktopServices::openUrl(QUrl(u"https://www.qbittorrent.org/donate"_s));
1716 void MainWindow::showConnectionSettings()
1718 on_actionOptions_triggered();
1719 m_options->showConnectionTab();
1722 void MainWindow::minimizeWindow()
1724 setWindowState(windowState() | Qt::WindowMinimized);
1727 void MainWindow::on_actionExecutionLogs_triggered(bool checked)
1729 if (checked)
1731 Q_ASSERT(!m_executionLog);
1732 m_executionLog = new ExecutionLogWidget(executionLogMsgTypes(), m_tabs);
1733 #ifdef Q_OS_MACOS
1734 m_tabs->addTab(m_executionLog, tr("Execution Log"));
1735 #else
1736 const int indexTab = m_tabs->addTab(m_executionLog, tr("Execution Log"));
1737 m_tabs->setTabIcon(indexTab, UIThemeManager::instance()->getIcon(u"help-contents"_s));
1738 #endif
1740 else
1742 delete m_executionLog;
1745 m_ui->actionNormalMessages->setEnabled(checked);
1746 m_ui->actionInformationMessages->setEnabled(checked);
1747 m_ui->actionWarningMessages->setEnabled(checked);
1748 m_ui->actionCriticalMessages->setEnabled(checked);
1749 setExecutionLogEnabled(checked);
1752 void MainWindow::on_actionNormalMessages_triggered(const bool checked)
1754 if (!m_executionLog)
1755 return;
1757 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::NORMAL, checked);
1758 setExecutionLogMsgTypes(flags);
1761 void MainWindow::on_actionInformationMessages_triggered(const bool checked)
1763 if (!m_executionLog)
1764 return;
1766 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::INFO, checked);
1767 setExecutionLogMsgTypes(flags);
1770 void MainWindow::on_actionWarningMessages_triggered(const bool checked)
1772 if (!m_executionLog)
1773 return;
1775 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::WARNING, checked);
1776 setExecutionLogMsgTypes(flags);
1779 void MainWindow::on_actionCriticalMessages_triggered(const bool checked)
1781 if (!m_executionLog)
1782 return;
1784 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::CRITICAL, checked);
1785 setExecutionLogMsgTypes(flags);
1788 void MainWindow::on_actionAutoExit_toggled(bool enabled)
1790 qDebug() << Q_FUNC_INFO << enabled;
1791 Preferences::instance()->setShutdownqBTWhenDownloadsComplete(enabled);
1794 void MainWindow::on_actionAutoSuspend_toggled(bool enabled)
1796 qDebug() << Q_FUNC_INFO << enabled;
1797 Preferences::instance()->setSuspendWhenDownloadsComplete(enabled);
1800 void MainWindow::on_actionAutoHibernate_toggled(bool enabled)
1802 qDebug() << Q_FUNC_INFO << enabled;
1803 Preferences::instance()->setHibernateWhenDownloadsComplete(enabled);
1806 void MainWindow::on_actionAutoShutdown_toggled(bool enabled)
1808 qDebug() << Q_FUNC_INFO << enabled;
1809 Preferences::instance()->setShutdownWhenDownloadsComplete(enabled);
1812 void MainWindow::updatePowerManagementState() const
1814 const auto *pref = Preferences::instance();
1815 const bool preventFromSuspendWhenDownloading = pref->preventFromSuspendWhenDownloading();
1816 const bool preventFromSuspendWhenSeeding = pref->preventFromSuspendWhenSeeding();
1818 const QList<BitTorrent::Torrent *> allTorrents = BitTorrent::Session::instance()->torrents();
1819 const bool inhibitSuspend = std::any_of(allTorrents.cbegin(), allTorrents.cend(), [&](const BitTorrent::Torrent *torrent)
1821 if (preventFromSuspendWhenDownloading && (!torrent->isFinished() && !torrent->isStopped() && !torrent->isErrored() && torrent->hasMetadata()))
1822 return true;
1824 if (preventFromSuspendWhenSeeding && (torrent->isFinished() && !torrent->isStopped()))
1825 return true;
1827 return torrent->isMoving();
1829 m_pwr->setActivityState(inhibitSuspend);
1831 m_preventTimer->start(PREVENT_SUSPEND_INTERVAL);
1834 void MainWindow::applyTransferListFilter()
1836 m_transferListWidget->applyFilter(m_columnFilterEdit->text(), m_columnFilterComboBox->currentData().value<TransferListModel::Column>());
1839 void MainWindow::refreshWindowTitle()
1841 const auto *btSession = BitTorrent::Session::instance();
1842 if (btSession->isPaused())
1844 const QString title = tr("[PAUSED] %1", "%1 is the rest of the window title").arg(m_windowTitle);
1845 setWindowTitle(title);
1847 else
1849 if (m_displaySpeedInTitle)
1851 const QString title = tr("[D: %1, U: %2] %3", "D = Download; U = Upload; %3 is the rest of the window title")
1852 .arg(m_downloadRate, m_uploadRate, m_windowTitle);
1853 setWindowTitle(title);
1855 else
1857 setWindowTitle(m_windowTitle);
1862 void MainWindow::refreshTrayIconTooltip()
1864 const auto *btSession = BitTorrent::Session::instance();
1865 if (!btSession->isPaused())
1867 const auto toolTip = u"%1\n%2"_s.arg(
1868 tr("DL speed: %1", "e.g: Download speed: 10 KiB/s").arg(m_downloadRate)
1869 , tr("UP speed: %1", "e.g: Upload speed: 10 KiB/s").arg(m_uploadRate));
1870 app()->desktopIntegration()->setToolTip(toolTip);
1872 else
1874 app()->desktopIntegration()->setToolTip(tr("Paused"));
1878 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1879 void MainWindow::checkProgramUpdate(const bool invokedByUser)
1881 if (m_programUpdateTimer)
1882 m_programUpdateTimer->stop();
1884 m_ui->actionCheckForUpdates->setEnabled(false);
1885 m_ui->actionCheckForUpdates->setText(tr("Checking for Updates..."));
1886 m_ui->actionCheckForUpdates->setToolTip(tr("Already checking for program updates in the background"));
1888 auto *updater = new ProgramUpdater(this);
1889 connect(updater, &ProgramUpdater::updateCheckFinished
1890 , this, [this, invokedByUser, updater]()
1892 handleUpdateCheckFinished(updater, invokedByUser);
1894 updater->checkForUpdates();
1896 #endif
1898 #ifdef Q_OS_WIN
1899 void MainWindow::installPython()
1901 m_ui->actionSearchWidget->setEnabled(false);
1902 m_ui->actionSearchWidget->setToolTip(tr("Python installation in progress..."));
1903 setCursor(Qt::WaitCursor);
1904 // Download python
1905 Net::DownloadManager::instance()->download(
1906 Net::DownloadRequest(PYTHON_INSTALLER_URL).saveToFile(true)
1907 , Preferences::instance()->useProxyForGeneralPurposes()
1908 , this, &MainWindow::pythonDownloadFinished);
1911 bool MainWindow::verifyPythonInstaller(const Path &installerPath) const
1913 // Verify installer hash
1914 // Python.org only provides MD5 hash but MD5 is already broken and doesn't guarantee file is not tampered.
1915 // Therefore, MD5 is only included to prove that the hash is still the same with upstream and we rely on
1916 // SHA3-512 for the main check.
1918 QFile file {installerPath.data()};
1919 if (!file.open(QIODevice::ReadOnly))
1921 LogMsg((tr("Failed to open Python installer. File: \"%1\".").arg(installerPath.toString())), Log::WARNING);
1922 return false;
1925 QCryptographicHash md5Hash {QCryptographicHash::Md5};
1926 md5Hash.addData(&file);
1927 if (const QByteArray hashHex = md5Hash.result().toHex(); hashHex != PYTHON_INSTALLER_MD5)
1929 LogMsg((tr("Failed MD5 hash check for Python installer. File: \"%1\". Result hash: \"%2\". Expected hash: \"%3\".")
1930 .arg(installerPath.toString(), QString::fromLatin1(hashHex), QString::fromLatin1(PYTHON_INSTALLER_MD5)))
1931 , Log::WARNING);
1932 return false;
1935 file.seek(0);
1937 QCryptographicHash sha3Hash {QCryptographicHash::Sha3_512};
1938 sha3Hash.addData(&file);
1939 if (const QByteArray hashHex = sha3Hash.result().toHex(); hashHex != PYTHON_INSTALLER_SHA3_512)
1941 LogMsg((tr("Failed SHA3-512 hash check for Python installer. File: \"%1\". Result hash: \"%2\". Expected hash: \"%3\".")
1942 .arg(installerPath.toString(), QString::fromLatin1(hashHex), QString::fromLatin1(PYTHON_INSTALLER_SHA3_512)))
1943 , Log::WARNING);
1944 return false;
1947 return true;
1950 void MainWindow::pythonDownloadFinished(const Net::DownloadResult &result)
1952 auto restoreWidgetsGuard = qScopeGuard([this]
1954 m_ui->actionSearchWidget->setEnabled(true);
1955 m_ui->actionSearchWidget->setToolTip({});
1956 setCursor(Qt::ArrowCursor);
1959 if (result.status != Net::DownloadStatus::Success)
1961 QMessageBox::warning(
1962 this, tr("Download error")
1963 , tr("Python installer could not be downloaded. Error: %1.\nPlease install it manually.")
1964 .arg(result.errorString));
1965 return;
1968 const Path exePath = result.filePath + u".exe";
1969 if (!Utils::Fs::renameFile(result.filePath, exePath))
1971 LogMsg(tr("Rename Python installer failed. Source: \"%1\". Destination: \"%2\".")
1972 .arg(result.filePath.toString(), exePath.toString())
1973 , Log::WARNING);
1974 return;
1977 if (!verifyPythonInstaller(exePath))
1978 return;
1980 // launch installer
1981 auto *installer = new QProcess(this);
1982 installer->connect(installer, &QProcess::finished, this, [this, exePath, installer, restoreWidgetsGuard = std::move(restoreWidgetsGuard)](const int exitCode, const QProcess::ExitStatus exitStatus)
1984 installer->deleteLater();
1986 if ((exitStatus == QProcess::NormalExit) && (exitCode == 0))
1988 LogMsg(tr("Python installation success."), Log::INFO);
1990 // Delete installer
1991 Utils::Fs::removeFile(exePath);
1993 // Reload search engine
1994 if (Utils::ForeignApps::pythonInfo().isSupportedVersion())
1996 m_ui->actionSearchWidget->setChecked(true);
1997 displaySearchTab(true);
2000 else
2002 const QString errorInfo = (exitStatus == QProcess::NormalExit)
2003 ? tr("Exit code: %1.").arg(QString::number(exitCode))
2004 : tr("Reason: installer crashed.");
2005 LogMsg(u"%1 %2"_s.arg(tr("Python installation failed."), errorInfo), Log::WARNING);
2008 LogMsg(tr("Launching Python installer. File: \"%1\".").arg(exePath.toString()), Log::INFO);
2009 installer->start(exePath.toString(), {u"/passive"_s});
2011 #endif // Q_OS_WIN