qBittorrent
pluginselectdialog.cpp
Go to the documentation of this file.
1 /*
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>
5  *
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.
10  *
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.
15  *
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.
19  *
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.
28  */
29 
30 #include "pluginselectdialog.h"
31 
32 #include <QClipboard>
33 #include <QDropEvent>
34 #include <QFileDialog>
35 #include <QHeaderView>
36 #include <QImageReader>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QMimeData>
40 #include <QTableView>
41 
42 #include "base/global.h"
44 #include "base/utils/fs.h"
46 #include "gui/uithememanager.h"
47 #include "gui/utils.h"
48 #include "pluginsourcedialog.h"
49 #include "searchwidget.h"
50 #include "ui_pluginselectdialog.h"
51 
52 #define SETTINGS_KEY(name) "SearchPluginSelectDialog/" name
53 
55 {
60  PLUGIN_ID
61 };
62 
64  : QDialog(parent)
65  , m_ui(new Ui::PluginSelectDialog)
66  , m_storeDialogSize(SETTINGS_KEY("Size"))
67  , m_pluginManager(pluginManager)
68 {
69  m_ui->setupUi(this);
70  setAttribute(Qt::WA_DeleteOnClose);
71 
72  // This hack fixes reordering of first column with Qt5.
73  // https://github.com/qtproject/qtbase/commit/e0fc088c0c8bc61dbcaf5928b24986cd61a22777
74  QTableView unused;
75  unused.setVerticalHeader(m_ui->pluginsTree->header());
76  m_ui->pluginsTree->header()->setParent(m_ui->pluginsTree);
77  unused.setVerticalHeader(new QHeaderView(Qt::Horizontal));
78 
79  m_ui->pluginsTree->setRootIsDecorated(false);
80  m_ui->pluginsTree->hideColumn(PLUGIN_ID);
81  m_ui->pluginsTree->header()->setSortIndicator(0, Qt::AscendingOrder);
82 
83  m_ui->actionUninstall->setIcon(UIThemeManager::instance()->getIcon("list-remove"));
84 
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);
88 
90 
97 
99  show();
100 }
101 
103 {
104  m_storeDialogSize = size();
105  delete m_ui;
106 }
107 
108 void PluginSelectDialog::dropEvent(QDropEvent *event)
109 {
110  event->acceptProposedAction();
111 
112  QStringList files;
113  if (event->mimeData()->hasUrls())
114  {
115  for (const QUrl &url : asConst(event->mimeData()->urls()))
116  {
117  if (!url.isEmpty())
118  {
119  if (url.scheme().compare("file", Qt::CaseInsensitive) == 0)
120  files << url.toLocalFile();
121  else
122  files << url.toString();
123  }
124  }
125  }
126  else
127  {
128  files = event->mimeData()->text().split('\n');
129  }
130 
131  if (files.isEmpty()) return;
132 
133  for (const QString &file : asConst(files))
134  {
135  qDebug("dropped %s", qUtf8Printable(file));
136  startAsyncOp();
138  }
139 }
140 
141 // Decode if we accept drag 'n drop or not
142 void PluginSelectDialog::dragEnterEvent(QDragEnterEvent *event)
143 {
144  for (const QString &mime : asConst(event->mimeData()->formats()))
145  {
146  qDebug("mimeData: %s", qUtf8Printable(mime));
147  }
148 
149  if (event->mimeData()->hasFormat(QLatin1String("text/plain")) || event->mimeData()->hasFormat(QLatin1String("text/uri-list")))
150  {
151  event->acceptProposedAction();
152  }
153 }
154 
156 {
157  startAsyncOp();
159 }
160 
161 void PluginSelectDialog::togglePluginState(QTreeWidgetItem *item, int)
162 {
163  PluginInfo *plugin = m_pluginManager->pluginInfo(item->text(PLUGIN_ID));
164  m_pluginManager->enablePlugin(plugin->name, !plugin->enabled);
165  if (plugin->enabled)
166  {
167  item->setText(PLUGIN_STATE, tr("Yes"));
168  setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "green");
169  }
170  else
171  {
172  item->setText(PLUGIN_STATE, tr("No"));
173  setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "red");
174  }
175 }
176 
178 {
179  // Enable/disable pause/start action given the DL state
180  const QList<QTreeWidgetItem *> items = m_ui->pluginsTree->selectedItems();
181  if (items.isEmpty()) return;
182 
183  QMenu *myContextMenu = new QMenu(this);
184  myContextMenu->setAttribute(Qt::WA_DeleteOnClose);
185 
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);
191 
192  myContextMenu->popup(QCursor::pos());
193 }
194 
196 {
197  close();
198 }
199 
201 {
202  bool error = false;
203  for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
204  {
205  int index = m_ui->pluginsTree->indexOfTopLevelItem(item);
206  Q_ASSERT(index != -1);
207  QString id = item->text(PLUGIN_ID);
209  {
210  delete item;
211  }
212  else
213  {
214  error = true;
215  // Disable it instead
216  m_pluginManager->enablePlugin(id, false);
217  item->setText(PLUGIN_STATE, tr("No"));
218  setRowColor(index, "red");
219  }
220  }
221 
222  if (error)
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."));
224  else
225  QMessageBox::information(this, tr("Uninstall success"), tr("All selected plugins were uninstalled successfully"));
226 }
227 
229 {
230  for (QTreeWidgetItem *item : asConst(m_ui->pluginsTree->selectedItems()))
231  {
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);
236  if (enable)
237  {
238  item->setText(PLUGIN_STATE, tr("Yes"));
239  setRowColor(index, "green");
240  }
241  else
242  {
243  item->setText(PLUGIN_STATE, tr("No"));
244  setRowColor(index, "red");
245  }
246  }
247 }
248 
249 // Set the color of a row in data model
250 void PluginSelectDialog::setRowColor(const int row, const QString &color)
251 {
252  QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(row);
253  for (int i = 0; i < m_ui->pluginsTree->columnCount(); ++i)
254  {
255  item->setData(i, Qt::ForegroundRole, QColor(color));
256  }
257 }
258 
259 QVector<QTreeWidgetItem*> PluginSelectDialog::findItemsWithUrl(const QString &url)
260 {
261  QVector<QTreeWidgetItem*> res;
262  res.reserve(m_ui->pluginsTree->topLevelItemCount());
263 
264  for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
265  {
266  QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
267  if (url.startsWith(item->text(PLUGIN_URL), Qt::CaseInsensitive))
268  res << item;
269  }
270 
271  return res;
272 }
273 
274 QTreeWidgetItem *PluginSelectDialog::findItemWithID(const QString &id)
275 {
276  for (int i = 0; i < m_ui->pluginsTree->topLevelItemCount(); ++i)
277  {
278  QTreeWidgetItem *item = m_ui->pluginsTree->topLevelItem(i);
279  if (id == item->text(PLUGIN_ID))
280  return item;
281  }
282 
283  return nullptr;
284 }
285 
287 {
288  // Some clean up first
289  m_ui->pluginsTree->clear();
290  for (const QString &name : asConst(m_pluginManager->allPlugins()))
291  addNewPlugin(name);
292 }
293 
294 void PluginSelectDialog::addNewPlugin(const QString &pluginName)
295 {
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);
301  if (plugin->enabled)
302  {
303  item->setText(PLUGIN_STATE, tr("Yes"));
304  setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "green");
305  }
306  else
307  {
308  item->setText(PLUGIN_STATE, tr("No"));
309  setRowColor(m_ui->pluginsTree->indexOfTopLevelItem(item), "red");
310  }
311  // Handle icon
312  if (QFile::exists(plugin->iconPath))
313  {
314  // Good, we already have the icon
315  item->setData(PLUGIN_NAME, Qt::DecorationRole, QIcon(plugin->iconPath));
316  }
317  else
318  {
319  // Icon is missing, we must download it
320  using namespace Net;
321  DownloadManager::instance()->download(
322  DownloadRequest(plugin->url + "/favicon.ico").saveToFile(true)
324  }
325  item->setText(PLUGIN_VERSION, plugin->version);
326 }
327 
329 {
330  ++m_asyncOps;
331  if (m_asyncOps == 1)
332  setCursor(QCursor(Qt::WaitCursor));
333 }
334 
336 {
337  --m_asyncOps;
338  if (m_asyncOps == 0)
339  setCursor(QCursor(Qt::ArrowCursor));
340 }
341 
343 {
345  if ((m_pendingUpdates == 0) && !m_updatedPlugins.isEmpty())
346  {
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();
350  }
351 }
352 
354 {
355  auto *dlg = new PluginSourceDialog(this);
358 }
359 
361 {
362  bool ok = false;
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
370  );
371 
372  while (ok && !url.isEmpty() && !url.endsWith(".py"))
373  {
374  QMessageBox::warning(this, tr("Invalid link"), tr("The link doesn't seem to point to a search engine plugin."));
376  this, tr("New search engine plugin URL"),
377  tr("URL:"), QLineEdit::Normal, url, &ok
378  );
379  }
380 
381  if (ok && !url.isEmpty())
382  {
383  startAsyncOp();
385  }
386 }
387 
389 {
390  const QStringList pathsList = QFileDialog::getOpenFileNames(
391  nullptr, tr("Select search plugins"), QDir::homePath(),
392  tr("qBittorrent search plugin") + QLatin1String(" (*.py)")
393  );
394  for (const QString &path : pathsList)
395  {
396  startAsyncOp();
398  }
399 }
400 
402 {
403  if (result.status != Net::DownloadStatus::Success)
404  {
405  qDebug("Could not download favicon: %s, reason: %s", qUtf8Printable(result.url), qUtf8Printable(result.errorString));
406  return;
407  }
408 
409  const QString filePath = Utils::Fs::toUniformPath(result.filePath);
410 
411  // Icon downloaded
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());
416  if (!invalid)
417  {
418  for (QTreeWidgetItem *item : asConst(findItemsWithUrl(result.url)))
419  {
420  QString id = item->text(PLUGIN_ID);
421  PluginInfo *plugin = m_pluginManager->pluginInfo(id);
422  if (!plugin) continue;
423 
424  QString iconPath = QString("%1/%2.%3")
426  , id
427  , result.url.endsWith(".ico", Qt::CaseInsensitive) ? "ico" : "png");
428  if (QFile::copy(filePath, iconPath))
429  {
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());
436  if (invalidExt)
437  {
438  Utils::Fs::forceRemove(iconPath);
439  continue;
440  }
441 
442  item->setData(PLUGIN_NAME, Qt::DecorationRole, iconWithExt);
444  }
445  }
446  }
447  // Delete tmp file
448  Utils::Fs::forceRemove(filePath);
449 }
450 
451 void PluginSelectDialog::checkForUpdatesFinished(const QHash<QString, PluginVersion> &updateInfo)
452 {
453  finishAsyncOp();
454  if (updateInfo.isEmpty())
455  {
456  QMessageBox::information(this, tr("Search plugin update"), tr("All your plugins are already up to date."));
457  return;
458  }
459 
460  for (auto i = updateInfo.cbegin(); i != updateInfo.cend(); ++i)
461  {
462  startAsyncOp();
464  m_pluginManager->updatePlugin(i.key());
465  }
466 }
467 
468 void PluginSelectDialog::checkForUpdatesFailed(const QString &reason)
469 {
470  finishAsyncOp();
471  QMessageBox::warning(this, tr("Search plugin update"), tr("Sorry, couldn't check for plugin updates. %1").arg(reason));
472 }
473 
474 void PluginSelectDialog::pluginInstalled(const QString &name)
475 {
476  addNewPlugin(name);
477  finishAsyncOp();
478  m_updatedPlugins.append(name);
480 }
481 
482 void PluginSelectDialog::pluginInstallationFailed(const QString &name, const QString &reason)
483 {
484  finishAsyncOp();
485  QMessageBox::information(this, tr("Search plugin install")
486  , tr("Couldn't install \"%1\" search engine plugin. %2").arg(name, reason));
488 }
489 
490 void PluginSelectDialog::pluginUpdated(const QString &name)
491 {
492  finishAsyncOp();
494  QTreeWidgetItem *item = findItemWithID(name);
495  item->setText(PLUGIN_VERSION, version);
496  m_updatedPlugins.append(name);
498 }
499 
500 void PluginSelectDialog::pluginUpdateFailed(const QString &name, const QString &reason)
501 {
502  finishAsyncOp();
503  QMessageBox::information(this, tr("Search plugin update")
504  , tr("Couldn't update \"%1\" search engine plugin. %2").arg(name, reason));
506 }
static QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode=QLineEdit::Normal, const QString &text={}, bool *ok=nullptr, bool excludeExtension=false, Qt::InputMethodHints inputMethodHints=Qt::ImhNone)
static bool hasSupportedScheme(const QString &url)
Ui::PluginSelectDialog * m_ui
PluginSelectDialog(SearchPluginManager *pluginManager, QWidget *parent=nullptr)
void togglePluginState(QTreeWidgetItem *, int)
void setRowColor(int row, const QString &color)
void iconDownloadFinished(const Net::DownloadResult &result)
void checkForUpdatesFinished(const QHash< QString, PluginVersion > &updateInfo)
void pluginInstallationFailed(const QString &name, const QString &reason)
void displayContextMenu(const QPoint &)
void dragEnterEvent(QDragEnterEvent *event) override
void pluginUpdateFailed(const QString &name, const QString &reason)
QStringList m_updatedPlugins
QVector< QTreeWidgetItem * > findItemsWithUrl(const QString &url)
SearchPluginManager * m_pluginManager
SettingValue< QSize > m_storeDialogSize
void enableSelection(bool enable)
void addNewPlugin(const QString &pluginName)
void pluginInstalled(const QString &name)
QTreeWidgetItem * findItemWithID(const QString &id)
void checkForUpdatesFailed(const QString &reason)
void dropEvent(QDropEvent *event) override
void pluginUpdated(const QString &name)
void updatePlugin(const QString &name)
PluginInfo * pluginInfo(const QString &name) const
void enablePlugin(const QString &name, bool enabled=true)
void pluginUpdated(const QString &name)
void pluginInstallationFailed(const QString &name, const QString &reason)
static void updateIconPath(PluginInfo *const plugin)
static QString pluginsLocation()
void pluginInstalled(const QString &name)
QStringList allPlugins() const
void installPlugin(const QString &source)
void checkForUpdatesFinished(const QHash< QString, PluginVersion > &updateInfo)
void checkForUpdatesFailed(const QString &reason)
bool uninstallPlugin(const QString &name)
void pluginUpdateFailed(const QString &name, const QString &reason)
static UIThemeManager * instance()
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
flag icons free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to copy
flag icons free of to any person obtaining a copy of this software and associated documentation files(the "Software")
Definition: session.h:86
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
bool forceRemove(const QString &filePath)
Definition: fs.cpp:173
void resize(QWidget *widget, const QSize &newSize={})
Definition: utils.cpp:54
@ PLUGIN_URL
@ PLUGIN_ID
@ PLUGIN_VERSION
@ PLUGIN_NAME
@ PLUGIN_STATE
#define SETTINGS_KEY(name)
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5
DownloadStatus status
PluginVersion version