1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2016 - 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 * Copyright (C) 2016 Piotr Wójcik <chocimier@tlen.pl>
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (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, see <http://www.gnu.org/licenses/>.
18 *
19 **************************************************************************/
20
21 #include "AddonsContentsWidget.h"
22 #include "../../../core/SessionsManager.h"
23 #include "../../../core/JsonSettings.h"
24 #include "../../../core/ThemesManager.h"
25 #include "../../../core/UserScript.h"
26 #include "../../../core/Utils.h"
27
28 #include "ui_AddonsContentsWidget.h"
29
30 #include <QtCore/QFile>
31 #include <QtCore/QJsonObject>
32 #include <QtCore/QStandardPaths>
33 #include <QtCore/QTimer>
34 #include <QtGui/QKeyEvent>
35 #include <QtWidgets/QCheckBox>
36 #include <QtWidgets/QFileDialog>
37 #include <QtWidgets/QMenu>
38 #include <QtWidgets/QMessageBox>
39
40 namespace Otter
41 {
42
AddonsContentsWidget(const QVariantMap & parameters,Window * window,QWidget * parent)43 AddonsContentsWidget::AddonsContentsWidget(const QVariantMap ¶meters, Window *window, QWidget *parent) : ContentsWidget(parameters, window, parent),
44 m_model(new QStandardItemModel(this)),
45 m_isLoading(true),
46 m_ui(new Ui::AddonsContentsWidget)
47 {
48 m_ui->setupUi(this);
49 m_ui->filterLineEditWidget->setClearOnEscape(true);
50 m_ui->addonsViewWidget->setViewMode(ItemViewWidget::TreeView);
51 m_ui->addonsViewWidget->installEventFilter(this);
52
53 QTimer::singleShot(100, this, &AddonsContentsWidget::populateAddons);
54
55 connect(m_ui->filterLineEditWidget, &LineEditWidget::textChanged, m_ui->addonsViewWidget, &ItemViewWidget::setFilterString);
56 connect(m_ui->addonsViewWidget, &ItemViewWidget::customContextMenuRequested, this, &AddonsContentsWidget::showContextMenu);
57 connect(m_ui->addonsViewWidget, &ItemViewWidget::clicked, this, &AddonsContentsWidget::save);
58 }
59
~AddonsContentsWidget()60 AddonsContentsWidget::~AddonsContentsWidget()
61 {
62 delete m_ui;
63 }
64
changeEvent(QEvent * event)65 void AddonsContentsWidget::changeEvent(QEvent *event)
66 {
67 ContentsWidget::changeEvent(event);
68
69 if (event->type() == QEvent::LanguageChange)
70 {
71 m_ui->retranslateUi(this);
72
73 for (int i = 0; i < m_model->rowCount(); ++i)
74 {
75 const QModelIndex index(m_model->index(i, 0));
76
77 if (static_cast<Addon::AddonType>(index.data(TypeRole).toInt()) == Addon::UserScriptType)
78 {
79 m_model->setData(index, tr("User Scripts"), Qt::DisplayRole);
80 }
81 }
82 }
83 }
84
getSelectedAddons() const85 QVector<Addon*> AddonsContentsWidget::getSelectedAddons() const
86 {
87 const QModelIndexList indexes(m_ui->addonsViewWidget->selectionModel()->selectedIndexes());
88 QVector<Addon*> addons;
89 addons.reserve(indexes.count());
90
91 for (int i = 0; i < indexes.count(); ++i)
92 {
93 if (indexes.at(i).isValid() && indexes.at(i).parent() != m_model->invisibleRootItem()->index())
94 {
95 Addon::AddonType type(static_cast<Addon::AddonType>(indexes.at(i).parent().data(TypeRole).toInt()));
96
97 if (type == Addon::UserScriptType)
98 {
99 UserScript *script(AddonsManager::getUserScript(indexes.at(i).data(NameRole).toString()));
100
101 if (script)
102 {
103 addons.append(script);
104 }
105 }
106 }
107 }
108
109 addons.squeeze();
110
111 return addons;
112 }
113
populateAddons()114 void AddonsContentsWidget::populateAddons()
115 {
116 m_types.clear();
117 m_types[Addon::UserScriptType] = 0;
118
119 QStandardItem *userScriptsItem(new QStandardItem(ThemesManager::createIcon(QLatin1String("addon-user-script"), false), tr("User Scripts")));
120 userScriptsItem->setData(Addon::UserScriptType, TypeRole);
121
122 m_model->appendRow(userScriptsItem);
123
124 const QStringList userScripts(AddonsManager::getUserScripts());
125
126 for (int i = 0; i < userScripts.count(); ++i)
127 {
128 addAddon(AddonsManager::getUserScript(userScripts.at(i)));
129 }
130
131 m_ui->addonsViewWidget->setModel(m_model);
132 m_ui->addonsViewWidget->expandAll();
133
134 m_isLoading = false;
135
136 emit loadingStateChanged(WebWidget::FinishedLoadingState);
137
138 connect(AddonsManager::getInstance(), &AddonsManager::userScriptModified, this, &AddonsContentsWidget::updateAddon);
139 connect(m_ui->addonsViewWidget->selectionModel(), &QItemSelectionModel::selectionChanged, [&]()
140 {
141 emit arbitraryActionsStateChanged({ActionsManager::DeleteAction});
142 });
143 }
144
addAddon()145 void AddonsContentsWidget::addAddon()
146 {
147 const QStringList sourcePaths(QFileDialog::getOpenFileNames(this, tr("Select Files"), QStandardPaths::standardLocations(QStandardPaths::HomeLocation).value(0), Utils::formatFileTypes({tr("User Script files (*.js)")})));
148
149 if (sourcePaths.isEmpty())
150 {
151 return;
152 }
153
154 QStringList failedPaths;
155 ReplaceMode replaceMode(UnknownMode);
156
157 for (int i = 0; i < sourcePaths.count(); ++i)
158 {
159 if (sourcePaths.at(i).isEmpty())
160 {
161 continue;
162 }
163
164 const QString scriptName(QFileInfo(sourcePaths.at(i)).completeBaseName());
165 const QString targetDirectory(QDir(SessionsManager::getWritableDataPath(QLatin1String("scripts"))).filePath(scriptName));
166 const QString targetPath(QDir(targetDirectory).filePath(QFileInfo(sourcePaths.at(i)).fileName()));
167 bool replaceScript(false);
168
169 if (QFile::exists(targetPath))
170 {
171 if (replaceMode == IgnoreAllMode)
172 {
173 continue;
174 }
175
176 if (replaceMode == ReplaceAllMode)
177 {
178 replaceScript = true;
179 }
180 else
181 {
182 QMessageBox messageBox;
183 messageBox.setWindowTitle(tr("Question"));
184 messageBox.setText(tr("User Script with this name already exists:\n%1").arg(targetPath));
185 messageBox.setInformativeText(tr("Do you want to replace it?"));
186 messageBox.setIcon(QMessageBox::Question);
187 messageBox.addButton(QMessageBox::Yes);
188 messageBox.addButton(QMessageBox::No);
189
190 if (i < (sourcePaths.count() - 1))
191 {
192 messageBox.setCheckBox(new QCheckBox(tr("Apply to all")));
193 }
194
195 messageBox.exec();
196
197 replaceScript = (messageBox.standardButton(messageBox.clickedButton()) == QMessageBox::Yes);
198
199 if (messageBox.checkBox() && messageBox.checkBox()->isChecked())
200 {
201 replaceMode = (replaceScript ? ReplaceAllMode : IgnoreAllMode);
202 }
203 }
204
205 if (!replaceScript)
206 {
207 continue;
208 }
209 }
210
211 if ((replaceScript && !QDir().remove(targetPath)) || (!replaceScript && !QDir().mkpath(targetDirectory)) || !QFile::copy(sourcePaths.at(i), targetPath))
212 {
213 failedPaths.append(sourcePaths.at(i));
214
215 continue;
216 }
217
218 if (replaceScript)
219 {
220 UserScript *script(AddonsManager::getUserScript(scriptName));
221
222 if (script)
223 {
224 script->reload();
225 }
226 }
227 else
228 {
229 UserScript script(targetPath);
230
231 addAddon(&script);
232 }
233 }
234
235 if (!failedPaths.isEmpty())
236 {
237 QMessageBox::critical(this, tr("Error"), tr("Failed to import following User Script file(s):\n%1", "", failedPaths.count()).arg(failedPaths.join(QLatin1Char('\n'))), QMessageBox::Close);
238 }
239
240 save();
241
242 AddonsManager::loadUserScripts();
243 }
244
addAddon(Addon * addon)245 void AddonsContentsWidget::addAddon(Addon *addon)
246 {
247 if (!addon)
248 {
249 return;
250 }
251
252 QStandardItem *typeItem(nullptr);
253
254 if (m_types.contains(addon->getType()))
255 {
256 typeItem = m_model->item(m_types[addon->getType()]);
257 }
258
259 if (!typeItem)
260 {
261 return;
262 }
263
264 QStandardItem *item(new QStandardItem(getAddonIcon(addon), (addon->getVersion().isEmpty() ? addon->getTitle() : QStringLiteral("%1 %2").arg(addon->getTitle()).arg(addon->getVersion()))));
265 item->setFlags(item->flags() | Qt::ItemNeverHasChildren);
266 item->setCheckable(true);
267 item->setCheckState(addon->isEnabled() ? Qt::Checked : Qt::Unchecked);
268 item->setToolTip(addon->getDescription());
269
270 if (addon->getType() == Addon::UserScriptType)
271 {
272 const UserScript *script(static_cast<UserScript*>(addon));
273
274 if (script)
275 {
276 item->setData(script->getName(), NameRole);
277 }
278 }
279
280 typeItem->appendRow(item);
281 }
282
updateAddon(const QString & name)283 void AddonsContentsWidget::updateAddon(const QString &name)
284 {
285 const QStandardItem *userScriptsItem(m_model->item(m_types.value(Addon::UserScriptType)));
286
287 if (!userScriptsItem)
288 {
289 return;
290 }
291
292 for (int i = 0; i < userScriptsItem->rowCount(); ++i)
293 {
294 const QModelIndex index(userScriptsItem->child(i)->index());
295
296 if (index.isValid() && index.data(NameRole).toString() == name)
297 {
298 UserScript *script(AddonsManager::getUserScript(name));
299
300 if (script)
301 {
302 m_ui->addonsViewWidget->setData(index, getAddonIcon(script), Qt::DecorationRole);
303 m_ui->addonsViewWidget->setData(index, (script->getVersion().isEmpty() ? script->getTitle() : QStringLiteral("%1 %2").arg(script->getTitle()).arg(script->getVersion())), Qt::DisplayRole);
304 }
305
306 break;
307 }
308 }
309 }
310
openAddon()311 void AddonsContentsWidget::openAddon()
312 {
313 const QVector<Addon*> addons(getSelectedAddons());
314
315 for (int i = 0; i < addons.count(); ++i)
316 {
317 if (addons.at(i)->getType() == Addon::UserScriptType)
318 {
319 const UserScript *script(static_cast<UserScript*>(addons.at(i)));
320
321 if (script)
322 {
323 Utils::runApplication({}, QUrl(script->getPath()));
324 }
325 }
326 }
327 }
328
reloadAddon()329 void AddonsContentsWidget::reloadAddon()
330 {
331 const QModelIndexList indexes(m_ui->addonsViewWidget->selectionModel()->selectedIndexes());
332
333 for (int i = 0; i < indexes.count(); ++i)
334 {
335 if (indexes.at(i).isValid() && indexes.at(i).parent() != m_model->invisibleRootItem()->index())
336 {
337 Addon::AddonType type(static_cast<Addon::AddonType>(indexes.at(i).parent().data(TypeRole).toInt()));
338
339 if (type == Addon::UserScriptType)
340 {
341 UserScript *script(AddonsManager::getUserScript(indexes.at(i).data(NameRole).toString()));
342
343 if (script)
344 {
345 script->reload();
346
347 m_ui->addonsViewWidget->setData(indexes.at(i), getAddonIcon(script), Qt::DecorationRole);
348 m_ui->addonsViewWidget->setData(indexes.at(i), (script->getVersion().isEmpty() ? script->getTitle() : QStringLiteral("%1 %2").arg(script->getTitle()).arg(script->getVersion())), Qt::DisplayRole);
349 }
350 }
351 }
352 }
353 }
354
removeAddons()355 void AddonsContentsWidget::removeAddons()
356 {
357 const QVector<Addon*> addons(getSelectedAddons());
358
359 if (addons.isEmpty())
360 {
361 return;
362 }
363
364 QMessageBox messageBox;
365 messageBox.setWindowTitle(tr("Question"));
366 messageBox.setText(tr("You are about to irreversibly remove %n addon(s).", "", addons.count()));
367 messageBox.setInformativeText(tr("Do you want to continue?"));
368 messageBox.setIcon(QMessageBox::Question);
369 messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
370 messageBox.setDefaultButton(QMessageBox::Yes);
371
372 if (messageBox.exec() == QMessageBox::Yes)
373 {
374 for (int i = 0; i < addons.count(); ++i)
375 {
376 if (addons.at(i)->canRemove())
377 {
378 addons.at(i)->remove();
379 }
380 }
381 }
382
383 AddonsManager::loadUserScripts();
384
385 save();
386 }
387
save()388 void AddonsContentsWidget::save()
389 {
390 const QStandardItem *userScriptsItem(m_model->item(m_types.value(Addon::UserScriptType)));
391
392 if (!userScriptsItem)
393 {
394 return;
395 }
396
397 QModelIndexList indexesToRemove;
398 QJsonObject settingsObject;
399
400 for (int i = 0; i < userScriptsItem->rowCount(); ++i)
401 {
402 const QModelIndex index(userScriptsItem->child(i)->index());
403 const QString name(index.data(NameRole).toString());
404
405 if (index.isValid())
406 {
407 if (!name.isEmpty() && AddonsManager::getUserScript(name))
408 {
409 settingsObject.insert(name, QJsonObject({{QLatin1String("isEnabled"), QJsonValue(index.data(Qt::CheckStateRole).toInt() == Qt::Checked)}}));
410 }
411 else
412 {
413 indexesToRemove.append(index);
414 }
415 }
416 }
417
418 JsonSettings settings;
419 settings.setObject(settingsObject);
420 settings.save(SessionsManager::getWritableDataPath(QLatin1String("scripts/scripts.json")));
421
422 for (int i = (indexesToRemove.count() - 1); i >= 0; --i)
423 {
424 m_ui->addonsViewWidget->model()->removeRow(indexesToRemove.at(i).row(), indexesToRemove.at(i).parent());
425 }
426 }
427
showContextMenu(const QPoint & position)428 void AddonsContentsWidget::showContextMenu(const QPoint &position)
429 {
430 const QVector<Addon*> addons(getSelectedAddons());
431 QMenu menu(this);
432 menu.addAction(tr("Add Addon…"), this, static_cast<void(AddonsContentsWidget::*)()>(&AddonsContentsWidget::addAddon));
433
434 if (!addons.isEmpty())
435 {
436 menu.addSeparator();
437 menu.addAction(tr("Open Addon File"), this, &AddonsContentsWidget::openAddon);
438 menu.addAction(tr("Reload Addon"), this, &AddonsContentsWidget::reloadAddon);
439 menu.addSeparator();
440 menu.addAction(tr("Remove Addon…"), this, &AddonsContentsWidget::removeAddons);
441 }
442
443 menu.exec(m_ui->addonsViewWidget->mapToGlobal(position));
444 }
445
print(QPrinter * printer)446 void AddonsContentsWidget::print(QPrinter *printer)
447 {
448 m_ui->addonsViewWidget->render(printer);
449 }
450
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)451 void AddonsContentsWidget::triggerAction(int identifier, const QVariantMap ¶meters, ActionsManager::TriggerType trigger)
452 {
453 switch (identifier)
454 {
455 case ActionsManager::SelectAllAction:
456 m_ui->addonsViewWidget->selectAll();
457
458 break;
459 case ActionsManager::DeleteAction:
460 removeAddons();
461
462 break;
463 case ActionsManager::FindAction:
464 case ActionsManager::QuickFindAction:
465 m_ui->filterLineEditWidget->setFocus();
466
467 break;
468 case ActionsManager::ActivateContentAction:
469 m_ui->addonsViewWidget->setFocus();
470
471 break;
472 default:
473 ContentsWidget::triggerAction(identifier, parameters, trigger);
474
475 break;
476 }
477 }
478
getTitle() const479 QString AddonsContentsWidget::getTitle() const
480 {
481 return tr("Addons");
482 }
483
getType() const484 QLatin1String AddonsContentsWidget::getType() const
485 {
486 return QLatin1String("addons");
487 }
488
getUrl() const489 QUrl AddonsContentsWidget::getUrl() const
490 {
491 return QUrl(QLatin1String("about:addons"));
492 }
493
getIcon() const494 QIcon AddonsContentsWidget::getIcon() const
495 {
496 return ThemesManager::createIcon(QLatin1String("preferences-plugin"), false);
497 }
498
getAddonIcon(Addon * addon) const499 QIcon AddonsContentsWidget::getAddonIcon(Addon *addon) const
500 {
501 return ((!addon || addon->getIcon().isNull()) ? ThemesManager::createIcon(QLatin1String("addon-user-script"), false) : addon->getIcon());
502 }
503
getActionState(int identifier,const QVariantMap & parameters) const504 ActionsManager::ActionDefinition::State AddonsContentsWidget::getActionState(int identifier, const QVariantMap ¶meters) const
505 {
506 ActionsManager::ActionDefinition::State state(ActionsManager::getActionDefinition(identifier).getDefaultState());
507
508 switch (identifier)
509 {
510 case ActionsManager::SelectAllAction:
511 state.isEnabled = true;
512
513 return state;
514 case ActionsManager::DeleteAction:
515 state.isEnabled = (m_ui->addonsViewWidget->selectionModel() && !m_ui->addonsViewWidget->selectionModel()->selectedIndexes().isEmpty());
516
517 return state;
518 default:
519 break;
520 }
521
522 return ContentsWidget::getActionState(identifier, parameters);
523 }
524
getLoadingState() const525 WebWidget::LoadingState AddonsContentsWidget::getLoadingState() const
526 {
527 return (m_isLoading ? WebWidget::OngoingLoadingState : WebWidget::FinishedLoadingState);
528 }
529
eventFilter(QObject * object,QEvent * event)530 bool AddonsContentsWidget::eventFilter(QObject *object, QEvent *event)
531 {
532 if (object == m_ui->addonsViewWidget && event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Delete)
533 {
534 removeAddons();
535
536 return true;
537 }
538
539 return ContentsWidget::eventFilter(object, event);
540 }
541
542 }
543