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