2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015 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 "pluginselectdialog.h"
34 #include <QFileDialog>
35 #include <QHeaderView>
36 #include <QImageReader>
38 #include <QMessageBox>
42 #include "base/global.h"
43 #include "base/net/downloadmanager.h"
44 #include "base/utils/fs.h"
45 #include "gui/autoexpandabledialog.h"
46 #include "gui/uithememanager.h"
47 #include "gui/utils.h"
48 #include "pluginsourcedialog.h"
49 #include "searchwidget.h"
50 #include "ui_pluginselectdialog.h"
52 #define SETTINGS_KEY(name) "SearchPluginSelectDialog/" name
63 PluginSelectDialog::PluginSelectDialog(SearchPluginManager
*pluginManager
, QWidget
*parent
)
65 , m_ui(new Ui::PluginSelectDialog
)
66 , m_storeDialogSize(SETTINGS_KEY("Size"))
67 , m_pluginManager(pluginManager
)
70 setAttribute(Qt::WA_DeleteOnClose
);
72 // This hack fixes reordering of first column with Qt5.
73 // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
75 unused
.setVerticalHeader(m_ui
->pluginsTree
->header());
76 m_ui
->pluginsTree
->header()->setParent(m_ui
->pluginsTree
);
77 unused
.setVerticalHeader(new QHeaderView(Qt::Horizontal
));
79 m_ui
->pluginsTree
->setRootIsDecorated(false);
80 m_ui
->pluginsTree
->hideColumn(PLUGIN_ID
);
81 m_ui
->pluginsTree
->header()->setSortIndicator(0, Qt::AscendingOrder
);
83 m_ui
->actionUninstall
->setIcon(UIThemeManager::instance()->getIcon("list-remove"));
85 connect(m_ui
->actionEnable
, &QAction::toggled
, this, &PluginSelectDialog::enableSelection
);
86 connect(m_ui
->pluginsTree
, &QTreeWidget::customContextMenuRequested
, this, &PluginSelectDialog::displayContextMenu
);
87 connect(m_ui
->pluginsTree
, &QTreeWidget::itemDoubleClicked
, this, &PluginSelectDialog::togglePluginState
);
89 loadSupportedSearchPlugins();
91 connect(m_pluginManager
, &SearchPluginManager::pluginInstalled
, this, &PluginSelectDialog::pluginInstalled
);
92 connect(m_pluginManager
, &SearchPluginManager::pluginInstallationFailed
, this, &PluginSelectDialog::pluginInstallationFailed
);
93 connect(m_pluginManager
, &SearchPluginManager::pluginUpdated
, this, &PluginSelectDialog::pluginUpdated
);
94 connect(m_pluginManager
, &SearchPluginManager::pluginUpdateFailed
, this, &PluginSelectDialog::pluginUpdateFailed
);
95 connect(m_pluginManager
, &SearchPluginManager::checkForUpdatesFinished
, this, &PluginSelectDialog::checkForUpdatesFinished
);
96 connect(m_pluginManager
, &SearchPluginManager::checkForUpdatesFailed
, this, &PluginSelectDialog::checkForUpdatesFailed
);
98 Utils::Gui::resize(this, m_storeDialogSize
);
102 PluginSelectDialog::~PluginSelectDialog()
104 m_storeDialogSize
= size();
108 void PluginSelectDialog::dropEvent(QDropEvent
*event
)
110 event
->acceptProposedAction();
113 if (event
->mimeData()->hasUrls())
115 for (const QUrl
&url
: asConst(event
->mimeData()->urls()))
119 if (url
.scheme().compare("file", Qt::CaseInsensitive
) == 0)
120 files
<< url
.toLocalFile();
122 files
<< url
.toString();
128 files
= event
->mimeData()->text().split('\n');
131 if (files
.isEmpty()) return;
133 for (const QString
&file
: asConst(files
))
135 qDebug("dropped %s", qUtf8Printable(file
));
137 m_pluginManager
->installPlugin(file
);
141 // Decode if we accept drag 'n drop or not
142 void PluginSelectDialog::dragEnterEvent(QDragEnterEvent
*event
)
144 for (const QString
&mime
: asConst(event
->mimeData()->formats()))
146 qDebug("mimeData: %s", qUtf8Printable(mime
));
149 if (event
->mimeData()->hasFormat(QLatin1String("text/plain")) || event
->mimeData()->hasFormat(QLatin1String("text/uri-list")))
151 event
->acceptProposedAction();
155 void PluginSelectDialog::on_updateButton_clicked()
158 m_pluginManager
->checkForUpdates();
161 void PluginSelectDialog::togglePluginState(QTreeWidgetItem
*item
, int)
163 PluginInfo
*plugin
= m_pluginManager
->pluginInfo(item
->text(PLUGIN_ID
));
164 m_pluginManager
->enablePlugin(plugin
->name
, !plugin
->enabled
);
167 item
->setText(PLUGIN_STATE
, tr("Yes"));
168 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), "green");
172 item
->setText(PLUGIN_STATE
, tr("No"));
173 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), "red");
177 void PluginSelectDialog::displayContextMenu(const QPoint
&)
179 // Enable/disable pause/start action given the DL state
180 const QList
<QTreeWidgetItem
*> items
= m_ui
->pluginsTree
->selectedItems();
181 if (items
.isEmpty()) return;
183 QMenu
*myContextMenu
= new QMenu(this);
184 myContextMenu
->setAttribute(Qt::WA_DeleteOnClose
);
186 const QString firstID
= items
.first()->text(PLUGIN_ID
);
187 m_ui
->actionEnable
->setChecked(m_pluginManager
->pluginInfo(firstID
)->enabled
);
188 myContextMenu
->addAction(m_ui
->actionEnable
);
189 myContextMenu
->addSeparator();
190 myContextMenu
->addAction(m_ui
->actionUninstall
);
192 myContextMenu
->popup(QCursor::pos());
195 void PluginSelectDialog::on_closeButton_clicked()
200 void PluginSelectDialog::on_actionUninstall_triggered()
203 for (QTreeWidgetItem
*item
: asConst(m_ui
->pluginsTree
->selectedItems()))
205 int index
= m_ui
->pluginsTree
->indexOfTopLevelItem(item
);
206 Q_ASSERT(index
!= -1);
207 QString id
= item
->text(PLUGIN_ID
);
208 if (m_pluginManager
->uninstallPlugin(id
))
215 // Disable it instead
216 m_pluginManager
->enablePlugin(id
, false);
217 item
->setText(PLUGIN_STATE
, tr("No"));
218 setRowColor(index
, "red");
223 QMessageBox::warning(this, tr("Uninstall warning"), tr("Some plugins could not be uninstalled because they are included in qBittorrent. Only the ones you added yourself can be uninstalled.\nThose plugins were disabled."));
225 QMessageBox::information(this, tr("Uninstall success"), tr("All selected plugins were uninstalled successfully"));
228 void PluginSelectDialog::enableSelection(bool enable
)
230 for (QTreeWidgetItem
*item
: asConst(m_ui
->pluginsTree
->selectedItems()))
232 int index
= m_ui
->pluginsTree
->indexOfTopLevelItem(item
);
233 Q_ASSERT(index
!= -1);
234 QString id
= item
->text(PLUGIN_ID
);
235 m_pluginManager
->enablePlugin(id
, enable
);
238 item
->setText(PLUGIN_STATE
, tr("Yes"));
239 setRowColor(index
, "green");
243 item
->setText(PLUGIN_STATE
, tr("No"));
244 setRowColor(index
, "red");
249 // Set the color of a row in data model
250 void PluginSelectDialog::setRowColor(const int row
, const QString
&color
)
252 QTreeWidgetItem
*item
= m_ui
->pluginsTree
->topLevelItem(row
);
253 for (int i
= 0; i
< m_ui
->pluginsTree
->columnCount(); ++i
)
255 item
->setData(i
, Qt::ForegroundRole
, QColor(color
));
259 QVector
<QTreeWidgetItem
*> PluginSelectDialog::findItemsWithUrl(const QString
&url
)
261 QVector
<QTreeWidgetItem
*> res
;
262 res
.reserve(m_ui
->pluginsTree
->topLevelItemCount());
264 for (int i
= 0; i
< m_ui
->pluginsTree
->topLevelItemCount(); ++i
)
266 QTreeWidgetItem
*item
= m_ui
->pluginsTree
->topLevelItem(i
);
267 if (url
.startsWith(item
->text(PLUGIN_URL
), Qt::CaseInsensitive
))
274 QTreeWidgetItem
*PluginSelectDialog::findItemWithID(const QString
&id
)
276 for (int i
= 0; i
< m_ui
->pluginsTree
->topLevelItemCount(); ++i
)
278 QTreeWidgetItem
*item
= m_ui
->pluginsTree
->topLevelItem(i
);
279 if (id
== item
->text(PLUGIN_ID
))
286 void PluginSelectDialog::loadSupportedSearchPlugins()
288 // Some clean up first
289 m_ui
->pluginsTree
->clear();
290 for (const QString
&name
: asConst(m_pluginManager
->allPlugins()))
294 void PluginSelectDialog::addNewPlugin(const QString
&pluginName
)
296 auto *item
= new QTreeWidgetItem(m_ui
->pluginsTree
);
297 PluginInfo
*plugin
= m_pluginManager
->pluginInfo(pluginName
);
298 item
->setText(PLUGIN_NAME
, plugin
->fullName
);
299 item
->setText(PLUGIN_URL
, plugin
->url
);
300 item
->setText(PLUGIN_ID
, plugin
->name
);
303 item
->setText(PLUGIN_STATE
, tr("Yes"));
304 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), "green");
308 item
->setText(PLUGIN_STATE
, tr("No"));
309 setRowColor(m_ui
->pluginsTree
->indexOfTopLevelItem(item
), "red");
312 if (QFile::exists(plugin
->iconPath
))
314 // Good, we already have the icon
315 item
->setData(PLUGIN_NAME
, Qt::DecorationRole
, QIcon(plugin
->iconPath
));
319 // Icon is missing, we must download it
321 DownloadManager::instance()->download(
322 DownloadRequest(plugin
->url
+ "/favicon.ico").saveToFile(true)
323 , this, &PluginSelectDialog::iconDownloadFinished
);
325 item
->setText(PLUGIN_VERSION
, plugin
->version
);
328 void PluginSelectDialog::startAsyncOp()
332 setCursor(QCursor(Qt::WaitCursor
));
335 void PluginSelectDialog::finishAsyncOp()
339 setCursor(QCursor(Qt::ArrowCursor
));
342 void PluginSelectDialog::finishPluginUpdate()
345 if ((m_pendingUpdates
== 0) && !m_updatedPlugins
.isEmpty())
347 m_updatedPlugins
.sort(Qt::CaseInsensitive
);
348 QMessageBox::information(this, tr("Search plugin update"), tr("Plugins installed or updated: %1").arg(m_updatedPlugins
.join(", ")));
349 m_updatedPlugins
.clear();
353 void PluginSelectDialog::on_installButton_clicked()
355 auto *dlg
= new PluginSourceDialog(this);
356 connect(dlg
, &PluginSourceDialog::askForLocalFile
, this, &PluginSelectDialog::askForLocalPlugin
);
357 connect(dlg
, &PluginSourceDialog::askForUrl
, this, &PluginSelectDialog::askForPluginUrl
);
360 void PluginSelectDialog::askForPluginUrl()
363 QString clipTxt
= qApp
->clipboard()->text();
364 QString defaultUrl
= "http://";
365 if (Net::DownloadManager::hasSupportedScheme(clipTxt
) && clipTxt
.endsWith(".py"))
366 defaultUrl
= clipTxt
;
367 QString url
= AutoExpandableDialog::getText(
368 this, tr("New search engine plugin URL"),
369 tr("URL:"), QLineEdit::Normal
, defaultUrl
, &ok
372 while (ok
&& !url
.isEmpty() && !url
.endsWith(".py"))
374 QMessageBox::warning(this, tr("Invalid link"), tr("The link doesn't seem to point to a search engine plugin."));
375 url
= AutoExpandableDialog::getText(
376 this, tr("New search engine plugin URL"),
377 tr("URL:"), QLineEdit::Normal
, url
, &ok
381 if (ok
&& !url
.isEmpty())
384 m_pluginManager
->installPlugin(url
);
388 void PluginSelectDialog::askForLocalPlugin()
390 const QStringList pathsList
= QFileDialog::getOpenFileNames(
391 nullptr, tr("Select search plugins"), QDir::homePath(),
392 tr("qBittorrent search plugin") + QLatin1String(" (*.py)")
394 for (const QString
&path
: pathsList
)
397 m_pluginManager
->installPlugin(path
);
401 void PluginSelectDialog::iconDownloadFinished(const Net::DownloadResult
&result
)
403 if (result
.status
!= Net::DownloadStatus::Success
)
405 qDebug("Could not download favicon: %s, reason: %s", qUtf8Printable(result
.url
), qUtf8Printable(result
.errorString
));
409 const QString filePath
= Utils::Fs::toUniformPath(result
.filePath
);
412 QIcon
icon(filePath
);
413 // Detect a non-decodable icon
414 QList
<QSize
> sizes
= icon
.availableSizes();
415 bool invalid
= (sizes
.isEmpty() || icon
.pixmap(sizes
.first()).isNull());
418 for (QTreeWidgetItem
*item
: asConst(findItemsWithUrl(result
.url
)))
420 QString id
= item
->text(PLUGIN_ID
);
421 PluginInfo
*plugin
= m_pluginManager
->pluginInfo(id
);
422 if (!plugin
) continue;
424 QString iconPath
= QString("%1/%2.%3")
425 .arg(SearchPluginManager::pluginsLocation()
427 , result
.url
.endsWith(".ico", Qt::CaseInsensitive
) ? "ico" : "png");
428 if (QFile::copy(filePath
, iconPath
))
430 // This 2nd check is necessary. Some favicons (eg from piratebay)
431 // decode fine without an ext, but fail to do so when appending the ext
432 // from the url. Probably a Qt bug.
433 QIcon
iconWithExt(iconPath
);
434 QList
<QSize
> sizesExt
= iconWithExt
.availableSizes();
435 bool invalidExt
= (sizesExt
.isEmpty() || iconWithExt
.pixmap(sizesExt
.first()).isNull());
438 Utils::Fs::forceRemove(iconPath
);
442 item
->setData(PLUGIN_NAME
, Qt::DecorationRole
, iconWithExt
);
443 m_pluginManager
->updateIconPath(plugin
);
448 Utils::Fs::forceRemove(filePath
);
451 void PluginSelectDialog::checkForUpdatesFinished(const QHash
<QString
, PluginVersion
> &updateInfo
)
454 if (updateInfo
.isEmpty())
456 QMessageBox::information(this, tr("Search plugin update"), tr("All your plugins are already up to date."));
460 for (auto i
= updateInfo
.cbegin(); i
!= updateInfo
.cend(); ++i
)
464 m_pluginManager
->updatePlugin(i
.key());
468 void PluginSelectDialog::checkForUpdatesFailed(const QString
&reason
)
471 QMessageBox::warning(this, tr("Search plugin update"), tr("Sorry, couldn't check for plugin updates. %1").arg(reason
));
474 void PluginSelectDialog::pluginInstalled(const QString
&name
)
478 m_updatedPlugins
.append(name
);
479 finishPluginUpdate();
482 void PluginSelectDialog::pluginInstallationFailed(const QString
&name
, const QString
&reason
)
485 QMessageBox::information(this, tr("Search plugin install")
486 , tr("Couldn't install \"%1\" search engine plugin. %2").arg(name
, reason
));
487 finishPluginUpdate();
490 void PluginSelectDialog::pluginUpdated(const QString
&name
)
493 PluginVersion version
= m_pluginManager
->pluginInfo(name
)->version
;
494 QTreeWidgetItem
*item
= findItemWithID(name
);
495 item
->setText(PLUGIN_VERSION
, version
);
496 m_updatedPlugins
.append(name
);
497 finishPluginUpdate();
500 void PluginSelectDialog::pluginUpdateFailed(const QString
&name
, const QString
&reason
)
503 QMessageBox::information(this, tr("Search plugin update")
504 , tr("Couldn't update \"%1\" search engine plugin. %2").arg(name
, reason
));
505 finishPluginUpdate();