Correctly handle "torrent finished" events
[qBittorrent.git] / src / gui / mainwindow.cpp
blob4798a680058da9b1a892a5563ccf879b0ad495b1
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 #include "base/bittorrent/session.h"
60 #include "base/bittorrent/sessionstatus.h"
61 #include "base/global.h"
62 #include "base/net/downloadmanager.h"
63 #include "base/path.h"
64 #include "base/preferences.h"
65 #include "base/rss/rss_folder.h"
66 #include "base/rss/rss_session.h"
67 #include "base/utils/foreignapps.h"
68 #include "base/utils/fs.h"
69 #include "base/utils/misc.h"
70 #include "base/utils/password.h"
71 #include "base/version.h"
72 #include "aboutdialog.h"
73 #include "autoexpandabledialog.h"
74 #include "cookiesdialog.h"
75 #include "desktopintegration.h"
76 #include "downloadfromurldialog.h"
77 #include "executionlogwidget.h"
78 #include "hidabletabwidget.h"
79 #include "interfaces/iguiapplication.h"
80 #include "lineedit.h"
81 #include "optionsdialog.h"
82 #include "powermanagement/powermanagement.h"
83 #include "properties/peerlistwidget.h"
84 #include "properties/propertieswidget.h"
85 #include "properties/proptabbar.h"
86 #include "rss/rsswidget.h"
87 #include "search/searchwidget.h"
88 #include "speedlimitdialog.h"
89 #include "statsdialog.h"
90 #include "statusbar.h"
91 #include "torrentcreatordialog.h"
92 #include "trackerlist/trackerlistwidget.h"
93 #include "transferlistfilterswidget.h"
94 #include "transferlistmodel.h"
95 #include "transferlistwidget.h"
96 #include "ui_mainwindow.h"
97 #include "uithememanager.h"
98 #include "utils.h"
100 #ifdef Q_OS_MACOS
101 #include "macosdockbadge/badger.h"
102 #endif
103 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
104 #include "programupdater.h"
105 #endif
107 using namespace std::chrono_literals;
109 namespace
111 #define SETTINGS_KEY(name) u"GUI/" name
112 #define EXECUTIONLOG_SETTINGS_KEY(name) (SETTINGS_KEY(u"Log/"_s) name)
114 const std::chrono::seconds PREVENT_SUSPEND_INTERVAL {60};
117 MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, const QString &titleSuffix)
118 : GUIApplicationComponent(app)
119 , m_ui {new Ui::MainWindow}
120 , m_downloadRate {Utils::Misc::friendlyUnit(0, true)}
121 , m_uploadRate {Utils::Misc::friendlyUnit(0, true)}
122 , m_storeExecutionLogEnabled {EXECUTIONLOG_SETTINGS_KEY(u"Enabled"_s)}
123 , m_storeDownloadTrackerFavicon {SETTINGS_KEY(u"DownloadTrackerFavicon"_s)}
124 , m_storeExecutionLogTypes {EXECUTIONLOG_SETTINGS_KEY(u"Types"_s), Log::MsgType::ALL}
125 #ifdef Q_OS_MACOS
126 , m_badger {std::make_unique<MacUtils::Badger>()}
127 #endif // Q_OS_MACOS
129 m_ui->setupUi(this);
131 Preferences *const pref = Preferences::instance();
132 m_uiLocked = pref->isUILocked();
133 m_displaySpeedInTitle = pref->speedInTitleBar();
134 // Setting icons
135 #ifndef Q_OS_MACOS
136 setWindowIcon(UIThemeManager::instance()->getIcon(u"qbittorrent"_s));
137 #endif // Q_OS_MACOS
139 setTitleSuffix(titleSuffix);
141 #if (defined(Q_OS_UNIX))
142 m_ui->actionOptions->setText(tr("Preferences"));
143 #endif
145 addToolbarContextMenu();
147 m_ui->actionOpen->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_s));
148 m_ui->actionDownloadFromURL->setIcon(UIThemeManager::instance()->getIcon(u"insert-link"_s));
149 m_ui->actionSetGlobalSpeedLimits->setIcon(UIThemeManager::instance()->getIcon(u"speedometer"_s));
150 m_ui->actionCreateTorrent->setIcon(UIThemeManager::instance()->getIcon(u"torrent-creator"_s, u"document-edit"_s));
151 m_ui->actionAbout->setIcon(UIThemeManager::instance()->getIcon(u"help-about"_s));
152 m_ui->actionStatistics->setIcon(UIThemeManager::instance()->getIcon(u"view-statistics"_s));
153 m_ui->actionTopQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-top"_s));
154 m_ui->actionIncreaseQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-up"_s));
155 m_ui->actionDecreaseQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-down"_s));
156 m_ui->actionBottomQueuePos->setIcon(UIThemeManager::instance()->getIcon(u"go-bottom"_s));
157 m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"list-remove"_s));
158 m_ui->actionDocumentation->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
159 m_ui->actionDonateMoney->setIcon(UIThemeManager::instance()->getIcon(u"wallet-open"_s));
160 m_ui->actionExit->setIcon(UIThemeManager::instance()->getIcon(u"application-exit"_s));
161 m_ui->actionLock->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
162 m_ui->actionOptions->setIcon(UIThemeManager::instance()->getIcon(u"configure"_s, u"preferences-system"_s));
163 m_ui->actionStart->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
164 m_ui->actionStop->setIcon(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s));
165 m_ui->actionPauseSession->setIcon(UIThemeManager::instance()->getIcon(u"pause-session"_s, u"media-playback-pause"_s));
166 m_ui->actionResumeSession->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
167 m_ui->menuAutoShutdownOnDownloadsCompletion->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_s, u"application-exit"_s));
168 m_ui->actionManageCookies->setIcon(UIThemeManager::instance()->getIcon(u"browser-cookies"_s, u"preferences-web-browser-cookies"_s));
169 m_ui->menuLog->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
170 m_ui->actionCheckForUpdates->setIcon(UIThemeManager::instance()->getIcon(u"view-refresh"_s));
172 m_ui->actionPauseSession->setVisible(!BitTorrent::Session::instance()->isPaused());
173 m_ui->actionResumeSession->setVisible(BitTorrent::Session::instance()->isPaused());
174 connect(BitTorrent::Session::instance(), &BitTorrent::Session::paused, this, [this]
176 m_ui->actionPauseSession->setVisible(false);
177 m_ui->actionResumeSession->setVisible(true);
178 refreshWindowTitle();
179 refreshTrayIconTooltip();
181 connect(BitTorrent::Session::instance(), &BitTorrent::Session::resumed, this, [this]
183 m_ui->actionPauseSession->setVisible(true);
184 m_ui->actionResumeSession->setVisible(false);
185 refreshWindowTitle();
186 refreshTrayIconTooltip();
189 auto *lockMenu = new QMenu(m_ui->menuView);
190 lockMenu->addAction(tr("&Set Password"), this, &MainWindow::defineUILockPassword);
191 lockMenu->addAction(tr("&Clear Password"), this, &MainWindow::clearUILockPassword);
192 m_ui->actionLock->setMenu(lockMenu);
194 updateAltSpeedsBtn(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled());
196 connect(BitTorrent::Session::instance(), &BitTorrent::Session::speedLimitModeChanged, this, &MainWindow::updateAltSpeedsBtn);
198 qDebug("create tabWidget");
199 m_tabs = new HidableTabWidget(this);
200 connect(m_tabs.data(), &QTabWidget::currentChanged, this, &MainWindow::tabChanged);
202 m_splitter = new QSplitter(Qt::Horizontal, this);
203 // vSplitter->setChildrenCollapsible(false);
205 auto *hSplitter = new QSplitter(Qt::Vertical, this);
206 hSplitter->setChildrenCollapsible(false);
207 hSplitter->setFrameShape(QFrame::NoFrame);
209 // Torrent filter
210 m_columnFilterEdit = new LineEdit;
211 m_columnFilterEdit->setPlaceholderText(tr("Filter torrents..."));
212 m_columnFilterEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
213 m_columnFilterEdit->setFixedWidth(200);
214 m_columnFilterEdit->setContextMenuPolicy(Qt::CustomContextMenu);
215 connect(m_columnFilterEdit, &QWidget::customContextMenuRequested, this, &MainWindow::showFilterContextMenu);
216 auto *columnFilterLabel = new QLabel(tr("Filter by:"));
217 m_columnFilterComboBox = new QComboBox;
218 QHBoxLayout *columnFilterLayout = new QHBoxLayout(m_columnFilterWidget);
219 columnFilterLayout->setContentsMargins(0, 0, 0, 0);
220 auto *columnFilterSpacer = new QWidget(this);
221 columnFilterSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
222 columnFilterLayout->addWidget(columnFilterSpacer);
223 columnFilterLayout->addWidget(m_columnFilterEdit);
224 columnFilterLayout->addWidget(columnFilterLabel, 0);
225 columnFilterLayout->addWidget(m_columnFilterComboBox, 0);
226 m_columnFilterWidget = new QWidget(this);
227 m_columnFilterWidget->setLayout(columnFilterLayout);
228 m_columnFilterAction = m_ui->toolBar->insertWidget(m_ui->actionLock, m_columnFilterWidget);
230 auto *spacer = new QWidget(this);
231 spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
232 m_ui->toolBar->insertWidget(m_columnFilterAction, spacer);
234 // Transfer List tab
235 m_transferListWidget = new TransferListWidget(app, this);
236 m_propertiesWidget = new PropertiesWidget(hSplitter);
237 connect(m_transferListWidget, &TransferListWidget::currentTorrentChanged, m_propertiesWidget, &PropertiesWidget::loadTorrentInfos);
238 hSplitter->addWidget(m_transferListWidget);
239 hSplitter->addWidget(m_propertiesWidget);
240 m_splitter->addWidget(hSplitter);
241 m_splitter->setCollapsible(0, false);
242 m_tabs->addTab(m_splitter,
243 #ifndef Q_OS_MACOS
244 UIThemeManager::instance()->getIcon(u"folder-remote"_s),
245 #endif
246 tr("Transfers"));
247 // Filter types
248 const QList<TransferListModel::Column> filterTypes = {TransferListModel::Column::TR_NAME, TransferListModel::Column::TR_SAVE_PATH};
249 for (const TransferListModel::Column type : filterTypes)
251 const QString typeName = m_transferListWidget->getSourceModel()->headerData(type, Qt::Horizontal, Qt::DisplayRole).value<QString>();
252 m_columnFilterComboBox->addItem(typeName, type);
254 connect(m_columnFilterComboBox, &QComboBox::currentIndexChanged, this, &MainWindow::applyTransferListFilter);
255 connect(m_columnFilterEdit, &LineEdit::textChanged, this, &MainWindow::applyTransferListFilter);
256 connect(hSplitter, &QSplitter::splitterMoved, this, &MainWindow::saveSettings);
257 connect(m_splitter, &QSplitter::splitterMoved, this, &MainWindow::saveSplitterSettings);
259 #ifdef Q_OS_MACOS
260 // Increase top spacing to avoid tab overlapping
261 m_ui->centralWidgetLayout->addSpacing(8);
262 #endif
264 m_ui->centralWidgetLayout->addWidget(m_tabs);
266 m_queueSeparator = m_ui->toolBar->insertSeparator(m_ui->actionTopQueuePos);
267 m_queueSeparatorMenu = m_ui->menuEdit->insertSeparator(m_ui->actionTopQueuePos);
269 #ifdef Q_OS_MACOS
270 for (QAction *action : asConst(m_ui->toolBar->actions()))
272 if (action->isSeparator())
274 QWidget *spacer = new QWidget(this);
275 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
276 spacer->setMinimumWidth(16);
277 m_ui->toolBar->insertWidget(action, spacer);
278 m_ui->toolBar->removeAction(action);
282 QWidget *spacer = new QWidget(this);
283 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
284 spacer->setMinimumWidth(8);
285 m_ui->toolBar->insertWidget(m_ui->actionDownloadFromURL, spacer);
288 QWidget *spacer = new QWidget(this);
289 spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
290 spacer->setMinimumWidth(8);
291 m_ui->toolBar->addWidget(spacer);
293 #endif // Q_OS_MACOS
295 // Transfer list slots
296 connect(m_ui->actionStart, &QAction::triggered, m_transferListWidget, &TransferListWidget::startSelectedTorrents);
297 connect(m_ui->actionStop, &QAction::triggered, m_transferListWidget, &TransferListWidget::stopSelectedTorrents);
298 connect(m_ui->actionPauseSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::pauseSession);
299 connect(m_ui->actionResumeSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::resumeSession);
300 connect(m_ui->actionDelete, &QAction::triggered, m_transferListWidget, &TransferListWidget::softDeleteSelectedTorrents);
301 connect(m_ui->actionTopQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::topQueuePosSelectedTorrents);
302 connect(m_ui->actionIncreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::increaseQueuePosSelectedTorrents);
303 connect(m_ui->actionDecreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::decreaseQueuePosSelectedTorrents);
304 connect(m_ui->actionBottomQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::bottomQueuePosSelectedTorrents);
305 connect(m_ui->actionMinimize, &QAction::triggered, this, &MainWindow::minimizeWindow);
306 connect(m_ui->actionUseAlternativeSpeedLimits, &QAction::triggered, this, &MainWindow::toggleAlternativeSpeeds);
308 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
309 connect(m_ui->actionCheckForUpdates, &QAction::triggered, this, [this]() { checkProgramUpdate(true); });
311 // trigger an early check on startup
312 if (pref->isUpdateCheckEnabled())
313 checkProgramUpdate(false);
314 #else
315 m_ui->actionCheckForUpdates->setVisible(false);
316 #endif
318 // Certain menu items should reside at specific places on macOS.
319 // Qt partially does it on its own, but updates and different languages require tuning.
320 m_ui->actionExit->setMenuRole(QAction::QuitRole);
321 m_ui->actionAbout->setMenuRole(QAction::AboutRole);
322 m_ui->actionCheckForUpdates->setMenuRole(QAction::ApplicationSpecificRole);
323 m_ui->actionOptions->setMenuRole(QAction::PreferencesRole);
325 connect(m_ui->actionManageCookies, &QAction::triggered, this, &MainWindow::manageCookies);
327 // Initialise system sleep inhibition timer
328 m_pwr = new PowerManagement(this);
329 m_preventTimer = new QTimer(this);
330 m_preventTimer->setSingleShot(true);
331 connect(m_preventTimer, &QTimer::timeout, this, &MainWindow::updatePowerManagementState);
332 connect(pref, &Preferences::changed, this, &MainWindow::updatePowerManagementState);
333 updatePowerManagementState();
335 // Configure BT session according to options
336 loadPreferences();
338 connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &MainWindow::loadSessionStats);
339 connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated, this, &MainWindow::reloadTorrentStats);
341 createKeyboardShortcuts();
343 #ifdef Q_OS_MACOS
344 setUnifiedTitleAndToolBarOnMac(true);
345 #endif
347 // View settings
348 m_ui->actionTopToolBar->setChecked(pref->isToolbarDisplayed());
349 m_ui->actionShowStatusbar->setChecked(pref->isStatusbarDisplayed());
350 m_ui->actionSpeedInTitleBar->setChecked(pref->speedInTitleBar());
351 m_ui->actionRSSReader->setChecked(pref->isRSSWidgetEnabled());
352 m_ui->actionSearchWidget->setChecked(pref->isSearchEnabled());
353 m_ui->actionExecutionLogs->setChecked(isExecutionLogEnabled());
355 const Log::MsgTypes flags = executionLogMsgTypes();
356 m_ui->actionNormalMessages->setChecked(flags.testFlag(Log::NORMAL));
357 m_ui->actionInformationMessages->setChecked(flags.testFlag(Log::INFO));
358 m_ui->actionWarningMessages->setChecked(flags.testFlag(Log::WARNING));
359 m_ui->actionCriticalMessages->setChecked(flags.testFlag(Log::CRITICAL));
361 displayRSSTab(m_ui->actionRSSReader->isChecked());
362 on_actionExecutionLogs_triggered(m_ui->actionExecutionLogs->isChecked());
363 on_actionNormalMessages_triggered(m_ui->actionNormalMessages->isChecked());
364 on_actionInformationMessages_triggered(m_ui->actionInformationMessages->isChecked());
365 on_actionWarningMessages_triggered(m_ui->actionWarningMessages->isChecked());
366 on_actionCriticalMessages_triggered(m_ui->actionCriticalMessages->isChecked());
367 if (m_ui->actionSearchWidget->isChecked())
368 QMetaObject::invokeMethod(this, &MainWindow::on_actionSearchWidget_triggered, Qt::QueuedConnection);
369 // Auto shutdown actions
370 auto *autoShutdownGroup = new QActionGroup(this);
371 autoShutdownGroup->setExclusive(true);
372 autoShutdownGroup->addAction(m_ui->actionAutoShutdownDisabled);
373 autoShutdownGroup->addAction(m_ui->actionAutoExit);
374 autoShutdownGroup->addAction(m_ui->actionAutoShutdown);
375 autoShutdownGroup->addAction(m_ui->actionAutoSuspend);
376 autoShutdownGroup->addAction(m_ui->actionAutoHibernate);
377 #if (!defined(Q_OS_UNIX) || defined(Q_OS_MACOS)) || defined(QBT_USES_DBUS)
378 m_ui->actionAutoShutdown->setChecked(pref->shutdownWhenDownloadsComplete());
379 m_ui->actionAutoSuspend->setChecked(pref->suspendWhenDownloadsComplete());
380 m_ui->actionAutoHibernate->setChecked(pref->hibernateWhenDownloadsComplete());
381 #else
382 m_ui->actionAutoShutdown->setDisabled(true);
383 m_ui->actionAutoSuspend->setDisabled(true);
384 m_ui->actionAutoHibernate->setDisabled(true);
385 #endif
386 m_ui->actionAutoExit->setChecked(pref->shutdownqBTWhenDownloadsComplete());
388 if (!autoShutdownGroup->checkedAction())
389 m_ui->actionAutoShutdownDisabled->setChecked(true);
391 // Load Window state and sizes
392 loadSettings();
394 populateDesktopIntegrationMenu();
395 #ifndef Q_OS_MACOS
396 m_ui->actionLock->setVisible(app->desktopIntegration()->isActive());
397 connect(app->desktopIntegration(), &DesktopIntegration::stateChanged, this, [this, app]()
399 m_ui->actionLock->setVisible(app->desktopIntegration()->isActive());
401 #endif
402 connect(app->desktopIntegration(), &DesktopIntegration::notificationClicked, this, &MainWindow::desktopNotificationClicked);
403 connect(app->desktopIntegration(), &DesktopIntegration::activationRequested, this, [this]()
405 #ifdef Q_OS_MACOS
406 if (!isVisible())
407 activate();
408 #else
409 toggleVisibility();
410 #endif
413 #ifdef Q_OS_MACOS
414 if (initialState == WindowState::Normal)
416 show();
417 activateWindow();
418 raise();
420 else
422 // Make sure the Window is visible if we don't have a tray icon
423 showMinimized();
425 #else
426 if (app->desktopIntegration()->isActive())
428 if ((initialState == WindowState::Normal) && !m_uiLocked)
430 show();
431 activateWindow();
432 raise();
434 else if (initialState == WindowState::Minimized)
436 showMinimized();
437 if (pref->minimizeToTray())
439 hide();
440 if (!pref->minimizeToTrayNotified())
442 app->desktopIntegration()->showNotification(tr("qBittorrent is minimized to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
443 pref->setMinimizeToTrayNotified(true);
448 else
450 // Make sure the Window is visible if we don't have a tray icon
451 if (initialState != WindowState::Normal)
453 showMinimized();
455 else
457 show();
458 activateWindow();
459 raise();
462 #endif
464 const bool isFiltersSidebarVisible = pref->isFiltersSidebarVisible();
465 m_ui->actionShowFiltersSidebar->setChecked(isFiltersSidebarVisible);
466 if (isFiltersSidebarVisible)
468 showFiltersSidebar(true);
470 else
472 m_transferListWidget->applyStatusFilter(pref->getTransSelFilter());
473 m_transferListWidget->applyCategoryFilter(QString());
474 m_transferListWidget->applyTagFilter(std::nullopt);
475 m_transferListWidget->applyTrackerFilterAll();
478 // Start watching the executable for updates
479 m_executableWatcher = new QFileSystemWatcher(this);
480 connect(m_executableWatcher, &QFileSystemWatcher::fileChanged, this, &MainWindow::notifyOfUpdate);
481 m_executableWatcher->addPath(qApp->applicationFilePath());
483 m_transferListWidget->setFocus();
485 // Update the number of torrents (tab)
486 updateNbTorrents();
487 connect(m_transferListWidget->getSourceModel(), &QAbstractItemModel::rowsInserted, this, &MainWindow::updateNbTorrents);
488 connect(m_transferListWidget->getSourceModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateNbTorrents);
490 connect(pref, &Preferences::changed, this, &MainWindow::optionsSaved);
492 qDebug("GUI Built");
495 MainWindow::~MainWindow()
497 delete m_ui;
500 bool MainWindow::isExecutionLogEnabled() const
502 return m_storeExecutionLogEnabled;
505 void MainWindow::setExecutionLogEnabled(const bool value)
507 m_storeExecutionLogEnabled = value;
510 Log::MsgTypes MainWindow::executionLogMsgTypes() const
512 return m_storeExecutionLogTypes;
515 void MainWindow::setExecutionLogMsgTypes(const Log::MsgTypes value)
517 m_executionLog->setMessageTypes(value);
518 m_storeExecutionLogTypes = value;
521 bool MainWindow::isDownloadTrackerFavicon() const
523 return m_storeDownloadTrackerFavicon;
526 void MainWindow::setDownloadTrackerFavicon(const bool value)
528 if (m_transferListFiltersWidget)
529 m_transferListFiltersWidget->setDownloadTrackerFavicon(value);
530 m_storeDownloadTrackerFavicon = value;
533 void MainWindow::setTitleSuffix(const QString &suffix)
535 const auto emDash = QChar(0x2014);
536 const QString separator = u' ' + emDash + u' ';
537 m_windowTitle = QStringLiteral("qBittorrent " QBT_VERSION)
538 + (!suffix.isEmpty() ? (separator + suffix) : QString());
540 refreshWindowTitle();
543 void MainWindow::addToolbarContextMenu()
545 const Preferences *const pref = Preferences::instance();
546 m_toolbarMenu = new QMenu(this);
548 m_ui->toolBar->setContextMenuPolicy(Qt::CustomContextMenu);
549 connect(m_ui->toolBar, &QWidget::customContextMenuRequested, this, &MainWindow::toolbarMenuRequested);
551 QAction *iconsOnly = m_toolbarMenu->addAction(tr("Icons Only"), this, &MainWindow::toolbarIconsOnly);
552 QAction *textOnly = m_toolbarMenu->addAction(tr("Text Only"), this, &MainWindow::toolbarTextOnly);
553 QAction *textBesideIcons = m_toolbarMenu->addAction(tr("Text Alongside Icons"), this, &MainWindow::toolbarTextBeside);
554 QAction *textUnderIcons = m_toolbarMenu->addAction(tr("Text Under Icons"), this, &MainWindow::toolbarTextUnder);
555 QAction *followSystemStyle = m_toolbarMenu->addAction(tr("Follow System Style"), this, &MainWindow::toolbarFollowSystem);
557 auto *textPositionGroup = new QActionGroup(m_toolbarMenu);
558 textPositionGroup->addAction(iconsOnly);
559 iconsOnly->setCheckable(true);
560 textPositionGroup->addAction(textOnly);
561 textOnly->setCheckable(true);
562 textPositionGroup->addAction(textBesideIcons);
563 textBesideIcons->setCheckable(true);
564 textPositionGroup->addAction(textUnderIcons);
565 textUnderIcons->setCheckable(true);
566 textPositionGroup->addAction(followSystemStyle);
567 followSystemStyle->setCheckable(true);
569 const auto buttonStyle = static_cast<Qt::ToolButtonStyle>(pref->getToolbarTextPosition());
570 if ((buttonStyle >= Qt::ToolButtonIconOnly) && (buttonStyle <= Qt::ToolButtonFollowStyle))
571 m_ui->toolBar->setToolButtonStyle(buttonStyle);
572 switch (buttonStyle)
574 case Qt::ToolButtonIconOnly:
575 iconsOnly->setChecked(true);
576 break;
577 case Qt::ToolButtonTextOnly:
578 textOnly->setChecked(true);
579 break;
580 case Qt::ToolButtonTextBesideIcon:
581 textBesideIcons->setChecked(true);
582 break;
583 case Qt::ToolButtonTextUnderIcon:
584 textUnderIcons->setChecked(true);
585 break;
586 default:
587 followSystemStyle->setChecked(true);
591 void MainWindow::manageCookies()
593 auto *cookieDialog = new CookiesDialog(this);
594 cookieDialog->setAttribute(Qt::WA_DeleteOnClose);
595 cookieDialog->open();
598 void MainWindow::toolbarMenuRequested()
600 m_toolbarMenu->popup(QCursor::pos());
603 void MainWindow::toolbarIconsOnly()
605 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
606 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonIconOnly);
609 void MainWindow::toolbarTextOnly()
611 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
612 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextOnly);
615 void MainWindow::toolbarTextBeside()
617 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
618 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextBesideIcon);
621 void MainWindow::toolbarTextUnder()
623 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
624 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonTextUnderIcon);
627 void MainWindow::toolbarFollowSystem()
629 m_ui->toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
630 Preferences::instance()->setToolbarTextPosition(Qt::ToolButtonFollowStyle);
633 bool MainWindow::defineUILockPassword()
635 bool ok = false;
636 const QString newPassword = AutoExpandableDialog::getText(this, tr("UI lock password")
637 , tr("Please type the UI lock password:"), QLineEdit::Password, {}, &ok);
638 if (!ok)
639 return false;
641 if (newPassword.size() < 3)
643 QMessageBox::warning(this, tr("Invalid password"), tr("The password must be at least 3 characters long"));
644 return false;
647 Preferences::instance()->setUILockPassword(Utils::Password::PBKDF2::generate(newPassword));
648 return true;
651 void MainWindow::clearUILockPassword()
653 const QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Clear the password")
654 , tr("Are you sure you want to clear the password?"), (QMessageBox::Yes | QMessageBox::No), QMessageBox::No);
655 if (answer == QMessageBox::Yes)
656 Preferences::instance()->setUILockPassword({});
659 void MainWindow::on_actionLock_triggered()
661 Preferences *const pref = Preferences::instance();
663 // Check if there is a password
664 if (pref->getUILockPassword().isEmpty())
666 if (!defineUILockPassword())
667 return;
670 // Lock the interface
671 m_uiLocked = true;
672 pref->setUILocked(true);
673 app()->desktopIntegration()->menu()->setEnabled(false);
674 hide();
677 void MainWindow::handleRSSUnreadCountUpdated(int count)
679 m_tabs->setTabText(m_tabs->indexOf(m_rssWidget), tr("RSS (%1)").arg(count));
682 void MainWindow::displayRSSTab(bool enable)
684 if (enable)
686 // RSS tab
687 if (!m_rssWidget)
689 m_rssWidget = new RSSWidget(app(), m_tabs);
690 connect(m_rssWidget.data(), &RSSWidget::unreadCountUpdated, this, &MainWindow::handleRSSUnreadCountUpdated);
691 #ifdef Q_OS_MACOS
692 m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
693 #else
694 const int indexTab = m_tabs->addTab(m_rssWidget, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
695 m_tabs->setTabIcon(indexTab, UIThemeManager::instance()->getIcon(u"application-rss"_s));
696 #endif
699 else
701 delete m_rssWidget;
705 void MainWindow::showFilterContextMenu()
707 const Preferences *pref = Preferences::instance();
709 QMenu *menu = m_columnFilterEdit->createStandardContextMenu();
710 menu->setAttribute(Qt::WA_DeleteOnClose);
711 menu->addSeparator();
713 QAction *useRegexAct = menu->addAction(tr("Use regular expressions"));
714 useRegexAct->setCheckable(true);
715 useRegexAct->setChecked(pref->getRegexAsFilteringPatternForTransferList());
716 connect(useRegexAct, &QAction::toggled, pref, &Preferences::setRegexAsFilteringPatternForTransferList);
717 connect(useRegexAct, &QAction::toggled, this, &MainWindow::applyTransferListFilter);
719 menu->popup(QCursor::pos());
722 void MainWindow::displaySearchTab(bool enable)
724 Preferences::instance()->setSearchEnabled(enable);
725 if (enable)
727 // RSS tab
728 if (!m_searchWidget)
730 m_searchWidget = new SearchWidget(app(), this);
731 m_tabs->insertTab(1, m_searchWidget,
732 #ifndef Q_OS_MACOS
733 UIThemeManager::instance()->getIcon(u"edit-find"_s),
734 #endif
735 tr("Search"));
738 else
740 delete m_searchWidget;
744 void MainWindow::toggleFocusBetweenLineEdits()
746 if (m_columnFilterEdit->hasFocus() && (m_propertiesWidget->tabBar()->currentIndex() == PropTabBar::FilesTab))
748 m_propertiesWidget->contentFilterLine()->setFocus();
749 m_propertiesWidget->contentFilterLine()->selectAll();
751 else
753 m_columnFilterEdit->setFocus();
754 m_columnFilterEdit->selectAll();
758 void MainWindow::updateNbTorrents()
760 m_tabs->setTabText(0, tr("Transfers (%1)").arg(m_transferListWidget->getSourceModel()->rowCount()));
763 void MainWindow::on_actionDocumentation_triggered() const
765 QDesktopServices::openUrl(QUrl(u"https://doc.qbittorrent.org"_s));
768 void MainWindow::tabChanged([[maybe_unused]] const int newTab)
770 // We cannot rely on the index newTab
771 // because the tab order is undetermined now
772 if (m_tabs->currentWidget() == m_splitter)
774 qDebug("Changed tab to transfer list, refreshing the list");
775 m_propertiesWidget->loadDynamicData();
776 m_columnFilterAction->setVisible(true);
777 return;
779 m_columnFilterAction->setVisible(false);
781 if (m_tabs->currentWidget() == m_searchWidget)
783 qDebug("Changed tab to search engine, giving focus to search input");
784 m_searchWidget->giveFocusToSearchInput();
788 void MainWindow::saveSettings() const
790 auto *pref = Preferences::instance();
791 pref->setMainGeometry(saveGeometry());
792 m_propertiesWidget->saveSettings();
795 void MainWindow::saveSplitterSettings() const
797 if (!m_transferListFiltersWidget)
798 return;
800 auto *pref = Preferences::instance();
801 pref->setFiltersSidebarWidth(m_splitter->sizes()[0]);
804 void MainWindow::cleanup()
806 if (!m_neverShown)
808 saveSettings();
809 saveSplitterSettings();
812 // delete RSSWidget explicitly to avoid crash in
813 // handleRSSUnreadCountUpdated() at application shutdown
814 delete m_rssWidget;
816 delete m_executableWatcher;
818 m_preventTimer->stop();
820 #if (defined(Q_OS_WIN) || defined(Q_OS_MACOS))
821 if (m_programUpdateTimer)
822 m_programUpdateTimer->stop();
823 #endif
825 // remove all child widgets
826 while (auto *w = findChild<QWidget *>())
827 delete w;
830 void MainWindow::loadSettings()
832 const auto *pref = Preferences::instance();
834 if (const QByteArray mainGeo = pref->getMainGeometry();
835 !mainGeo.isEmpty() && restoreGeometry(mainGeo))
837 m_posInitialized = true;
841 void MainWindow::desktopNotificationClicked()
843 if (isHidden())
845 if (m_uiLocked)
847 // Ask for UI lock password
848 if (!unlockUI())
849 return;
851 show();
852 if (isMinimized())
853 showNormal();
856 raise();
857 activateWindow();
860 void MainWindow::createKeyboardShortcuts()
862 m_ui->actionCreateTorrent->setShortcut(QKeySequence::New);
863 m_ui->actionOpen->setShortcut(QKeySequence::Open);
864 m_ui->actionDelete->setShortcut(QKeySequence::Delete);
865 m_ui->actionDelete->setShortcutContext(Qt::WidgetShortcut); // nullify its effect: delete key event is handled by respective widgets, not here
866 m_ui->actionDownloadFromURL->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O);
867 m_ui->actionExit->setShortcut(Qt::CTRL | Qt::Key_Q);
868 #ifdef Q_OS_MACOS
869 m_ui->actionCloseWindow->setShortcut(QKeySequence::Close);
870 #else
871 m_ui->actionCloseWindow->setVisible(false);
872 #endif
874 const auto *switchTransferShortcut = new QShortcut((Qt::ALT | Qt::Key_1), this);
875 connect(switchTransferShortcut, &QShortcut::activated, this, &MainWindow::displayTransferTab);
876 const auto *switchSearchShortcut = new QShortcut((Qt::ALT | Qt::Key_2), this);
877 connect(switchSearchShortcut, &QShortcut::activated, this, qOverload<>(&MainWindow::displaySearchTab));
878 const auto *switchRSSShortcut = new QShortcut((Qt::ALT | Qt::Key_3), this);
879 connect(switchRSSShortcut, &QShortcut::activated, this, qOverload<>(&MainWindow::displayRSSTab));
880 const auto *switchExecutionLogShortcut = new QShortcut((Qt::ALT | Qt::Key_4), this);
881 connect(switchExecutionLogShortcut, &QShortcut::activated, this, &MainWindow::displayExecutionLogTab);
882 const auto *switchSearchFilterShortcut = new QShortcut(QKeySequence::Find, m_transferListWidget);
883 connect(switchSearchFilterShortcut, &QShortcut::activated, this, &MainWindow::toggleFocusBetweenLineEdits);
884 const auto *switchSearchFilterShortcutAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), m_transferListWidget);
885 connect(switchSearchFilterShortcutAlternative, &QShortcut::activated, this, &MainWindow::toggleFocusBetweenLineEdits);
887 m_ui->actionDocumentation->setShortcut(QKeySequence::HelpContents);
888 m_ui->actionOptions->setShortcut(Qt::ALT | Qt::Key_O);
889 m_ui->actionStatistics->setShortcut(Qt::CTRL | Qt::Key_I);
890 m_ui->actionStart->setShortcut(Qt::CTRL | Qt::Key_S);
891 m_ui->actionStop->setShortcut(Qt::CTRL | Qt::Key_P);
892 m_ui->actionPauseSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
893 m_ui->actionResumeSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
894 m_ui->actionBottomQueuePos->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Minus);
895 m_ui->actionDecreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Minus);
896 m_ui->actionIncreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Plus);
897 m_ui->actionTopQueuePos->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Plus);
898 #ifdef Q_OS_MACOS
899 m_ui->actionMinimize->setShortcut(Qt::CTRL | Qt::Key_M);
900 addAction(m_ui->actionMinimize);
901 #endif
904 // Keyboard shortcuts slots
905 void MainWindow::displayTransferTab() const
907 m_tabs->setCurrentWidget(m_splitter);
910 void MainWindow::displaySearchTab()
912 if (!m_searchWidget)
914 m_ui->actionSearchWidget->setChecked(true);
915 displaySearchTab(true);
918 m_tabs->setCurrentWidget(m_searchWidget);
921 void MainWindow::displayRSSTab()
923 if (!m_rssWidget)
925 m_ui->actionRSSReader->setChecked(true);
926 displayRSSTab(true);
929 m_tabs->setCurrentWidget(m_rssWidget);
932 void MainWindow::displayExecutionLogTab()
934 if (!m_executionLog)
936 m_ui->actionExecutionLogs->setChecked(true);
937 on_actionExecutionLogs_triggered(true);
940 m_tabs->setCurrentWidget(m_executionLog);
943 // End of keyboard shortcuts slots
945 void MainWindow::on_actionSetGlobalSpeedLimits_triggered()
947 auto *dialog = new SpeedLimitDialog {this};
948 dialog->setAttribute(Qt::WA_DeleteOnClose);
949 dialog->open();
952 // Necessary if we want to close the window
953 // in one time if "close to systray" is enabled
954 void MainWindow::on_actionExit_triggered()
956 // UI locking enforcement.
957 if (isHidden() && m_uiLocked)
958 // Ask for UI lock password
959 if (!unlockUI()) return;
961 m_forceExit = true;
962 close();
965 #ifdef Q_OS_MACOS
966 void MainWindow::on_actionCloseWindow_triggered()
968 // On macOS window close is basically equivalent to window hide.
969 // If you decide to implement this functionality for other OS,
970 // then you will also need ui lock checks like in actionExit.
971 close();
973 #endif
975 QWidget *MainWindow::currentTabWidget() const
977 if (isMinimized() || !isVisible())
978 return nullptr;
979 if (m_tabs->currentIndex() == 0)
980 return m_transferListWidget;
981 return m_tabs->currentWidget();
984 TransferListWidget *MainWindow::transferListWidget() const
986 return m_transferListWidget;
989 bool MainWindow::unlockUI()
991 if (m_unlockDlgShowing)
992 return false;
994 bool ok = false;
995 const QString password = AutoExpandableDialog::getText(this, tr("UI lock password")
996 , tr("Please type the UI lock password:"), QLineEdit::Password, {}, &ok);
997 if (!ok) return false;
999 Preferences *const pref = Preferences::instance();
1001 const QByteArray secret = pref->getUILockPassword();
1002 if (!Utils::Password::PBKDF2::verify(secret, password))
1004 QMessageBox::warning(this, tr("Invalid password"), tr("The password is invalid"));
1005 return false;
1008 m_uiLocked = false;
1009 pref->setUILocked(false);
1010 app()->desktopIntegration()->menu()->setEnabled(true);
1011 return true;
1014 void MainWindow::notifyOfUpdate(const QString &)
1016 // Show restart message
1017 m_statusBar->showRestartRequired();
1018 LogMsg(tr("qBittorrent was just updated and needs to be restarted for the changes to be effective.")
1019 , Log::CRITICAL);
1020 // Delete the executable watcher
1021 delete m_executableWatcher;
1022 m_executableWatcher = nullptr;
1025 #ifndef Q_OS_MACOS
1026 // Toggle Main window visibility
1027 void MainWindow::toggleVisibility()
1029 if (isHidden())
1031 if (m_uiLocked && !unlockUI()) // Ask for UI lock password
1032 return;
1034 // Make sure the window is not minimized
1035 setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
1037 // Then show it
1038 show();
1039 raise();
1040 activateWindow();
1042 else
1044 hide();
1047 #endif // Q_OS_MACOS
1049 // Display About Dialog
1050 void MainWindow::on_actionAbout_triggered()
1052 // About dialog
1053 if (m_aboutDlg)
1055 m_aboutDlg->activateWindow();
1057 else
1059 m_aboutDlg = new AboutDialog(this);
1060 m_aboutDlg->setAttribute(Qt::WA_DeleteOnClose);
1061 m_aboutDlg->show();
1065 void MainWindow::on_actionStatistics_triggered()
1067 if (m_statsDlg)
1069 m_statsDlg->activateWindow();
1071 else
1073 m_statsDlg = new StatsDialog(this);
1074 m_statsDlg->setAttribute(Qt::WA_DeleteOnClose);
1075 m_statsDlg->show();
1079 void MainWindow::showEvent(QShowEvent *e)
1081 qDebug("** Show Event **");
1082 e->accept();
1084 if (isVisible())
1086 // preparations before showing the window
1088 if (m_neverShown)
1090 m_propertiesWidget->readSettings();
1091 m_neverShown = false;
1094 if (currentTabWidget() == m_transferListWidget)
1095 m_propertiesWidget->loadDynamicData();
1097 // Make sure the window is initially centered
1098 if (!m_posInitialized)
1100 move(Utils::Gui::screenCenter(this));
1101 m_posInitialized = true;
1104 else
1106 // to avoid blank screen when restoring from tray icon
1107 show();
1111 void MainWindow::keyPressEvent(QKeyEvent *event)
1113 if (event->matches(QKeySequence::Paste))
1115 const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData();
1116 if (mimeData->hasText())
1118 const QStringList lines = mimeData->text().split(u'\n', Qt::SkipEmptyParts);
1119 for (QString line : lines)
1121 line = line.trimmed();
1122 if (!Utils::Misc::isTorrentLink(line))
1123 continue;
1125 app()->addTorrentManager()->addTorrent(line);
1128 return;
1132 QMainWindow::keyPressEvent(event);
1135 // Called when we close the program
1136 void MainWindow::closeEvent(QCloseEvent *e)
1138 Preferences *const pref = Preferences::instance();
1139 #ifdef Q_OS_MACOS
1140 if (!m_forceExit)
1142 hide();
1143 e->accept();
1144 return;
1146 #else
1147 const bool goToSystrayOnExit = pref->closeToTray();
1148 if (!m_forceExit && app()->desktopIntegration()->isActive() && goToSystrayOnExit && !this->isHidden())
1150 e->ignore();
1151 QMetaObject::invokeMethod(this, &QWidget::hide, Qt::QueuedConnection);
1152 if (!pref->closeToTrayNotified())
1154 app()->desktopIntegration()->showNotification(tr("qBittorrent is closed to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
1155 pref->setCloseToTrayNotified(true);
1157 return;
1159 #endif // Q_OS_MACOS
1161 const QList<BitTorrent::Torrent *> allTorrents = BitTorrent::Session::instance()->torrents();
1162 const bool hasActiveTorrents = std::any_of(allTorrents.cbegin(), allTorrents.cend(), [](BitTorrent::Torrent *torrent)
1164 return torrent->isActive();
1166 if (pref->confirmOnExit() && hasActiveTorrents)
1168 if (e->spontaneous() || m_forceExit)
1170 if (!isVisible())
1171 show();
1172 QMessageBox confirmBox(QMessageBox::Question, tr("Exiting qBittorrent"),
1173 // Split it because the last sentence is used in the WebUI
1174 tr("Some files are currently transferring.") + u'\n' + tr("Are you sure you want to quit qBittorrent?"),
1175 QMessageBox::NoButton, this);
1176 QPushButton *noBtn = confirmBox.addButton(tr("&No"), QMessageBox::NoRole);
1177 confirmBox.addButton(tr("&Yes"), QMessageBox::YesRole);
1178 QPushButton *alwaysBtn = confirmBox.addButton(tr("&Always Yes"), QMessageBox::YesRole);
1179 confirmBox.setDefaultButton(noBtn);
1180 confirmBox.exec();
1181 if (!confirmBox.clickedButton() || (confirmBox.clickedButton() == noBtn))
1183 // Cancel exit
1184 e->ignore();
1185 m_forceExit = false;
1186 return;
1188 if (confirmBox.clickedButton() == alwaysBtn)
1189 // Remember choice
1190 Preferences::instance()->setConfirmOnExit(false);
1194 // Accept exit
1195 e->accept();
1196 qApp->exit();
1199 // Display window to create a torrent
1200 void MainWindow::on_actionCreateTorrent_triggered()
1202 createTorrentTriggered({});
1205 void MainWindow::createTorrentTriggered(const Path &path)
1207 if (m_createTorrentDlg)
1209 m_createTorrentDlg->updateInputPath(path);
1210 m_createTorrentDlg->activateWindow();
1212 else
1214 m_createTorrentDlg = new TorrentCreatorDialog(this, path);
1215 m_createTorrentDlg->setAttribute(Qt::WA_DeleteOnClose);
1216 m_createTorrentDlg->show();
1220 bool MainWindow::event(QEvent *e)
1222 #ifndef Q_OS_MACOS
1223 switch (e->type())
1225 case QEvent::WindowStateChange:
1226 qDebug("Window change event");
1227 // Now check to see if the window is minimised
1228 if (isMinimized())
1230 qDebug("minimisation");
1231 Preferences *const pref = Preferences::instance();
1232 if (app()->desktopIntegration()->isActive() && pref->minimizeToTray())
1234 qDebug() << "Has active window:" << (qApp->activeWindow() != nullptr);
1235 // Check if there is a modal window
1236 const QWidgetList allWidgets = QApplication::allWidgets();
1237 const bool hasModalWindow = std::any_of(allWidgets.cbegin(), allWidgets.cend()
1238 , [](const QWidget *widget) { return widget->isModal(); });
1239 // Iconify if there is no modal window
1240 if (!hasModalWindow)
1242 qDebug("Minimize to Tray enabled, hiding!");
1243 e->ignore();
1244 QMetaObject::invokeMethod(this, &QWidget::hide, Qt::QueuedConnection);
1245 if (!pref->minimizeToTrayNotified())
1247 app()->desktopIntegration()->showNotification(tr("qBittorrent is minimized to tray"), tr("This behavior can be changed in the settings. You won't be reminded again."));
1248 pref->setMinimizeToTrayNotified(true);
1250 return true;
1254 break;
1255 case QEvent::ToolBarChange:
1257 qDebug("MAC: Received a toolbar change event!");
1258 const bool ret = QMainWindow::event(e);
1260 qDebug("MAC: new toolbar visibility is %d", !m_ui->actionTopToolBar->isChecked());
1261 m_ui->actionTopToolBar->toggle();
1262 Preferences::instance()->setToolbarDisplayed(m_ui->actionTopToolBar->isChecked());
1263 return ret;
1265 default:
1266 break;
1268 #endif // Q_OS_MACOS
1270 return QMainWindow::event(e);
1273 // Display a dialog to allow user to add
1274 // torrents to download list
1275 void MainWindow::on_actionOpen_triggered()
1277 Preferences *const pref = Preferences::instance();
1278 // Open File Open Dialog
1279 // Note: it is possible to select more than one file
1280 const QStringList pathsList = QFileDialog::getOpenFileNames(this, tr("Open Torrent Files")
1281 , pref->getMainLastDir().data(), tr("Torrent Files") + u" (*" + TORRENT_FILE_EXTENSION + u')');
1283 if (pathsList.isEmpty())
1284 return;
1286 for (const QString &file : pathsList)
1287 app()->addTorrentManager()->addTorrent(file);
1289 // Save last dir to remember it
1290 const Path topDir {pathsList.at(0)};
1291 const Path parentDir = topDir.parentPath();
1292 pref->setMainLastDir(parentDir.isEmpty() ? topDir : parentDir);
1295 void MainWindow::activate()
1297 if (!m_uiLocked || unlockUI())
1299 show();
1300 activateWindow();
1301 raise();
1305 void MainWindow::optionsSaved()
1307 LogMsg(tr("Options saved."));
1308 loadPreferences();
1311 void MainWindow::showStatusBar(bool show)
1313 if (!show)
1315 // Remove status bar
1316 setStatusBar(nullptr);
1318 else if (!m_statusBar)
1320 // Create status bar
1321 m_statusBar = new StatusBar;
1322 connect(m_statusBar.data(), &StatusBar::connectionButtonClicked, this, &MainWindow::showConnectionSettings);
1323 connect(m_statusBar.data(), &StatusBar::alternativeSpeedsButtonClicked, this, &MainWindow::toggleAlternativeSpeeds);
1324 setStatusBar(m_statusBar);
1328 void MainWindow::showFiltersSidebar(const bool show)
1330 if (show && !m_transferListFiltersWidget)
1332 m_transferListFiltersWidget = new TransferListFiltersWidget(m_splitter, m_transferListWidget, isDownloadTrackerFavicon());
1333 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersAdded, m_transferListFiltersWidget, &TransferListFiltersWidget::addTrackers);
1334 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersRemoved, m_transferListFiltersWidget, &TransferListFiltersWidget::removeTrackers);
1335 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_transferListFiltersWidget, &TransferListFiltersWidget::refreshTrackers);
1336 connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntryStatusesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntryStatusesUpdated);
1338 m_splitter->insertWidget(0, m_transferListFiltersWidget);
1339 m_splitter->setCollapsible(0, true);
1340 // From https://doc.qt.io/qt-5/qsplitter.html#setSizes:
1341 // Instead, any additional/missing space is distributed amongst the widgets
1342 // according to the relative weight of the sizes.
1343 m_splitter->setStretchFactor(0, 0);
1344 m_splitter->setStretchFactor(1, 1);
1345 m_splitter->setSizes({Preferences::instance()->getFiltersSidebarWidth()});
1347 else if (!show && m_transferListFiltersWidget)
1349 saveSplitterSettings();
1350 delete m_transferListFiltersWidget;
1351 m_transferListFiltersWidget = nullptr;
1355 void MainWindow::loadPreferences()
1357 const Preferences *pref = Preferences::instance();
1359 // General
1360 if (pref->isToolbarDisplayed())
1362 m_ui->toolBar->setVisible(true);
1364 else
1366 // Clear search filter before hiding the top toolbar
1367 m_columnFilterEdit->clear();
1368 m_ui->toolBar->setVisible(false);
1371 showStatusBar(pref->isStatusbarDisplayed());
1373 m_transferListWidget->setAlternatingRowColors(pref->useAlternatingRowColors());
1374 m_propertiesWidget->getFilesList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1375 m_propertiesWidget->getTrackerList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1376 m_propertiesWidget->getPeerList()->setAlternatingRowColors(pref->useAlternatingRowColors());
1378 // Queueing System
1379 if (BitTorrent::Session::instance()->isQueueingSystemEnabled())
1381 if (!m_ui->actionDecreaseQueuePos->isVisible())
1383 m_transferListWidget->hideQueuePosColumn(false);
1384 m_ui->actionDecreaseQueuePos->setVisible(true);
1385 m_ui->actionIncreaseQueuePos->setVisible(true);
1386 m_ui->actionTopQueuePos->setVisible(true);
1387 m_ui->actionBottomQueuePos->setVisible(true);
1388 #ifndef Q_OS_MACOS
1389 m_queueSeparator->setVisible(true);
1390 #endif
1391 m_queueSeparatorMenu->setVisible(true);
1394 else
1396 if (m_ui->actionDecreaseQueuePos->isVisible())
1398 m_transferListWidget->hideQueuePosColumn(true);
1399 m_ui->actionDecreaseQueuePos->setVisible(false);
1400 m_ui->actionIncreaseQueuePos->setVisible(false);
1401 m_ui->actionTopQueuePos->setVisible(false);
1402 m_ui->actionBottomQueuePos->setVisible(false);
1403 #ifndef Q_OS_MACOS
1404 m_queueSeparator->setVisible(false);
1405 #endif
1406 m_queueSeparatorMenu->setVisible(false);
1410 // Torrent properties
1411 m_propertiesWidget->reloadPreferences();
1413 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1414 if (pref->isUpdateCheckEnabled())
1416 if (!m_programUpdateTimer)
1418 m_programUpdateTimer = new QTimer(this);
1419 m_programUpdateTimer->setInterval(24h);
1420 m_programUpdateTimer->setSingleShot(true);
1421 connect(m_programUpdateTimer, &QTimer::timeout, this, [this]() { checkProgramUpdate(false); });
1422 m_programUpdateTimer->start();
1425 else
1427 delete m_programUpdateTimer;
1428 m_programUpdateTimer = nullptr;
1430 #endif
1432 qDebug("GUI settings loaded");
1435 void MainWindow::loadSessionStats()
1437 const auto *btSession = BitTorrent::Session::instance();
1438 const BitTorrent::SessionStatus &status = btSession->status();
1439 m_downloadRate = Utils::Misc::friendlyUnit(status.payloadDownloadRate, true);
1440 m_uploadRate = Utils::Misc::friendlyUnit(status.payloadUploadRate, true);
1442 // update global information
1443 #ifdef Q_OS_MACOS
1444 m_badger->updateSpeed(status.payloadDownloadRate, status.payloadUploadRate);
1445 #else
1446 refreshTrayIconTooltip();
1447 #endif // Q_OS_MACOS
1449 refreshWindowTitle();
1452 void MainWindow::reloadTorrentStats(const QList<BitTorrent::Torrent *> &torrents)
1454 if (currentTabWidget() == m_transferListWidget)
1456 if (torrents.contains(m_propertiesWidget->getCurrentTorrent()))
1457 m_propertiesWidget->loadDynamicData();
1461 void MainWindow::downloadFromURLList(const QStringList &urlList)
1463 for (const QString &url : urlList)
1464 app()->addTorrentManager()->addTorrent(url);
1467 void MainWindow::populateDesktopIntegrationMenu()
1469 auto *menu = app()->desktopIntegration()->menu();
1470 menu->clear();
1472 #ifndef Q_OS_MACOS
1473 connect(menu, &QMenu::aboutToShow, this, [this]()
1475 m_ui->actionToggleVisibility->setText(isVisible() ? tr("Hide") : tr("Show"));
1477 connect(m_ui->actionToggleVisibility, &QAction::triggered, this, &MainWindow::toggleVisibility);
1479 menu->addAction(m_ui->actionToggleVisibility);
1480 menu->addSeparator();
1481 #endif
1483 menu->addAction(m_ui->actionOpen);
1484 menu->addAction(m_ui->actionDownloadFromURL);
1485 menu->addSeparator();
1487 menu->addAction(m_ui->actionUseAlternativeSpeedLimits);
1488 menu->addAction(m_ui->actionSetGlobalSpeedLimits);
1489 menu->addSeparator();
1491 menu->addAction(m_ui->actionResumeSession);
1492 menu->addAction(m_ui->actionPauseSession);
1494 #ifndef Q_OS_MACOS
1495 menu->addSeparator();
1496 menu->addAction(m_ui->actionExit);
1497 #endif
1499 if (m_uiLocked)
1500 menu->setEnabled(false);
1503 void MainWindow::updateAltSpeedsBtn(const bool alternative)
1505 m_ui->actionUseAlternativeSpeedLimits->setChecked(alternative);
1508 PropertiesWidget *MainWindow::propertiesWidget() const
1510 return m_propertiesWidget;
1513 // Display Program Options
1514 void MainWindow::on_actionOptions_triggered()
1516 if (m_options)
1518 m_options->activateWindow();
1520 else
1522 m_options = new OptionsDialog(app(), this);
1523 m_options->setAttribute(Qt::WA_DeleteOnClose);
1524 m_options->open();
1528 void MainWindow::on_actionTopToolBar_triggered()
1530 const bool isVisible = static_cast<QAction *>(sender())->isChecked();
1531 m_ui->toolBar->setVisible(isVisible);
1532 Preferences::instance()->setToolbarDisplayed(isVisible);
1535 void MainWindow::on_actionShowStatusbar_triggered()
1537 const bool isVisible = static_cast<QAction *>(sender())->isChecked();
1538 Preferences::instance()->setStatusbarDisplayed(isVisible);
1539 showStatusBar(isVisible);
1542 void MainWindow::on_actionShowFiltersSidebar_triggered(const bool checked)
1544 Preferences *const pref = Preferences::instance();
1545 pref->setFiltersSidebarVisible(checked);
1546 showFiltersSidebar(checked);
1549 void MainWindow::on_actionSpeedInTitleBar_triggered()
1551 m_displaySpeedInTitle = static_cast<QAction *>(sender())->isChecked();
1552 Preferences::instance()->showSpeedInTitleBar(m_displaySpeedInTitle);
1553 refreshWindowTitle();
1556 void MainWindow::on_actionRSSReader_triggered()
1558 Preferences::instance()->setRSSWidgetVisible(m_ui->actionRSSReader->isChecked());
1559 displayRSSTab(m_ui->actionRSSReader->isChecked());
1562 void MainWindow::on_actionSearchWidget_triggered()
1564 if (m_ui->actionSearchWidget->isChecked())
1566 const Utils::ForeignApps::PythonInfo pyInfo = Utils::ForeignApps::pythonInfo();
1568 // Not found
1569 if (!pyInfo.isValid())
1571 m_ui->actionSearchWidget->setChecked(false);
1572 Preferences::instance()->setSearchEnabled(false);
1574 #ifdef Q_OS_WIN
1575 const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Missing Python Runtime")
1576 , tr("Python is required to use the search engine but it does not seem to be installed.\nDo you want to install it now?")
1577 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
1578 if (buttonPressed == QMessageBox::Yes)
1579 installPython();
1580 #else
1581 QMessageBox::information(this, tr("Missing Python Runtime")
1582 , tr("Python is required to use the search engine but it does not seem to be installed."));
1583 #endif
1584 return;
1587 // Check version requirement
1588 if (!pyInfo.isSupportedVersion())
1590 m_ui->actionSearchWidget->setChecked(false);
1591 Preferences::instance()->setSearchEnabled(false);
1593 #ifdef Q_OS_WIN
1594 const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
1595 , tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
1596 .arg(pyInfo.version.toString(), u"3.9.0")
1597 , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
1598 if (buttonPressed == QMessageBox::Yes)
1599 installPython();
1600 #else
1601 QMessageBox::information(this, tr("Old Python Runtime")
1602 , tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
1603 .arg(pyInfo.version.toString(), u"3.9.0"));
1604 #endif
1605 return;
1608 m_ui->actionSearchWidget->setChecked(true);
1609 Preferences::instance()->setSearchEnabled(true);
1612 displaySearchTab(m_ui->actionSearchWidget->isChecked());
1615 // Display an input dialog to prompt user for
1616 // an url
1617 void MainWindow::on_actionDownloadFromURL_triggered()
1619 if (!m_downloadFromURLDialog)
1621 m_downloadFromURLDialog = new DownloadFromURLDialog(this);
1622 m_downloadFromURLDialog->setAttribute(Qt::WA_DeleteOnClose);
1623 connect(m_downloadFromURLDialog.data(), &DownloadFromURLDialog::urlsReadyToBeDownloaded, this, &MainWindow::downloadFromURLList);
1624 m_downloadFromURLDialog->open();
1628 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1629 void MainWindow::handleUpdateCheckFinished(ProgramUpdater *updater, const bool invokedByUser)
1631 m_ui->actionCheckForUpdates->setEnabled(true);
1632 m_ui->actionCheckForUpdates->setText(tr("&Check for Updates"));
1633 m_ui->actionCheckForUpdates->setToolTip(tr("Check for program updates"));
1635 const auto cleanup = [this, updater]()
1637 if (m_programUpdateTimer)
1638 m_programUpdateTimer->start();
1639 updater->deleteLater();
1642 const QString newVersion = updater->getNewVersion();
1643 if (!newVersion.isEmpty())
1645 const QString msg {tr("A new version is available.") + u"<br/>"
1646 + tr("Do you want to download %1?").arg(newVersion) + u"<br/><br/>"
1647 + u"<a href=\"https://www.qbittorrent.org/news.php\">%1</a>"_s.arg(tr("Open changelog..."))};
1648 auto *msgBox = new QMessageBox {QMessageBox::Question, tr("qBittorrent Update Available"), msg
1649 , (QMessageBox::Yes | QMessageBox::No), this};
1650 msgBox->setAttribute(Qt::WA_DeleteOnClose);
1651 msgBox->setAttribute(Qt::WA_ShowWithoutActivating);
1652 msgBox->setDefaultButton(QMessageBox::Yes);
1653 msgBox->setWindowModality(Qt::NonModal);
1654 connect(msgBox, &QMessageBox::buttonClicked, this, [msgBox, updater](QAbstractButton *button)
1656 if (msgBox->buttonRole(button) == QMessageBox::YesRole)
1658 updater->updateProgram();
1661 connect(msgBox, &QDialog::finished, this, cleanup);
1662 msgBox->show();
1664 else
1666 if (invokedByUser)
1668 auto *msgBox = new QMessageBox {QMessageBox::Information, u"qBittorrent"_s
1669 , tr("No updates available.\nYou are already using the latest version.")
1670 , QMessageBox::Ok, this};
1671 msgBox->setAttribute(Qt::WA_DeleteOnClose);
1672 msgBox->setWindowModality(Qt::NonModal);
1673 connect(msgBox, &QDialog::finished, this, cleanup);
1674 msgBox->show();
1676 else
1678 cleanup();
1682 #endif
1684 void MainWindow::toggleAlternativeSpeeds()
1686 BitTorrent::Session *const session = BitTorrent::Session::instance();
1687 session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled());
1690 void MainWindow::on_actionDonateMoney_triggered()
1692 QDesktopServices::openUrl(QUrl(u"https://www.qbittorrent.org/donate"_s));
1695 void MainWindow::showConnectionSettings()
1697 on_actionOptions_triggered();
1698 m_options->showConnectionTab();
1701 void MainWindow::minimizeWindow()
1703 setWindowState(windowState() | Qt::WindowMinimized);
1706 void MainWindow::on_actionExecutionLogs_triggered(bool checked)
1708 if (checked)
1710 Q_ASSERT(!m_executionLog);
1711 m_executionLog = new ExecutionLogWidget(executionLogMsgTypes(), m_tabs);
1712 #ifdef Q_OS_MACOS
1713 m_tabs->addTab(m_executionLog, tr("Execution Log"));
1714 #else
1715 const int indexTab = m_tabs->addTab(m_executionLog, tr("Execution Log"));
1716 m_tabs->setTabIcon(indexTab, UIThemeManager::instance()->getIcon(u"help-contents"_s));
1717 #endif
1719 else
1721 delete m_executionLog;
1724 m_ui->actionNormalMessages->setEnabled(checked);
1725 m_ui->actionInformationMessages->setEnabled(checked);
1726 m_ui->actionWarningMessages->setEnabled(checked);
1727 m_ui->actionCriticalMessages->setEnabled(checked);
1728 setExecutionLogEnabled(checked);
1731 void MainWindow::on_actionNormalMessages_triggered(const bool checked)
1733 if (!m_executionLog)
1734 return;
1736 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::NORMAL, checked);
1737 setExecutionLogMsgTypes(flags);
1740 void MainWindow::on_actionInformationMessages_triggered(const bool checked)
1742 if (!m_executionLog)
1743 return;
1745 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::INFO, checked);
1746 setExecutionLogMsgTypes(flags);
1749 void MainWindow::on_actionWarningMessages_triggered(const bool checked)
1751 if (!m_executionLog)
1752 return;
1754 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::WARNING, checked);
1755 setExecutionLogMsgTypes(flags);
1758 void MainWindow::on_actionCriticalMessages_triggered(const bool checked)
1760 if (!m_executionLog)
1761 return;
1763 const Log::MsgTypes flags = executionLogMsgTypes().setFlag(Log::CRITICAL, checked);
1764 setExecutionLogMsgTypes(flags);
1767 void MainWindow::on_actionAutoExit_toggled(bool enabled)
1769 qDebug() << Q_FUNC_INFO << enabled;
1770 Preferences::instance()->setShutdownqBTWhenDownloadsComplete(enabled);
1773 void MainWindow::on_actionAutoSuspend_toggled(bool enabled)
1775 qDebug() << Q_FUNC_INFO << enabled;
1776 Preferences::instance()->setSuspendWhenDownloadsComplete(enabled);
1779 void MainWindow::on_actionAutoHibernate_toggled(bool enabled)
1781 qDebug() << Q_FUNC_INFO << enabled;
1782 Preferences::instance()->setHibernateWhenDownloadsComplete(enabled);
1785 void MainWindow::on_actionAutoShutdown_toggled(bool enabled)
1787 qDebug() << Q_FUNC_INFO << enabled;
1788 Preferences::instance()->setShutdownWhenDownloadsComplete(enabled);
1791 void MainWindow::updatePowerManagementState() const
1793 const auto *pref = Preferences::instance();
1794 const bool preventFromSuspendWhenDownloading = pref->preventFromSuspendWhenDownloading();
1795 const bool preventFromSuspendWhenSeeding = pref->preventFromSuspendWhenSeeding();
1797 const QList<BitTorrent::Torrent *> allTorrents = BitTorrent::Session::instance()->torrents();
1798 const bool inhibitSuspend = std::any_of(allTorrents.cbegin(), allTorrents.cend(), [&](const BitTorrent::Torrent *torrent)
1800 if (preventFromSuspendWhenDownloading && (!torrent->isFinished() && !torrent->isStopped() && !torrent->isErrored() && torrent->hasMetadata()))
1801 return true;
1803 if (preventFromSuspendWhenSeeding && (torrent->isFinished() && !torrent->isStopped()))
1804 return true;
1806 return torrent->isMoving();
1808 m_pwr->setActivityState(inhibitSuspend);
1810 m_preventTimer->start(PREVENT_SUSPEND_INTERVAL);
1813 void MainWindow::applyTransferListFilter()
1815 m_transferListWidget->applyFilter(m_columnFilterEdit->text(), m_columnFilterComboBox->currentData().value<TransferListModel::Column>());
1818 void MainWindow::refreshWindowTitle()
1820 const auto *btSession = BitTorrent::Session::instance();
1821 if (btSession->isPaused())
1823 const QString title = tr("[PAUSED] %1", "%1 is the rest of the window title").arg(m_windowTitle);
1824 setWindowTitle(title);
1826 else
1828 if (m_displaySpeedInTitle)
1830 const QString title = tr("[D: %1, U: %2] %3", "D = Download; U = Upload; %3 is the rest of the window title")
1831 .arg(m_downloadRate, m_uploadRate, m_windowTitle);
1832 setWindowTitle(title);
1834 else
1836 setWindowTitle(m_windowTitle);
1841 void MainWindow::refreshTrayIconTooltip()
1843 const auto *btSession = BitTorrent::Session::instance();
1844 if (!btSession->isPaused())
1846 const auto toolTip = u"%1\n%2"_s.arg(
1847 tr("DL speed: %1", "e.g: Download speed: 10 KiB/s").arg(m_downloadRate)
1848 , tr("UP speed: %1", "e.g: Upload speed: 10 KiB/s").arg(m_uploadRate));
1849 app()->desktopIntegration()->setToolTip(toolTip);
1851 else
1853 app()->desktopIntegration()->setToolTip(tr("Paused"));
1857 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
1858 void MainWindow::checkProgramUpdate(const bool invokedByUser)
1860 if (m_programUpdateTimer)
1861 m_programUpdateTimer->stop();
1863 m_ui->actionCheckForUpdates->setEnabled(false);
1864 m_ui->actionCheckForUpdates->setText(tr("Checking for Updates..."));
1865 m_ui->actionCheckForUpdates->setToolTip(tr("Already checking for program updates in the background"));
1867 auto *updater = new ProgramUpdater(this);
1868 connect(updater, &ProgramUpdater::updateCheckFinished
1869 , this, [this, invokedByUser, updater]()
1871 handleUpdateCheckFinished(updater, invokedByUser);
1873 updater->checkForUpdates();
1875 #endif
1877 #ifdef Q_OS_WIN
1878 void MainWindow::installPython()
1880 setCursor(QCursor(Qt::WaitCursor));
1881 // Download python
1882 const auto installerURL = u"https://www.python.org/ftp/python/3.13.0/python-3.13.0-amd64.exe"_s;
1883 Net::DownloadManager::instance()->download(
1884 Net::DownloadRequest(installerURL).saveToFile(true)
1885 , Preferences::instance()->useProxyForGeneralPurposes()
1886 , this, &MainWindow::pythonDownloadFinished);
1889 void MainWindow::pythonDownloadFinished(const Net::DownloadResult &result)
1891 if (result.status != Net::DownloadStatus::Success)
1893 setCursor(QCursor(Qt::ArrowCursor));
1894 QMessageBox::warning(
1895 this, tr("Download error")
1896 , tr("Python setup could not be downloaded, reason: %1.\nPlease install it manually.")
1897 .arg(result.errorString));
1898 return;
1901 setCursor(QCursor(Qt::ArrowCursor));
1902 QProcess installer;
1903 qDebug("Launching Python installer in passive mode...");
1905 const Path exePath = result.filePath + u".exe";
1906 Utils::Fs::renameFile(result.filePath, exePath);
1907 installer.start(exePath.toString(), {u"/passive"_s});
1909 // Wait for setup to complete
1910 installer.waitForFinished(10 * 60 * 1000);
1912 qDebug("Installer stdout: %s", installer.readAllStandardOutput().data());
1913 qDebug("Installer stderr: %s", installer.readAllStandardError().data());
1914 qDebug("Setup should be complete!");
1916 // Delete temp file
1917 Utils::Fs::removeFile(exePath);
1919 // Reload search engine
1920 if (Utils::ForeignApps::pythonInfo().isSupportedVersion())
1922 m_ui->actionSearchWidget->setChecked(true);
1923 displaySearchTab(true);
1926 #endif // Q_OS_WIN