|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 import contextlib |
|
7 import json |
|
8 |
|
9 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QUrl |
|
10 from PyQt6.QtGui import QClipboard, QGuiApplication |
|
11 from PyQt6.QtWidgets import ( |
|
12 QAbstractItemView, QApplication, QDialog, QListWidget, QListWidgetItem, QMenu |
|
13 ) |
|
14 |
|
15 import Preferences |
|
16 |
|
17 from .HelpBookmarkPropertiesDialog import HelpBookmarkPropertiesDialog |
|
18 |
|
19 class HelpBookmarksWidget(QListWidget): |
|
20 """ |
|
21 Class implementing a widget showing the list of bookmarks. |
|
22 |
|
23 @signal escapePressed() emitted when the ESC key was pressed |
|
24 @signal openUrl(QUrl, str) emitted to open an entry in the current tab |
|
25 @signal newTab(QUrl, str) emitted to open an entry in a new tab |
|
26 @signal newBackgroundTab(QUrl, str) emitted to open an entry in a |
|
27 new background tab |
|
28 """ |
|
29 escapePressed = pyqtSignal() |
|
30 openUrl = pyqtSignal(QUrl) |
|
31 newTab = pyqtSignal(QUrl) |
|
32 newBackgroundTab = pyqtSignal(QUrl) |
|
33 |
|
34 UrlRole = Qt.ItemDataRole.UserRole + 1 |
|
35 |
|
36 def __init__(self, parent=None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param parent reference to the parent widget (defaults to None) |
|
41 @type QWidget (optional) |
|
42 """ |
|
43 super().__init__(parent) |
|
44 self.setObjectName("HelpBookmarksWidget") |
|
45 |
|
46 self.__helpViewer = parent |
|
47 |
|
48 self.setAlternatingRowColors(True) |
|
49 self.setSelectionMode( |
|
50 QAbstractItemView.SelectionMode.ExtendedSelection) |
|
51 self.setSortingEnabled(True) |
|
52 |
|
53 self.setContextMenuPolicy( |
|
54 Qt.ContextMenuPolicy.CustomContextMenu) |
|
55 self.customContextMenuRequested.connect( |
|
56 self.__showContextMenu) |
|
57 |
|
58 self.__bookmarks = [] |
|
59 self.__loadBookmarks() |
|
60 |
|
61 self.itemDoubleClicked.connect(self.__bookmarkActivated) |
|
62 |
|
63 @pyqtSlot(QPoint) |
|
64 def __showContextMenu(self, point): |
|
65 """ |
|
66 Private slot to handle the customContextMenuRequested signal of |
|
67 the viewlist. |
|
68 |
|
69 @param point position to open the menu at |
|
70 @type QPoint |
|
71 """ |
|
72 selectedItemsCount = len(self.selectedItems()) |
|
73 if selectedItemsCount == 0: |
|
74 # background menu |
|
75 self.__showBackgroundMenu(point) |
|
76 elif selectedItemsCount == 1: |
|
77 # single bookmark menu |
|
78 self.__showBookmarkContextMenu(point) |
|
79 else: |
|
80 # multiple selected bookmarks |
|
81 self.__showBookmarksContextMenu(point) |
|
82 |
|
83 @pyqtSlot(QPoint) |
|
84 def __showBackgroundMenu(self, point): |
|
85 """ |
|
86 Private slot to show the background menu (i.e. no selection). |
|
87 |
|
88 @param point position to open the menu at |
|
89 @type QPoint |
|
90 """ |
|
91 menu = QMenu() |
|
92 openBookmarks = menu.addAction(self.tr("Open All Bookmarks")) |
|
93 menu.addSeparator() |
|
94 newBookmark = menu.addAction(self.tr("New Bookmark")) |
|
95 addBookmark = menu.addAction(self.tr("Bookmark Page")) |
|
96 menu.addSeparator() |
|
97 deleteBookmarks = menu.addAction(self.tr("Delete All Bookmark")) |
|
98 |
|
99 act = menu.exec(self.mapToGlobal(point)) |
|
100 if act == openBookmarks: |
|
101 self.__openBookmarks(selected=False) |
|
102 elif act == newBookmark: |
|
103 self.__newBookmark() |
|
104 elif act == addBookmark: |
|
105 self.__bookmarkCurrentPage() |
|
106 elif act == deleteBookmarks: |
|
107 self.__deleteBookmarks([ |
|
108 self.item(row) for row in range(self.count()) |
|
109 ]) |
|
110 |
|
111 @pyqtSlot(QPoint) |
|
112 def __showBookmarkContextMenu(self, point): |
|
113 """ |
|
114 Private slot to show the context menu for a bookmark. |
|
115 |
|
116 @param point position to open the menu at |
|
117 @type QPoint |
|
118 """ |
|
119 itm = self.selectedItems()[0] |
|
120 url = itm.data(self.UrlRole) |
|
121 validUrl = ( |
|
122 url is not None and not url.isEmpty() and url.isValid() |
|
123 ) |
|
124 |
|
125 menu = QMenu() |
|
126 curPage = menu.addAction(self.tr("Open Link")) |
|
127 curPage.setEnabled(validUrl) |
|
128 newPage = menu.addAction(self.tr("Open Link in New Page")) |
|
129 newPage.setEnabled(validUrl) |
|
130 newBackgroundPage = menu.addAction( |
|
131 self.tr("Open Link in Background Page")) |
|
132 newBackgroundPage.setEnabled(validUrl) |
|
133 menu.addSeparator() |
|
134 copyUrl = menu.addAction(self.tr("Copy URL to Clipboard")) |
|
135 copyUrl.setEnabled(validUrl) |
|
136 menu.addSeparator() |
|
137 newBookmark = menu.addAction(self.tr("New Bookmark")) |
|
138 addBookmark = menu.addAction(self.tr("Bookmark Page")) |
|
139 menu.addSeparator() |
|
140 editBookmark = menu.addAction(self.tr("Edit Bookmark")) |
|
141 menu.addSeparator() |
|
142 deleteBookmark = menu.addAction(self.tr("Delete Bookmark")) |
|
143 |
|
144 act = menu.exec(self.mapToGlobal(point)) |
|
145 if act == curPage: |
|
146 self.openUrl.emit(url) |
|
147 elif act == newPage: |
|
148 self.newTab.emit(url) |
|
149 elif act == newBackgroundPage: |
|
150 self.newBackgroundTab.emit(url) |
|
151 elif act == copyUrl: |
|
152 # copy the URL to both clipboard areas |
|
153 QGuiApplication.clipboard().setText( |
|
154 url.toString(), QClipboard.Mode.Clipboard) |
|
155 QGuiApplication.clipboard().setText( |
|
156 url.toString(), QClipboard.Mode.Selection) |
|
157 elif act == newBookmark: |
|
158 self.__newBookmark() |
|
159 elif act == addBookmark: |
|
160 self.__bookmarkCurrentPage() |
|
161 elif act == editBookmark: |
|
162 self.__editBookmark(itm) |
|
163 elif act == deleteBookmark: |
|
164 self.__deleteBookmarks([itm]) |
|
165 |
|
166 @pyqtSlot(QPoint) |
|
167 def __showBookmarksContextMenu(self, point): |
|
168 """ |
|
169 Private slot to show the context menu for multiple bookmark. |
|
170 |
|
171 @param point position to open the menu at |
|
172 @type QPoint |
|
173 """ |
|
174 menu = QMenu() |
|
175 openBookmarks = menu.addAction(self.tr("Open Selected Bookmarks")) |
|
176 menu.addSeparator() |
|
177 deleteBookmarks = menu.addAction(self.tr("Delete Selected Bookmarks")) |
|
178 |
|
179 act = menu.exec(self.mapToGlobal(point)) |
|
180 if act == openBookmarks: |
|
181 self.__openBookmarks(selected=True) |
|
182 elif act == deleteBookmarks: |
|
183 self.__deleteBookmarks(self.selectedItems()) |
|
184 |
|
185 @pyqtSlot(str, str) |
|
186 def __addBookmark(self, title, url): |
|
187 """ |
|
188 Private slot to add a bookmark entry. |
|
189 |
|
190 @param title title for the bookmark |
|
191 @type str |
|
192 @param url URL for the bookmark |
|
193 @type str |
|
194 """ |
|
195 url = url.strip() |
|
196 |
|
197 itm = QListWidgetItem(title, self) |
|
198 itm.setData(self.UrlRole, QUrl(url)) |
|
199 itm.setToolTip(url) |
|
200 |
|
201 @pyqtSlot(str, QUrl) |
|
202 def addBookmark(self, title, url): |
|
203 """ |
|
204 Public slot to add a bookmark with given data. |
|
205 |
|
206 @param title title for the bookmark |
|
207 @type str |
|
208 @param url URL for the bookmark |
|
209 @type QUrl |
|
210 """ |
|
211 dlg = HelpBookmarkPropertiesDialog(title, url.toString(), self) |
|
212 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
213 title, url = dlg.getData() |
|
214 self.__addBookmark(title, url) |
|
215 self.sortItems() |
|
216 self.__saveBookmarks() |
|
217 |
|
218 @pyqtSlot() |
|
219 def __bookmarkCurrentPage(self): |
|
220 """ |
|
221 Private slot to bookmark the current page. |
|
222 """ |
|
223 currentViewer = self.__helpViewer.currentViewer() |
|
224 title = currentViewer.pageTitle() |
|
225 url = currentViewer.link() |
|
226 self.addBookmark(title, url) |
|
227 |
|
228 @pyqtSlot() |
|
229 def __newBookmark(self): |
|
230 """ |
|
231 Private slot to create a new bookmark. |
|
232 """ |
|
233 dlg = HelpBookmarkPropertiesDialog(parent=self) |
|
234 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
235 title, url = dlg.getData() |
|
236 self.__addBookmark(title, url) |
|
237 self.sortItems() |
|
238 self.__saveBookmarks() |
|
239 |
|
240 @pyqtSlot() |
|
241 def __editBookmark(self, itm): |
|
242 """ |
|
243 Private slot to edit a bookmark. |
|
244 |
|
245 @param itm reference to the bookmark item to be edited |
|
246 @type QListWidgetItem |
|
247 """ |
|
248 dlg = HelpBookmarkPropertiesDialog( |
|
249 itm.text(), itm.data(self.UrlRole).toString(), self) |
|
250 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
251 title, url = dlg.getData() |
|
252 itm.setText(title) |
|
253 itm.setData(self.UrlRole, QUrl(url)) |
|
254 itm.setToolTip(url) |
|
255 self.sortItems() |
|
256 self.__saveBookmarks() |
|
257 |
|
258 @pyqtSlot(QListWidgetItem) |
|
259 def __bookmarkActivated(self, itm): |
|
260 """ |
|
261 Private slot handling the activation of a bookmark. |
|
262 |
|
263 @param itm reference to the activated item |
|
264 @type QListWidgetItem |
|
265 """ |
|
266 url = itm.data(self.UrlRole) |
|
267 if url and not url.isEmpty() and url.isValid(): |
|
268 buttons = QApplication.mouseButtons() |
|
269 modifiers = QApplication.keyboardModifiers() |
|
270 |
|
271 if buttons & Qt.MouseButton.MiddleButton: |
|
272 self.newTab.emit(url) |
|
273 else: |
|
274 if ( |
|
275 modifiers & ( |
|
276 Qt.KeyboardModifier.ControlModifier | |
|
277 Qt.KeyboardModifier.ShiftModifier |
|
278 ) == ( |
|
279 Qt.KeyboardModifier.ControlModifier | |
|
280 Qt.KeyboardModifier.ShiftModifier |
|
281 ) |
|
282 ): |
|
283 self.newBackgroundTab.emit(url) |
|
284 elif modifiers & Qt.KeyboardModifier.ControlModifier: |
|
285 self.newTab.emit(url) |
|
286 elif ( |
|
287 modifiers & Qt.KeyboardModifier.ShiftModifier and |
|
288 not self.__internal |
|
289 ): |
|
290 self.newWindow.emit(url) |
|
291 else: |
|
292 self.openUrl.emit(url) |
|
293 |
|
294 def __openBookmarks(self, selected=False): |
|
295 """ |
|
296 Private method to open all or selected bookmarks. |
|
297 |
|
298 @param selected flag indicating to open the selected bookmarks |
|
299 (defaults to False) |
|
300 @type bool (optional) |
|
301 """ |
|
302 items = ( |
|
303 self.selectedItems() |
|
304 if selected else |
|
305 [self.item(row) for row in range(self.count())] |
|
306 ) |
|
307 |
|
308 for itm in items: |
|
309 url = itm.data(self.UrlRole) |
|
310 if url is not None and not url.isEmpty() and url.isValid(): |
|
311 self.newTab.emit(url) |
|
312 |
|
313 def __deleteBookmarks(self, items): |
|
314 """ |
|
315 Private method to delete the given bookmark items. |
|
316 |
|
317 @param items list of bookmarks to be deleted |
|
318 @type list of QListWidgetItem |
|
319 """ |
|
320 from UI.DeleteFilesConfirmationDialog import ( |
|
321 DeleteFilesConfirmationDialog |
|
322 ) |
|
323 dlg = DeleteFilesConfirmationDialog( |
|
324 self, |
|
325 self.tr("Delete Bookmarks"), |
|
326 self.tr("Shall these bookmarks really be deleted?"), |
|
327 [itm.text() for itm in items] |
|
328 ) |
|
329 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
330 for itm in items: |
|
331 self.takeItem(self.row(itm)) |
|
332 del itm |
|
333 self.__saveBookmarks() |
|
334 |
|
335 def __loadBookmarks(self): |
|
336 """ |
|
337 Private method to load the defined bookmarks. |
|
338 """ |
|
339 bookmarksStr = Preferences.getHelp("Bookmarks") |
|
340 with contextlib.suppress(ValueError): |
|
341 bookmarks = json.loads(bookmarksStr) |
|
342 |
|
343 self.clear() |
|
344 for bookmark in bookmarks: |
|
345 self.__addBookmark(bookmark["title"], bookmark["url"]) |
|
346 self.sortItems() |
|
347 |
|
348 def __saveBookmarks(self): |
|
349 """ |
|
350 Private method to save the defined bookmarks. |
|
351 """ |
|
352 bookmarks = [] |
|
353 for row in range(self.count()): |
|
354 itm = self.item(row) |
|
355 bookmarks.append({ |
|
356 "title": itm.text(), |
|
357 "url": itm.data(self.UrlRole).toString(), |
|
358 }) |
|
359 Preferences.setHelp("Bookmarks", json.dumps(bookmarks)) |
|
360 |
|
361 def __exportBookmarks(self): |
|
362 """ |
|
363 Private method to export the bookmarks into a JSON file. |
|
364 """ |
|
365 # TODO: not yet implemented |
|
366 |
|
367 def __importBookmarks(self): |
|
368 """ |
|
369 Private method to import bookmarks from a JSON file. |
|
370 """ |
|
371 # TODO: not yet implemented |
|
372 # 1. read file |
|
373 # 2. check, if exported from eric and compatible format version |
|
374 # 3. process each entry |