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>
38 #include <QActionGroup>
40 #include <QCloseEvent>
43 #include <QDesktopServices>
44 #include <QFileDialog>
45 #include <QFileSystemWatcher>
48 #include <QMessageBox>
49 #include <QMetaObject>
52 #include <QPushButton>
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"
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"
101 #include "macosdockbadge/badger.h"
103 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
104 #include "programupdater.h"
107 using namespace std::chrono_literals
;
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
}
126 , m_badger
{std::make_unique
<MacUtils::Badger
>()}
131 Preferences
*const pref
= Preferences::instance();
132 m_uiLocked
= pref
->isUILocked();
133 m_displaySpeedInTitle
= pref
->speedInTitleBar();
136 setWindowIcon(UIThemeManager::instance()->getIcon(u
"qbittorrent"_s
));
139 setTitleSuffix(titleSuffix
);
141 #if (defined(Q_OS_UNIX))
142 m_ui
->actionOptions
->setText(tr("Preferences"));
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
);
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
);
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
,
244 UIThemeManager::instance()->getIcon(u
"folder-remote"_s
),
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
);
260 // Increase top spacing to avoid tab overlapping
261 m_ui
->centralWidgetLayout
->addSpacing(8);
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
);
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
);
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);
315 m_ui
->actionCheckForUpdates
->setVisible(false);
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
338 connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated
, this, &MainWindow::loadSessionStats
);
339 connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated
, this, &MainWindow::reloadTorrentStats
);
341 createKeyboardShortcuts();
344 setUnifiedTitleAndToolBarOnMac(true);
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());
382 m_ui
->actionAutoShutdown
->setDisabled(true);
383 m_ui
->actionAutoSuspend
->setDisabled(true);
384 m_ui
->actionAutoHibernate
->setDisabled(true);
386 m_ui
->actionAutoExit
->setChecked(pref
->shutdownqBTWhenDownloadsComplete());
388 if (!autoShutdownGroup
->checkedAction())
389 m_ui
->actionAutoShutdownDisabled
->setChecked(true);
391 // Load Window state and sizes
394 populateDesktopIntegrationMenu();
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());
402 connect(app
->desktopIntegration(), &DesktopIntegration::notificationClicked
, this, &MainWindow::desktopNotificationClicked
);
403 connect(app
->desktopIntegration(), &DesktopIntegration::activationRequested
, this, [this]()
414 if (initialState
== WindowState::Normal
)
422 // Make sure the Window is visible if we don't have a tray icon
426 if (app
->desktopIntegration()->isActive())
428 if ((initialState
== WindowState::Normal
) && !m_uiLocked
)
434 else if (initialState
== WindowState::Minimized
)
437 if (pref
->minimizeToTray())
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);
450 // Make sure the Window is visible if we don't have a tray icon
451 if (initialState
!= WindowState::Normal
)
464 const bool isFiltersSidebarVisible
= pref
->isFiltersSidebarVisible();
465 m_ui
->actionShowFiltersSidebar
->setChecked(isFiltersSidebarVisible
);
466 if (isFiltersSidebarVisible
)
468 showFiltersSidebar(true);
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)
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
);
495 MainWindow::~MainWindow()
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
);
574 case Qt::ToolButtonIconOnly
:
575 iconsOnly
->setChecked(true);
577 case Qt::ToolButtonTextOnly
:
578 textOnly
->setChecked(true);
580 case Qt::ToolButtonTextBesideIcon
:
581 textBesideIcons
->setChecked(true);
583 case Qt::ToolButtonTextUnderIcon
:
584 textUnderIcons
->setChecked(true);
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()
636 const QString newPassword
= AutoExpandableDialog::getText(this, tr("UI lock password")
637 , tr("Please type the UI lock password:"), QLineEdit::Password
, {}, &ok
);
641 if (newPassword
.size() < 3)
643 QMessageBox::warning(this, tr("Invalid password"), tr("The password must be at least 3 characters long"));
647 Preferences::instance()->setUILockPassword(Utils::Password::PBKDF2::generate(newPassword
));
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())
670 // Lock the interface
672 pref
->setUILocked(true);
673 app()->desktopIntegration()->menu()->setEnabled(false);
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
)
689 m_rssWidget
= new RSSWidget(app(), m_tabs
);
690 connect(m_rssWidget
.data(), &RSSWidget::unreadCountUpdated
, this, &MainWindow::handleRSSUnreadCountUpdated
);
692 m_tabs
->addTab(m_rssWidget
, tr("RSS (%1)").arg(RSS::Session::instance()->rootFolder()->unreadCount()));
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
));
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
);
730 m_searchWidget
= new SearchWidget(app(), this);
731 m_tabs
->insertTab(1, m_searchWidget
,
733 UIThemeManager::instance()->getIcon(u
"edit-find"_s
),
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();
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);
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
)
800 auto *pref
= Preferences::instance();
801 pref
->setFiltersSidebarWidth(m_splitter
->sizes()[0]);
804 void MainWindow::cleanup()
809 saveSplitterSettings();
812 // delete RSSWidget explicitly to avoid crash in
813 // handleRSSUnreadCountUpdated() at application shutdown
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();
825 // remove all child widgets
826 while (auto *w
= findChild
<QWidget
*>())
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()
847 // Ask for UI lock password
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
);
869 m_ui
->actionCloseWindow
->setShortcut(QKeySequence::Close
);
871 m_ui
->actionCloseWindow
->setVisible(false);
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
);
899 m_ui
->actionMinimize
->setShortcut(Qt::CTRL
| Qt::Key_M
);
900 addAction(m_ui
->actionMinimize
);
904 // Keyboard shortcuts slots
905 void MainWindow::displayTransferTab() const
907 m_tabs
->setCurrentWidget(m_splitter
);
910 void MainWindow::displaySearchTab()
914 m_ui
->actionSearchWidget
->setChecked(true);
915 displaySearchTab(true);
918 m_tabs
->setCurrentWidget(m_searchWidget
);
921 void MainWindow::displayRSSTab()
925 m_ui
->actionRSSReader
->setChecked(true);
929 m_tabs
->setCurrentWidget(m_rssWidget
);
932 void MainWindow::displayExecutionLogTab()
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
);
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;
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.
975 QWidget
*MainWindow::currentTabWidget() const
977 if (isMinimized() || !isVisible())
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
)
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"));
1009 pref
->setUILocked(false);
1010 app()->desktopIntegration()->menu()->setEnabled(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.")
1020 // Delete the executable watcher
1021 delete m_executableWatcher
;
1022 m_executableWatcher
= nullptr;
1026 // Toggle Main window visibility
1027 void MainWindow::toggleVisibility()
1031 if (m_uiLocked
&& !unlockUI()) // Ask for UI lock password
1034 // Make sure the window is not minimized
1035 setWindowState((windowState() & ~Qt::WindowMinimized
) | Qt::WindowActive
);
1047 #endif // Q_OS_MACOS
1049 // Display About Dialog
1050 void MainWindow::on_actionAbout_triggered()
1055 m_aboutDlg
->activateWindow();
1059 m_aboutDlg
= new AboutDialog(this);
1060 m_aboutDlg
->setAttribute(Qt::WA_DeleteOnClose
);
1065 void MainWindow::on_actionStatistics_triggered()
1069 m_statsDlg
->activateWindow();
1073 m_statsDlg
= new StatsDialog(this);
1074 m_statsDlg
->setAttribute(Qt::WA_DeleteOnClose
);
1079 void MainWindow::showEvent(QShowEvent
*e
)
1081 qDebug("** Show Event **");
1086 // preparations before showing the window
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;
1106 // to avoid blank screen when restoring from tray icon
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
))
1125 app()->addTorrentManager()->addTorrent(line
);
1132 QMainWindow::keyPressEvent(event
);
1135 // Called when we close the program
1136 void MainWindow::closeEvent(QCloseEvent
*e
)
1138 Preferences
*const pref
= Preferences::instance();
1147 const bool goToSystrayOnExit
= pref
->closeToTray();
1148 if (!m_forceExit
&& app()->desktopIntegration()->isActive() && goToSystrayOnExit
&& !this->isHidden())
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);
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
)
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
);
1181 if (!confirmBox
.clickedButton() || (confirmBox
.clickedButton() == noBtn
))
1185 m_forceExit
= false;
1188 if (confirmBox
.clickedButton() == alwaysBtn
)
1190 Preferences::instance()->setConfirmOnExit(false);
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();
1214 m_createTorrentDlg
= new TorrentCreatorDialog(this, path
);
1215 m_createTorrentDlg
->setAttribute(Qt::WA_DeleteOnClose
);
1216 m_createTorrentDlg
->show();
1220 bool MainWindow::event(QEvent
*e
)
1225 case QEvent::WindowStateChange
:
1226 qDebug("Window change event");
1227 // Now check to see if the window is minimised
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!");
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);
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());
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())
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())
1305 void MainWindow::optionsSaved()
1307 LogMsg(tr("Options saved."));
1311 void MainWindow::showStatusBar(bool 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();
1360 if (pref
->isToolbarDisplayed())
1362 m_ui
->toolBar
->setVisible(true);
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());
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);
1389 m_queueSeparator
->setVisible(true);
1391 m_queueSeparatorMenu
->setVisible(true);
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);
1404 m_queueSeparator
->setVisible(false);
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();
1427 delete m_programUpdateTimer
;
1428 m_programUpdateTimer
= nullptr;
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
1444 m_badger
->updateSpeed(status
.payloadDownloadRate
, status
.payloadUploadRate
);
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();
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();
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
);
1495 menu
->addSeparator();
1496 menu
->addAction(m_ui
->actionExit
);
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()
1518 m_options
->activateWindow();
1522 m_options
= new OptionsDialog(app(), this);
1523 m_options
->setAttribute(Qt::WA_DeleteOnClose
);
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();
1569 if (!pyInfo
.isValid())
1571 m_ui
->actionSearchWidget
->setChecked(false);
1572 Preferences::instance()->setSearchEnabled(false);
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
)
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."));
1587 // Check version requirement
1588 if (!pyInfo
.isSupportedVersion())
1590 m_ui
->actionSearchWidget
->setChecked(false);
1591 Preferences::instance()->setSearchEnabled(false);
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
)
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"));
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
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
);
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
);
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
)
1710 Q_ASSERT(!m_executionLog
);
1711 m_executionLog
= new ExecutionLogWidget(executionLogMsgTypes(), m_tabs
);
1713 m_tabs
->addTab(m_executionLog
, tr("Execution Log"));
1715 const int indexTab
= m_tabs
->addTab(m_executionLog
, tr("Execution Log"));
1716 m_tabs
->setTabIcon(indexTab
, UIThemeManager::instance()->getIcon(u
"help-contents"_s
));
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
)
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
)
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
)
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
)
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()))
1803 if (preventFromSuspendWhenSeeding
&& (torrent
->isFinished() && !torrent
->isStopped()))
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
);
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
);
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
);
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();
1878 void MainWindow::installPython()
1880 setCursor(QCursor(Qt::WaitCursor
));
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
));
1901 setCursor(QCursor(Qt::ArrowCursor
));
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!");
1917 Utils::Fs::removeFile(exePath
);
1919 // Reload search engine
1920 if (Utils::ForeignApps::pythonInfo().isSupportedVersion())
1922 m_ui
->actionSearchWidget
->setChecked(true);
1923 displaySearchTab(true);