1 /*
2     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
3 
4     SPDX-FileCopyrightText: 2010 Eike Hein <hein@kde.org>
5 */
6 
7 #include "urlcatcher.h"
8 #include "application.h"
9 
10 #include <QClipboard>
11 #include <QTreeView>
12 #include <QLineEdit>
13 
14 #include <KBookmarkDialog>
15 #include <KBookmarkManager>
16 #include <QFileDialog>
17 #include <KFilterProxySearchLine>
18 #include <KIO/CopyJob>
19 #include <QIcon>
20 #include <KLocalizedString>
21 #include <QMenu>
22 #include <KMessageBox>
23 #include <KToolBar>
24 
UrlDateItem(const QDateTime & dateTime)25 UrlDateItem::UrlDateItem(const QDateTime& dateTime)
26 {
27     setData(dateTime);
28 }
29 
~UrlDateItem()30 UrlDateItem::~UrlDateItem()
31 {
32 }
33 
data(int role) const34 QVariant UrlDateItem::data(int role) const
35 {
36     if (role == Qt::DisplayRole)
37         return QLocale().toString(QStandardItem::data().toDateTime(), QLocale::ShortFormat);
38 
39     return QStandardItem::data(role);
40 }
41 
UrlSortFilterProxyModel(QObject * parent)42 UrlSortFilterProxyModel::UrlSortFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent)
43 {
44 }
45 
~UrlSortFilterProxyModel()46 UrlSortFilterProxyModel::~UrlSortFilterProxyModel()
47 {
48 }
49 
flags(const QModelIndex & index) const50 Qt::ItemFlags UrlSortFilterProxyModel::flags(const QModelIndex& index) const
51 {
52     return QSortFilterProxyModel::flags(index) & ~Qt::ItemIsEditable;
53 }
54 
lessThan(const QModelIndex & left,const QModelIndex & right) const55 bool UrlSortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
56 {
57     if (sortColumn() == 2)
58     {
59         QVariant leftData = sourceModel()->data(left, Qt::UserRole + 1);
60         QVariant rightData = sourceModel()->data(right, Qt::UserRole + 1);
61 
62         return leftData.toDateTime() < rightData.toDateTime();
63     }
64 
65     return QSortFilterProxyModel::lessThan(left, right);
66 }
67 
UrlCatcher(QWidget * parent)68 UrlCatcher::UrlCatcher(QWidget* parent) : ChatWindow(parent)
69 {
70     setName(i18n("URL Catcher"));
71     setType(ChatWindow::UrlCatcher);
72 
73     setSpacing(0);
74 
75     setupActions();
76     setupUrlTree();
77 }
78 
~UrlCatcher()79 UrlCatcher::~UrlCatcher()
80 {
81     Preferences::saveColumnState(m_urlTree, QStringLiteral("UrlCatcher ViewSettings"));
82 }
83 
setupActions()84 void UrlCatcher::setupActions()
85 {
86     m_toolBar = new KToolBar(this, true, true);
87     m_contextMenu = new QMenu(this);
88 
89     QAction* action;
90 
91     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("open url", "&Open"), this, &UrlCatcher::openSelectedUrls);
92     m_itemActions.append(action);
93     m_contextMenu->addAction(action);
94     action->setStatusTip(i18n("Open URLs in external browser."));
95     action->setWhatsThis(i18n("<p>Select one or several <b>URLs</b> below, then click this button to launch the application associated with the mimetype of the URL.</p>-<p>In the <b>Settings</b>, under <b>Behavior</b> | <b>General</b>, you can specify a custom web browser for web URLs.</p>"));
96     action->setEnabled(false);
97 
98     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("&Save..."), this, &UrlCatcher::saveSelectedUrls);
99     m_itemActions.append(action);
100     m_contextMenu->addAction(action);
101     action->setStatusTip(i18n("Save selected URLs to the disk."));
102     action->setEnabled(false);
103 
104     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add Bookmark..."), this, SLOT (bookmarkSelectedUrls()));
105     m_itemActions.append(action);
106     m_contextMenu->addAction(action);
107     action->setEnabled(false);
108 
109     m_toolBar->addSeparator();
110     m_contextMenu->addSeparator();
111 
112     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("copy url","&Copy"), this, &UrlCatcher::copySelectedUrls);
113     m_itemActions.append(action);
114     m_contextMenu->addAction(action);
115     action->setStatusTip(i18n("Copy URLs to the clipboard."));
116     action->setWhatsThis(i18n("Select one or several <b>URLs</b> above, then click this button to copy them to the clipboard."));
117     action->setEnabled(false);
118 
119     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("delete url","&Delete"), this, &UrlCatcher::deleteSelectedUrls);
120     m_itemActions.append(action);
121     m_contextMenu->addAction(action);
122     action->setWhatsThis(i18n("Select one or several <b>URLs</b> above, then click this button to delete them from the list."));
123     action->setStatusTip(i18n("Delete selected link."));
124     action->setEnabled(false);
125 
126     m_toolBar->addSeparator();
127     m_contextMenu->addSeparator();
128 
129     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("save url list", "&Save List..."), this, &UrlCatcher::saveUrlModel);
130     m_listActions.append(action);
131     action->setStatusTip(i18n("Save list."));
132     action->setWhatsThis(i18n("Click to save the entire list to a file."));
133     action->setEnabled(false);
134 
135     action = m_toolBar->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18nc("clear url list","&Clear List"), this, &UrlCatcher::clearUrlModel);
136     m_listActions.append(action);
137     action->setStatusTip(i18n("Clear list."));
138     action->setWhatsThis(i18n("Click to erase the entire list."));
139     action->setEnabled(false);
140 
141     updateListActionStates();
142 }
143 
setupUrlTree()144 void UrlCatcher::setupUrlTree()
145 {
146     m_searchLine = new QLineEdit(this);
147     m_searchLine->setClearButtonEnabled(true);
148     m_searchLine->setPlaceholderText(i18n("Search"));
149 
150     m_filterTimer = new QTimer(this);
151     m_filterTimer->setSingleShot(true);
152     connect(m_filterTimer, &QTimer::timeout,
153             this, &UrlCatcher::updateFilter);
154 
155     m_urlTree = new QTreeView(this);
156     m_urlTree->setWhatsThis(i18n("List of Uniform Resource Locators mentioned in any of the Konversation windows during this session."));
157     m_urlTree->setContextMenuPolicy(Qt::CustomContextMenu);
158     m_urlTree->setSortingEnabled(true);
159     m_urlTree->header()->setSectionsMovable(false);
160     m_urlTree->header()->setSortIndicatorShown(true);
161     m_urlTree->setAllColumnsShowFocus(true);
162     m_urlTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
163     m_urlTree->setRootIsDecorated(false);
164     connect(m_urlTree, &QTreeView::customContextMenuRequested, this, &UrlCatcher::openContextMenu);
165     connect(m_urlTree, &QTreeView::doubleClicked, this, &UrlCatcher::openUrl);
166 
167     Application* konvApp = Application::instance();
168     QStandardItemModel* urlModel = konvApp->getUrlModel();
169     auto* item = new QStandardItem(i18n("From"));
170     urlModel->setHorizontalHeaderItem(0, item);
171     item = new QStandardItem(i18n("URL"));
172     urlModel->setHorizontalHeaderItem(1, item);
173     item = new QStandardItem(i18n("Date"));
174     urlModel->setHorizontalHeaderItem(2, item);
175     connect(urlModel, &QStandardItemModel::rowsInserted, this, &UrlCatcher::updateListActionStates);
176     connect(urlModel, &QStandardItemModel::rowsRemoved, this, &UrlCatcher::updateListActionStates);
177 
178     auto* proxyModel = new UrlSortFilterProxyModel(this);
179     proxyModel->setSourceModel(urlModel);
180     proxyModel->setDynamicSortFilter(true);
181     proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
182     proxyModel->setFilterKeyColumn(-1);
183 
184     m_urlTree->setModel(proxyModel);
185     connect(m_urlTree->selectionModel(), &QItemSelectionModel::selectionChanged,
186         this, &UrlCatcher::updateItemActionStates);
187 
188     connect(m_searchLine, &QLineEdit::textChanged,
189             this, &UrlCatcher::startFilterTimer);
190 
191     Preferences::restoreColumnState(m_urlTree, QStringLiteral("UrlCatcher ViewSettings"), 2, Qt::DescendingOrder);
192 }
193 
updateItemActionStates()194 void UrlCatcher::updateItemActionStates()
195 {
196     bool enable = m_urlTree->selectionModel()->hasSelection();
197 
198     for (QAction* action : qAsConst(m_itemActions))
199         action->setEnabled(enable);
200 }
201 
updateListActionStates()202 void UrlCatcher::updateListActionStates()
203 {
204     Application* konvApp = Application::instance();
205     bool enable = konvApp->getUrlModel()->rowCount();
206 
207     for (QAction* action : qAsConst(m_listActions))
208         action->setEnabled(enable);
209 }
210 
openContextMenu(const QPoint & p)211 void UrlCatcher::openContextMenu(const QPoint& p)
212 {
213     QModelIndex index = m_urlTree->indexAt(p);
214     if (!index.isValid()) return;
215 
216     m_contextMenu->exec(QCursor::pos());
217 }
218 
openUrl(const QModelIndex & index)219 void UrlCatcher::openUrl(const QModelIndex& index)
220 {
221     Application::openUrl(index.sibling(index.row(), 1).data().toString());
222 }
223 
openSelectedUrls()224 void UrlCatcher::openSelectedUrls()
225 {
226     const QModelIndexList selectedIndexes = m_urlTree->selectionModel()->selectedRows(1);
227 
228     if (selectedIndexes.count() > 1)
229     {
230         int ret = KMessageBox::warningContinueCancel(this,
231             i18n("You have selected more than one URL. Do you really want to open several URLs at once?"),
232             i18n("Open URLs"),
233             KStandardGuiItem::cont(),
234             KStandardGuiItem::cancel(),
235             QString(),
236             KMessageBox::Notify | KMessageBox::Dangerous);
237 
238         if (ret != KMessageBox::Continue) return;
239     }
240 
241     for (const QModelIndex& index : selectedIndexes)
242         if (index.isValid()) Application::openUrl(index.data().toString());
243 }
244 
saveSelectedUrls()245 void UrlCatcher::saveSelectedUrls()
246 {
247     const QModelIndexList selectedIndexes = m_urlTree->selectionModel()->selectedRows(1);
248 
249     if (selectedIndexes.count() > 1)
250     {
251         int ret = KMessageBox::warningContinueCancel(this,
252             i18n("You have selected more than one URL. A file dialog to set the destination will open for each. "
253                 "Do you really want to save several URLs at once?"),
254             i18n("Open URLs"),
255             KStandardGuiItem::cont(),
256             KStandardGuiItem::cancel(),
257             QString(),
258             KMessageBox::Notify | KMessageBox::Dangerous);
259 
260         if (ret != KMessageBox::Continue) return;
261     }
262 
263     for (const QModelIndex& index : selectedIndexes) {
264         if (index.isValid())
265         {
266             QUrl url(index.data().toString());
267             QUrl targetUrl = QFileDialog::getSaveFileUrl(this, i18n("Save link as"), QUrl::fromLocalFile(url.fileName()));
268 
269             if (targetUrl.isEmpty() || !targetUrl.isValid())
270                 continue;
271 
272             KIO::copy(url, targetUrl);
273         }
274     }
275 }
276 
bookmarkSelectedUrls()277 void UrlCatcher::bookmarkSelectedUrls()
278 {
279     const QModelIndexList selectedIndexes = m_urlTree->selectionModel()->selectedRows(1);
280 
281     KBookmarkManager* manager = KBookmarkManager::userBookmarksManager();
282     auto* dialog = new KBookmarkDialog(manager, this);
283 
284     if (selectedIndexes.count() > 1)
285     {
286         QList<KBookmarkOwner::FutureBookmark> bookmarks;
287 
288         bookmarks.reserve(selectedIndexes.size());
289         for (const QModelIndex& index : selectedIndexes)
290             bookmarks << KBookmarkOwner::FutureBookmark(index.data().toString(), QUrl(index.data().toString()), QString());
291 
292         dialog->addBookmarks(bookmarks, i18n("New"));
293     }
294     else
295     {
296         QString url = selectedIndexes.first().data().toString();
297 
298         dialog->addBookmark(url, QUrl(url), QString());
299     }
300 
301     delete dialog;
302 }
303 
copySelectedUrls()304 void UrlCatcher::copySelectedUrls()
305 {
306     const QModelIndexList selectedIndexes = m_urlTree->selectionModel()->selectedRows(1);
307 
308     QStringList urls;
309 
310     for (const QModelIndex& index : selectedIndexes)
311         if (index.isValid()) urls << index.data().toString();
312 
313     QClipboard* clipboard = qApp->clipboard();
314     clipboard->setText(urls.join(QLatin1Char('\n')), QClipboard::Clipboard);
315 }
316 
deleteSelectedUrls()317 void UrlCatcher::deleteSelectedUrls()
318 {
319     QList<QPersistentModelIndex> selectedIndices;
320 
321     const auto nonPersistentSelectedIndices = m_urlTree->selectionModel()->selectedIndexes();
322     selectedIndices.reserve(nonPersistentSelectedIndices.size());
323     for (const QModelIndex& index : nonPersistentSelectedIndices)
324         selectedIndices << index;
325 
326     Application* konvApp = Application::instance();
327 
328     for (const QPersistentModelIndex& index : qAsConst(selectedIndices))
329         if (index.isValid()) konvApp->getUrlModel()->removeRow(index.row());
330 }
331 
saveUrlModel()332 void UrlCatcher::saveUrlModel()
333 {
334     QString target = QFileDialog::getSaveFileName(this,
335         i18n("Save URL List"));
336 
337     if (!target.isEmpty())
338     {
339         Application* konvApp = Application::instance();
340         QStandardItemModel* urlModel = konvApp->getUrlModel();
341 
342         int nickColumnWidth = 0;
343 
344         QModelIndex index = urlModel->index(0, 0, QModelIndex());
345         int rows = urlModel->rowCount();
346 
347         for (int r = 0; r < rows; r++)
348         {
349             int length = index.sibling(r, 0).data().toString().length();
350 
351             if (length > nickColumnWidth)
352                 nickColumnWidth = length;
353         }
354 
355         ++nickColumnWidth;
356 
357         QFile file(target);
358         file.open(QIODevice::WriteOnly);
359 
360         QTextStream stream(&file);
361 
362         stream << i18n("Konversation URL List: %1\n\n",
363             QLocale().toString(QDateTime::currentDateTime(), QLocale::LongFormat));
364 
365         for (int r = 0; r < rows; r++)
366         {
367             QString line = index.sibling(r, 0).data().toString().leftJustified(nickColumnWidth, QLatin1Char(' '));
368             line.append(index.sibling(r, 1).data().toString());
369             line.append(QLatin1Char('\n'));
370 
371             stream << line;
372         }
373 
374         file.close();
375     }
376 }
377 
clearUrlModel()378 void UrlCatcher::clearUrlModel()
379 {
380     Application* konvApp = Application::instance();
381     QStandardItemModel* urlModel = konvApp->getUrlModel();
382 
383     urlModel->removeRows(0, urlModel->rowCount());
384 }
385 
childAdjustFocus()386 void UrlCatcher::childAdjustFocus()
387 {
388     m_urlTree->setFocus();
389 }
390 
event(QEvent * event)391 bool UrlCatcher::event(QEvent* event)
392 {
393     if (event->type() == QEvent::LocaleChange) {
394         Application* konvApp = Application::instance();
395         QStandardItemModel* urlModel = konvApp->getUrlModel();
396 
397         m_urlTree->dataChanged(urlModel->index(0, 0), urlModel->index(urlModel->rowCount() - 1, 2));
398     }
399 
400     return ChatWindow::event(event);
401 }
402 
updateFilter()403 void UrlCatcher::updateFilter()
404 {
405     auto* proxy = qobject_cast<QSortFilterProxyModel*>(m_urlTree->model());
406 
407     if(!proxy)
408         return;
409 
410     proxy->setFilterFixedString(m_searchLine->text());
411 }
412 
startFilterTimer(const QString & filter)413 void UrlCatcher::startFilterTimer(const QString &filter)
414 {
415     Q_UNUSED(filter)
416     m_filterTimer->start(300);
417 }
418