|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a widget showing the list of bookmarks. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import datetime |
|
12 import json |
|
13 import os |
|
14 |
|
15 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QUrl |
|
16 from PyQt6.QtGui import QClipboard, QGuiApplication |
|
17 from PyQt6.QtWidgets import ( |
|
18 QAbstractItemView, QApplication, QDialog, QListWidget, QListWidgetItem, |
|
19 QMenu |
|
20 ) |
|
21 |
|
22 from EricWidgets import EricFileDialog, EricMessageBox |
|
23 |
|
24 import Preferences |
|
25 |
|
26 from .HelpBookmarkPropertiesDialog import HelpBookmarkPropertiesDialog |
|
27 |
|
28 |
|
29 class HelpBookmarksWidget(QListWidget): |
|
30 """ |
|
31 Class implementing a widget showing the list of bookmarks. |
|
32 |
|
33 @signal escapePressed() emitted when the ESC key was pressed |
|
34 @signal openUrl(QUrl, str) emitted to open an entry in the current tab |
|
35 @signal newTab(QUrl, str) emitted to open an entry in a new tab |
|
36 @signal newBackgroundTab(QUrl, str) emitted to open an entry in a |
|
37 new background tab |
|
38 """ |
|
39 escapePressed = pyqtSignal() |
|
40 openUrl = pyqtSignal(QUrl) |
|
41 newTab = pyqtSignal(QUrl) |
|
42 newBackgroundTab = pyqtSignal(QUrl) |
|
43 |
|
44 UrlRole = Qt.ItemDataRole.UserRole + 1 |
|
45 |
|
46 def __init__(self, parent=None): |
|
47 """ |
|
48 Constructor |
|
49 |
|
50 @param parent reference to the parent widget (defaults to None) |
|
51 @type QWidget (optional) |
|
52 """ |
|
53 super().__init__(parent) |
|
54 self.setObjectName("HelpBookmarksWidget") |
|
55 |
|
56 self.__helpViewer = parent |
|
57 |
|
58 self.setAlternatingRowColors(True) |
|
59 self.setSelectionMode( |
|
60 QAbstractItemView.SelectionMode.ExtendedSelection) |
|
61 self.setSortingEnabled(True) |
|
62 |
|
63 self.setContextMenuPolicy( |
|
64 Qt.ContextMenuPolicy.CustomContextMenu) |
|
65 self.customContextMenuRequested.connect( |
|
66 self.__showContextMenu) |
|
67 |
|
68 self.__bookmarks = [] |
|
69 self.__loadBookmarks() |
|
70 |
|
71 self.itemDoubleClicked.connect(self.__bookmarkActivated) |
|
72 |
|
73 @pyqtSlot(QPoint) |
|
74 def __showContextMenu(self, point): |
|
75 """ |
|
76 Private slot to handle the customContextMenuRequested signal of |
|
77 the viewlist. |
|
78 |
|
79 @param point position to open the menu at |
|
80 @type QPoint |
|
81 """ |
|
82 selectedItemsCount = len(self.selectedItems()) |
|
83 if selectedItemsCount == 0: |
|
84 # background menu |
|
85 self.__showBackgroundMenu(point) |
|
86 elif selectedItemsCount == 1: |
|
87 # single bookmark menu |
|
88 self.__showBookmarkContextMenu(point) |
|
89 else: |
|
90 # multiple selected bookmarks |
|
91 self.__showBookmarksContextMenu(point) |
|
92 |
|
93 @pyqtSlot(QPoint) |
|
94 def __showBackgroundMenu(self, point): |
|
95 """ |
|
96 Private slot to show the background menu (i.e. no selection). |
|
97 |
|
98 @param point position to open the menu at |
|
99 @type QPoint |
|
100 """ |
|
101 menu = QMenu() |
|
102 openBookmarks = menu.addAction(self.tr("Open All Bookmarks")) |
|
103 menu.addSeparator() |
|
104 newBookmark = menu.addAction(self.tr("New Bookmark")) |
|
105 addBookmark = menu.addAction(self.tr("Bookmark Page")) |
|
106 menu.addSeparator() |
|
107 deleteBookmarks = menu.addAction(self.tr("Delete All Bookmarks")) |
|
108 menu.addSeparator() |
|
109 exportBookmarks = menu.addAction(self.tr("Export All Bookmarks")) |
|
110 importBookmarks = menu.addAction(self.tr("Import Bookmarks")) |
|
111 |
|
112 act = menu.exec(self.mapToGlobal(point)) |
|
113 if act == openBookmarks: |
|
114 self.__openBookmarks(selected=False) |
|
115 elif act == newBookmark: |
|
116 self.__newBookmark() |
|
117 elif act == addBookmark: |
|
118 self.__bookmarkCurrentPage() |
|
119 elif act == deleteBookmarks: |
|
120 self.__deleteBookmarks([ |
|
121 self.item(row) for row in range(self.count()) |
|
122 ]) |
|
123 elif act == exportBookmarks: |
|
124 self.__exportBookmarks(selected=False) |
|
125 elif act == importBookmarks: |
|
126 self.__importBookmarks() |
|
127 |
|
128 @pyqtSlot(QPoint) |
|
129 def __showBookmarkContextMenu(self, point): |
|
130 """ |
|
131 Private slot to show the context menu for a bookmark. |
|
132 |
|
133 @param point position to open the menu at |
|
134 @type QPoint |
|
135 """ |
|
136 itm = self.selectedItems()[0] |
|
137 url = itm.data(self.UrlRole) |
|
138 validUrl = ( |
|
139 url is not None and not url.isEmpty() and url.isValid() |
|
140 ) |
|
141 |
|
142 menu = QMenu() |
|
143 curPage = menu.addAction(self.tr("Open Link")) |
|
144 curPage.setEnabled(validUrl) |
|
145 newPage = menu.addAction(self.tr("Open Link in New Page")) |
|
146 newPage.setEnabled(validUrl) |
|
147 newBackgroundPage = menu.addAction( |
|
148 self.tr("Open Link in Background Page")) |
|
149 newBackgroundPage.setEnabled(validUrl) |
|
150 menu.addSeparator() |
|
151 copyUrl = menu.addAction(self.tr("Copy URL to Clipboard")) |
|
152 copyUrl.setEnabled(validUrl) |
|
153 menu.addSeparator() |
|
154 newBookmark = menu.addAction(self.tr("New Bookmark")) |
|
155 addBookmark = menu.addAction(self.tr("Bookmark Page")) |
|
156 menu.addSeparator() |
|
157 editBookmark = menu.addAction(self.tr("Edit Bookmark")) |
|
158 menu.addSeparator() |
|
159 deleteBookmark = menu.addAction(self.tr("Delete Bookmark")) |
|
160 menu.addSeparator() |
|
161 exportBookmarks = menu.addAction(self.tr("Export All Bookmarks")) |
|
162 importBookmarks = menu.addAction(self.tr("Import Bookmarks")) |
|
163 |
|
164 act = menu.exec(self.mapToGlobal(point)) |
|
165 if act == curPage: |
|
166 self.openUrl.emit(url) |
|
167 elif act == newPage: |
|
168 self.newTab.emit(url) |
|
169 elif act == newBackgroundPage: |
|
170 self.newBackgroundTab.emit(url) |
|
171 elif act == copyUrl: |
|
172 # copy the URL to both clipboard areas |
|
173 QGuiApplication.clipboard().setText( |
|
174 url.toString(), QClipboard.Mode.Clipboard) |
|
175 QGuiApplication.clipboard().setText( |
|
176 url.toString(), QClipboard.Mode.Selection) |
|
177 elif act == newBookmark: |
|
178 self.__newBookmark() |
|
179 elif act == addBookmark: |
|
180 self.__bookmarkCurrentPage() |
|
181 elif act == editBookmark: |
|
182 self.__editBookmark(itm) |
|
183 elif act == deleteBookmark: |
|
184 self.__deleteBookmarks([itm]) |
|
185 elif act == exportBookmarks: |
|
186 self.__exportBookmarks(selected=False) |
|
187 elif act == importBookmarks: |
|
188 self.__importBookmarks() |
|
189 |
|
190 @pyqtSlot(QPoint) |
|
191 def __showBookmarksContextMenu(self, point): |
|
192 """ |
|
193 Private slot to show the context menu for multiple bookmark. |
|
194 |
|
195 @param point position to open the menu at |
|
196 @type QPoint |
|
197 """ |
|
198 menu = QMenu() |
|
199 openBookmarks = menu.addAction(self.tr("Open Selected Bookmarks")) |
|
200 menu.addSeparator() |
|
201 deleteBookmarks = menu.addAction(self.tr("Delete Selected Bookmarks")) |
|
202 menu.addSeparator() |
|
203 exportBookmarks = menu.addAction(self.tr("Export Selected Bookmarks")) |
|
204 exportAllBookmarks = menu.addAction(self.tr("Export All Bookmarks")) |
|
205 importBookmarks = menu.addAction(self.tr("Import Bookmarks")) |
|
206 |
|
207 act = menu.exec(self.mapToGlobal(point)) |
|
208 if act == openBookmarks: |
|
209 self.__openBookmarks(selected=True) |
|
210 elif act == deleteBookmarks: |
|
211 self.__deleteBookmarks(self.selectedItems()) |
|
212 elif act == exportBookmarks: |
|
213 self.__exportBookmarks(selected=True) |
|
214 elif act == exportAllBookmarks: |
|
215 self.__exportBookmarks(selected=False) |
|
216 elif act == importBookmarks: |
|
217 self.__importBookmarks() |
|
218 |
|
219 @pyqtSlot(str, str) |
|
220 def __addBookmark(self, title, url): |
|
221 """ |
|
222 Private slot to add a bookmark entry. |
|
223 |
|
224 @param title title for the bookmark |
|
225 @type str |
|
226 @param url URL for the bookmark |
|
227 @type str |
|
228 """ |
|
229 url = url.strip() |
|
230 |
|
231 itm = QListWidgetItem(title, self) |
|
232 itm.setData(self.UrlRole, QUrl(url)) |
|
233 itm.setToolTip(url) |
|
234 |
|
235 @pyqtSlot(str, QUrl) |
|
236 def addBookmark(self, title, url): |
|
237 """ |
|
238 Public slot to add a bookmark with given data. |
|
239 |
|
240 @param title title for the bookmark |
|
241 @type str |
|
242 @param url URL for the bookmark |
|
243 @type QUrl |
|
244 """ |
|
245 dlg = HelpBookmarkPropertiesDialog(title, url.toString(), self) |
|
246 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
247 title, url = dlg.getData() |
|
248 self.__addBookmark(title, url) |
|
249 self.sortItems() |
|
250 self.__saveBookmarks() |
|
251 |
|
252 @pyqtSlot() |
|
253 def __bookmarkCurrentPage(self): |
|
254 """ |
|
255 Private slot to bookmark the current page. |
|
256 """ |
|
257 currentViewer = self.__helpViewer.currentViewer() |
|
258 title = currentViewer.pageTitle() |
|
259 url = currentViewer.link() |
|
260 self.addBookmark(title, url) |
|
261 |
|
262 @pyqtSlot() |
|
263 def __newBookmark(self): |
|
264 """ |
|
265 Private slot to create a new bookmark. |
|
266 """ |
|
267 dlg = HelpBookmarkPropertiesDialog(parent=self) |
|
268 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
269 title, url = dlg.getData() |
|
270 self.__addBookmark(title, url) |
|
271 self.sortItems() |
|
272 self.__saveBookmarks() |
|
273 |
|
274 @pyqtSlot() |
|
275 def __editBookmark(self, itm): |
|
276 """ |
|
277 Private slot to edit a bookmark. |
|
278 |
|
279 @param itm reference to the bookmark item to be edited |
|
280 @type QListWidgetItem |
|
281 """ |
|
282 dlg = HelpBookmarkPropertiesDialog( |
|
283 itm.text(), itm.data(self.UrlRole).toString(), self) |
|
284 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
285 title, url = dlg.getData() |
|
286 itm.setText(title) |
|
287 itm.setData(self.UrlRole, QUrl(url)) |
|
288 itm.setToolTip(url) |
|
289 self.sortItems() |
|
290 self.__saveBookmarks() |
|
291 |
|
292 @pyqtSlot(QListWidgetItem) |
|
293 def __bookmarkActivated(self, itm): |
|
294 """ |
|
295 Private slot handling the activation of a bookmark. |
|
296 |
|
297 @param itm reference to the activated item |
|
298 @type QListWidgetItem |
|
299 """ |
|
300 url = itm.data(self.UrlRole) |
|
301 if url and not url.isEmpty() and url.isValid(): |
|
302 buttons = QApplication.mouseButtons() |
|
303 modifiers = QApplication.keyboardModifiers() |
|
304 |
|
305 if buttons & Qt.MouseButton.MiddleButton: |
|
306 self.newTab.emit(url) |
|
307 else: |
|
308 if ( |
|
309 modifiers & ( |
|
310 Qt.KeyboardModifier.ControlModifier | |
|
311 Qt.KeyboardModifier.ShiftModifier |
|
312 ) == ( |
|
313 Qt.KeyboardModifier.ControlModifier | |
|
314 Qt.KeyboardModifier.ShiftModifier |
|
315 ) |
|
316 ): |
|
317 self.newBackgroundTab.emit(url) |
|
318 elif modifiers & Qt.KeyboardModifier.ControlModifier: |
|
319 self.newTab.emit(url) |
|
320 elif ( |
|
321 modifiers & Qt.KeyboardModifier.ShiftModifier and |
|
322 not self.__internal |
|
323 ): |
|
324 self.newWindow.emit(url) |
|
325 else: |
|
326 self.openUrl.emit(url) |
|
327 |
|
328 def __openBookmarks(self, selected=False): |
|
329 """ |
|
330 Private method to open all or selected bookmarks. |
|
331 |
|
332 @param selected flag indicating to open the selected bookmarks |
|
333 (defaults to False) |
|
334 @type bool (optional) |
|
335 """ |
|
336 items = ( |
|
337 self.selectedItems() |
|
338 if selected else |
|
339 [self.item(row) for row in range(self.count())] |
|
340 ) |
|
341 |
|
342 for itm in items: |
|
343 url = itm.data(self.UrlRole) |
|
344 if url is not None and not url.isEmpty() and url.isValid(): |
|
345 self.newTab.emit(url) |
|
346 |
|
347 def __deleteBookmarks(self, items): |
|
348 """ |
|
349 Private method to delete the given bookmark items. |
|
350 |
|
351 @param items list of bookmarks to be deleted |
|
352 @type list of QListWidgetItem |
|
353 """ |
|
354 from UI.DeleteFilesConfirmationDialog import ( |
|
355 DeleteFilesConfirmationDialog |
|
356 ) |
|
357 dlg = DeleteFilesConfirmationDialog( |
|
358 self, |
|
359 self.tr("Delete Bookmarks"), |
|
360 self.tr("Shall these bookmarks really be deleted?"), |
|
361 [itm.text() for itm in items] |
|
362 ) |
|
363 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
364 for itm in items: |
|
365 self.takeItem(self.row(itm)) |
|
366 del itm |
|
367 self.__saveBookmarks() |
|
368 |
|
369 def __loadBookmarks(self): |
|
370 """ |
|
371 Private method to load the defined bookmarks. |
|
372 """ |
|
373 bookmarksStr = Preferences.getHelp("Bookmarks") |
|
374 with contextlib.suppress(ValueError): |
|
375 bookmarks = json.loads(bookmarksStr) |
|
376 |
|
377 self.clear() |
|
378 for bookmark in bookmarks: |
|
379 self.__addBookmark(bookmark["title"], bookmark["url"]) |
|
380 self.sortItems() |
|
381 |
|
382 def __saveBookmarks(self): |
|
383 """ |
|
384 Private method to save the defined bookmarks. |
|
385 """ |
|
386 bookmarks = [] |
|
387 for row in range(self.count()): |
|
388 itm = self.item(row) |
|
389 bookmarks.append({ |
|
390 "title": itm.text(), |
|
391 "url": itm.data(self.UrlRole).toString(), |
|
392 }) |
|
393 Preferences.setHelp("Bookmarks", json.dumps(bookmarks)) |
|
394 |
|
395 @pyqtSlot() |
|
396 def __exportBookmarks(self, selected=False): |
|
397 """ |
|
398 Private slot to export the bookmarks into a JSON file. |
|
399 |
|
400 @param selected flag indicating to export the selected bookmarks |
|
401 (defaults to False) |
|
402 @type bool (optional) |
|
403 """ |
|
404 filename, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( |
|
405 self, |
|
406 self.tr("Export Bookmarks"), |
|
407 "", |
|
408 self.tr("eric Bookmarks Files (*.json);;All Files (*)"), |
|
409 None, |
|
410 EricFileDialog.DontConfirmOverwrite |
|
411 ) |
|
412 if filename: |
|
413 ext = os.path.splitext(filename)[1] |
|
414 if not ext: |
|
415 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
416 if ex: |
|
417 filename += ex |
|
418 |
|
419 if os.path.exists(filename): |
|
420 ok = EricMessageBox.yesNo( |
|
421 self, |
|
422 self.tr("Export Bookmarks"), |
|
423 self.tr("""The file <b>{0}</b> already exists. Do you""" |
|
424 """ want to overwrite it?""").format(filename)) |
|
425 if not ok: |
|
426 return |
|
427 |
|
428 bookmarksDict = { |
|
429 "creator": "eric7", |
|
430 "version": 1, |
|
431 "created": datetime.datetime.now().isoformat( |
|
432 sep=" ", timespec="seconds"), |
|
433 "bookmarks": [] |
|
434 } |
|
435 bookmarkItems = ( |
|
436 self.selectedItems() |
|
437 if selected else |
|
438 [self.item(row) for row in range(self.count())] |
|
439 ) |
|
440 for bookmarkItem in bookmarkItems: |
|
441 bookmarksDict["bookmarks"].append({ |
|
442 "type": "url", |
|
443 "title": bookmarkItem.text(), |
|
444 "url": bookmarkItem.data(self.UrlRole).toString(), |
|
445 }) |
|
446 |
|
447 jsonStr = json.dumps(bookmarksDict, indent=2, sort_keys=True) |
|
448 try: |
|
449 with open(filename, "w") as f: |
|
450 f.write(jsonStr) |
|
451 except OSError as err: |
|
452 EricMessageBox.critical( |
|
453 self, |
|
454 self.tr("Export Bookmarks"), |
|
455 self.tr("""<p>The bookmarks could not be exported""" |
|
456 """ to <b>{0}</b>.</p><p>Reason: {1}</p>""") |
|
457 .format(filename, str(err))) |
|
458 |
|
459 @pyqtSlot() |
|
460 def __importBookmarks(self): |
|
461 """ |
|
462 Private slot to import bookmarks from a JSON file. |
|
463 """ |
|
464 from .HelpBookmarksImportDialog import HelpBookmarksImportDialog |
|
465 |
|
466 dlg = HelpBookmarksImportDialog(self) |
|
467 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
468 replace, filename = dlg.getData() |
|
469 |
|
470 try: |
|
471 with open(filename, "r") as f: |
|
472 jsonStr = f.read() |
|
473 bookmarks = json.loads(jsonStr) |
|
474 except (OSError, json.JSONDecodeError) as err: |
|
475 EricMessageBox.critical( |
|
476 self, |
|
477 self.tr("Import Bookmarks"), |
|
478 self.tr( |
|
479 "<p>The bookmarks file <b>{0}</b> could not be " |
|
480 "read.</p><p>Reason: {1}</p>" |
|
481 ).format(filename, str(err)) |
|
482 ) |
|
483 return |
|
484 |
|
485 if not isinstance(bookmarks, dict): |
|
486 EricMessageBox.critical( |
|
487 self, |
|
488 self.tr("Import Bookmarks"), |
|
489 self.tr( |
|
490 "The bookmarks file <b>{0}</b> has invalid contents." |
|
491 ).format(filename) |
|
492 ) |
|
493 return |
|
494 |
|
495 try: |
|
496 if bookmarks["creator"] != "eric7": |
|
497 EricMessageBox.critical( |
|
498 self, |
|
499 self.tr("Import Bookmarks"), |
|
500 self.tr( |
|
501 "The bookmarks file <b>{0}</b> was not created" |
|
502 " with 'eric7'." |
|
503 ).format(filename) |
|
504 ) |
|
505 return |
|
506 |
|
507 if bookmarks["version"] != 1: |
|
508 EricMessageBox.critical( |
|
509 self, |
|
510 self.tr("Import Bookmarks"), |
|
511 self.tr( |
|
512 "The bookmarks file <b>{0}</b> has an unsupported" |
|
513 " format version." |
|
514 ).format(filename) |
|
515 ) |
|
516 return |
|
517 |
|
518 if replace: |
|
519 self.clear() |
|
520 |
|
521 for bookmark in bookmarks["bookmarks"]: |
|
522 if bookmark["type"] == "url": |
|
523 self.__addBookmark(bookmark["title"], bookmark["url"]) |
|
524 self.sortItems() |
|
525 self.__saveBookmarks() |
|
526 |
|
527 except KeyError: |
|
528 EricMessageBox.critical( |
|
529 self, |
|
530 self.tr("Import Bookmarks"), |
|
531 self.tr( |
|
532 "The bookmarks file <b>{0}</b> has invalid contents." |
|
533 ).format(filename) |
|
534 ) |