src/eric7/HelpViewer/HelpBookmarksWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8902
ba9b8c6e4928
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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 )

eric ide

mercurial