|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the bookmark model class. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 |
|
12 from PyQt5.QtCore import ( |
|
13 Qt, QAbstractItemModel, QModelIndex, QUrl, QByteArray, QDataStream, |
|
14 QIODevice, QBuffer, QMimeData |
|
15 ) |
|
16 |
|
17 import UI.PixmapCache |
|
18 |
|
19 |
|
20 class BookmarksModel(QAbstractItemModel): |
|
21 """ |
|
22 Class implementing the bookmark model. |
|
23 """ |
|
24 TypeRole = Qt.ItemDataRole.UserRole + 1 |
|
25 UrlRole = Qt.ItemDataRole.UserRole + 2 |
|
26 UrlStringRole = Qt.ItemDataRole.UserRole + 3 |
|
27 VisitCountRole = Qt.ItemDataRole.UserRole + 4 |
|
28 SeparatorRole = Qt.ItemDataRole.UserRole + 5 |
|
29 |
|
30 MIMETYPE = "application/bookmarks.xbel" |
|
31 |
|
32 def __init__(self, manager, parent=None): |
|
33 """ |
|
34 Constructor |
|
35 |
|
36 @param manager reference to the bookmark manager object |
|
37 (BookmarksManager) |
|
38 @param parent reference to the parent object (QObject) |
|
39 """ |
|
40 super().__init__(parent) |
|
41 |
|
42 self.__endMacro = False |
|
43 self.__bookmarksManager = manager |
|
44 |
|
45 manager.entryAdded.connect(self.entryAdded) |
|
46 manager.entryRemoved.connect(self.entryRemoved) |
|
47 manager.entryChanged.connect(self.entryChanged) |
|
48 |
|
49 self.__headers = [ |
|
50 self.tr("Title"), |
|
51 self.tr("Address"), |
|
52 ] |
|
53 |
|
54 def bookmarksManager(self): |
|
55 """ |
|
56 Public method to get a reference to the bookmarks manager. |
|
57 |
|
58 @return reference to the bookmarks manager object (BookmarksManager) |
|
59 """ |
|
60 return self.__bookmarksManager |
|
61 |
|
62 def nodeIndex(self, node): |
|
63 """ |
|
64 Public method to get a model index. |
|
65 |
|
66 @param node reference to the node to get the index for (BookmarkNode) |
|
67 @return model index (QModelIndex) |
|
68 """ |
|
69 parent = node.parent() |
|
70 if parent is None: |
|
71 return QModelIndex() |
|
72 return self.createIndex(parent.children().index(node), 0, node) |
|
73 |
|
74 def entryAdded(self, node): |
|
75 """ |
|
76 Public slot to add a bookmark node. |
|
77 |
|
78 @param node reference to the bookmark node to add (BookmarkNode) |
|
79 """ |
|
80 if node is None or node.parent() is None: |
|
81 return |
|
82 |
|
83 parent = node.parent() |
|
84 row = parent.children().index(node) |
|
85 # node was already added so remove before beginInsertRows is called |
|
86 parent.remove(node) |
|
87 self.beginInsertRows(self.nodeIndex(parent), row, row) |
|
88 parent.add(node, row) |
|
89 self.endInsertRows() |
|
90 |
|
91 def entryRemoved(self, parent, row, node): |
|
92 """ |
|
93 Public slot to remove a bookmark node. |
|
94 |
|
95 @param parent reference to the parent bookmark node (BookmarkNode) |
|
96 @param row row number of the node (integer) |
|
97 @param node reference to the bookmark node to remove (BookmarkNode) |
|
98 """ |
|
99 # node was already removed, re-add so beginRemoveRows works |
|
100 parent.add(node, row) |
|
101 self.beginRemoveRows(self.nodeIndex(parent), row, row) |
|
102 parent.remove(node) |
|
103 self.endRemoveRows() |
|
104 |
|
105 def entryChanged(self, node): |
|
106 """ |
|
107 Public method to change a node. |
|
108 |
|
109 @param node reference to the bookmark node to change (BookmarkNode) |
|
110 """ |
|
111 idx = self.nodeIndex(node) |
|
112 self.dataChanged.emit(idx, idx) |
|
113 |
|
114 def removeRows(self, row, count, parent=None): |
|
115 """ |
|
116 Public method to remove bookmarks from the model. |
|
117 |
|
118 @param row row of the first bookmark to remove (integer) |
|
119 @param count number of bookmarks to remove (integer) |
|
120 @param parent index of the parent bookmark node (QModelIndex) |
|
121 @return flag indicating successful removal (boolean) |
|
122 """ |
|
123 if parent is None: |
|
124 parent = QModelIndex() |
|
125 |
|
126 if row < 0 or count <= 0 or row + count > self.rowCount(parent): |
|
127 return False |
|
128 |
|
129 bookmarkNode = self.node(parent) |
|
130 children = bookmarkNode.children()[row:(row + count)] |
|
131 for node in children: |
|
132 if node in ( |
|
133 self.__bookmarksManager.menu(), |
|
134 self.__bookmarksManager.toolbar() |
|
135 ): |
|
136 continue |
|
137 self.__bookmarksManager.removeBookmark(node) |
|
138 |
|
139 if self.__endMacro: |
|
140 self.__bookmarksManager.undoRedoStack().endMacro() |
|
141 self.__endMacro = False |
|
142 |
|
143 return True |
|
144 |
|
145 def headerData(self, section, orientation, |
|
146 role=Qt.ItemDataRole.DisplayRole): |
|
147 """ |
|
148 Public method to get the header data. |
|
149 |
|
150 @param section section number (integer) |
|
151 @param orientation header orientation (Qt.Orientation) |
|
152 @param role data role (Qt.ItemDataRole) |
|
153 @return header data |
|
154 """ |
|
155 if ( |
|
156 orientation == Qt.Orientation.Horizontal and |
|
157 role == Qt.ItemDataRole.DisplayRole |
|
158 ): |
|
159 with contextlib.suppress(IndexError): |
|
160 return self.__headers[section] |
|
161 return QAbstractItemModel.headerData(self, section, orientation, role) |
|
162 |
|
163 def data(self, index, role=Qt.ItemDataRole.DisplayRole): |
|
164 """ |
|
165 Public method to get data from the model. |
|
166 |
|
167 @param index index of bookmark to get data for (QModelIndex) |
|
168 @param role data role (integer) |
|
169 @return bookmark data |
|
170 """ |
|
171 if not index.isValid() or index.model() != self: |
|
172 return None |
|
173 |
|
174 from .BookmarkNode import BookmarkNode |
|
175 |
|
176 bookmarkNode = self.node(index) |
|
177 if role in [Qt.ItemDataRole.EditRole, Qt.ItemDataRole.DisplayRole]: |
|
178 if bookmarkNode.type() == BookmarkNode.Separator: |
|
179 if index.column() == 0: |
|
180 return 50 * '\xB7' |
|
181 elif index.column() == 1: |
|
182 return "" |
|
183 |
|
184 if index.column() == 0: |
|
185 return bookmarkNode.title |
|
186 elif index.column() == 1: |
|
187 return bookmarkNode.url |
|
188 |
|
189 elif role == BookmarksModel.UrlRole: |
|
190 return QUrl(bookmarkNode.url) |
|
191 |
|
192 elif role == BookmarksModel.UrlStringRole: |
|
193 return bookmarkNode.url |
|
194 |
|
195 elif role == BookmarksModel.VisitCountRole: |
|
196 return bookmarkNode.visitCount |
|
197 |
|
198 elif role == BookmarksModel.TypeRole: |
|
199 return bookmarkNode.type() |
|
200 |
|
201 elif role == BookmarksModel.SeparatorRole: |
|
202 return bookmarkNode.type() == BookmarkNode.Separator |
|
203 |
|
204 elif ( |
|
205 role == Qt.ItemDataRole.DecorationRole and |
|
206 index.column() == 0 |
|
207 ): |
|
208 if bookmarkNode.type() == BookmarkNode.Folder: |
|
209 return UI.PixmapCache.getIcon("dirOpen") |
|
210 import WebBrowser.WebBrowserWindow |
|
211 return WebBrowser.WebBrowserWindow.WebBrowserWindow.icon( |
|
212 QUrl(bookmarkNode.url)) |
|
213 |
|
214 return None |
|
215 |
|
216 def columnCount(self, parent=None): |
|
217 """ |
|
218 Public method to get the number of columns. |
|
219 |
|
220 @param parent index of parent (QModelIndex) |
|
221 @return number of columns (integer) |
|
222 """ |
|
223 if parent is None: |
|
224 parent = QModelIndex() |
|
225 |
|
226 if parent.column() > 0: |
|
227 return 0 |
|
228 else: |
|
229 return len(self.__headers) |
|
230 |
|
231 def rowCount(self, parent=None): |
|
232 """ |
|
233 Public method to determine the number of rows. |
|
234 |
|
235 @param parent index of parent (QModelIndex) |
|
236 @return number of rows (integer) |
|
237 """ |
|
238 if parent is None: |
|
239 parent = QModelIndex() |
|
240 |
|
241 if parent.column() > 0: |
|
242 return 0 |
|
243 |
|
244 if not parent.isValid(): |
|
245 return len(self.__bookmarksManager.bookmarks().children()) |
|
246 |
|
247 itm = parent.internalPointer() |
|
248 return len(itm.children()) |
|
249 |
|
250 def index(self, row, column, parent=None): |
|
251 """ |
|
252 Public method to get a model index for a node cell. |
|
253 |
|
254 @param row row number (integer) |
|
255 @param column column number (integer) |
|
256 @param parent index of the parent (QModelIndex) |
|
257 @return index (QModelIndex) |
|
258 """ |
|
259 if parent is None: |
|
260 parent = QModelIndex() |
|
261 |
|
262 if ( |
|
263 row < 0 or |
|
264 column < 0 or |
|
265 row >= self.rowCount(parent) or |
|
266 column >= self.columnCount(parent) |
|
267 ): |
|
268 return QModelIndex() |
|
269 |
|
270 parentNode = self.node(parent) |
|
271 return self.createIndex(row, column, parentNode.children()[row]) |
|
272 |
|
273 def parent(self, index=None): |
|
274 """ |
|
275 Public method to get the index of the parent node. |
|
276 |
|
277 @param index index of the child node (QModelIndex) |
|
278 @return index of the parent node (QModelIndex) |
|
279 """ |
|
280 if index is None: |
|
281 index = QModelIndex() |
|
282 |
|
283 if not index.isValid(): |
|
284 return QModelIndex() |
|
285 |
|
286 itemNode = self.node(index) |
|
287 parentNode = itemNode.parent() if itemNode else None |
|
288 |
|
289 if ( |
|
290 parentNode is None or |
|
291 parentNode == self.__bookmarksManager.bookmarks() |
|
292 ): |
|
293 return QModelIndex() |
|
294 |
|
295 # get the parent's row |
|
296 grandParentNode = parentNode.parent() |
|
297 parentRow = grandParentNode.children().index(parentNode) |
|
298 return self.createIndex(parentRow, 0, parentNode) |
|
299 |
|
300 def hasChildren(self, parent=None): |
|
301 """ |
|
302 Public method to check, if a parent node has some children. |
|
303 |
|
304 @param parent index of the parent node (QModelIndex) |
|
305 @return flag indicating the presence of children (boolean) |
|
306 """ |
|
307 if parent is None: |
|
308 parent = QModelIndex() |
|
309 |
|
310 if not parent.isValid(): |
|
311 return True |
|
312 |
|
313 from .BookmarkNode import BookmarkNode |
|
314 parentNode = self.node(parent) |
|
315 return parentNode.type() == BookmarkNode.Folder |
|
316 |
|
317 def flags(self, index): |
|
318 """ |
|
319 Public method to get flags for a node cell. |
|
320 |
|
321 @param index index of the node cell (QModelIndex) |
|
322 @return flags (Qt.ItemFlags) |
|
323 """ |
|
324 if not index.isValid(): |
|
325 return Qt.ItemFlag.NoItemFlags |
|
326 |
|
327 node = self.node(index) |
|
328 type_ = node.type() |
|
329 flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled |
|
330 |
|
331 if self.hasChildren(index): |
|
332 flags |= Qt.ItemFlag.ItemIsDropEnabled |
|
333 |
|
334 if node in ( |
|
335 self.__bookmarksManager.menu(), |
|
336 self.__bookmarksManager.toolbar() |
|
337 ): |
|
338 return flags |
|
339 |
|
340 flags |= Qt.ItemFlag.ItemIsDragEnabled |
|
341 |
|
342 from .BookmarkNode import BookmarkNode |
|
343 if ( |
|
344 (index.column() == 0 and type_ != BookmarkNode.Separator) or |
|
345 (index.column() == 1 and type_ == BookmarkNode.Bookmark) |
|
346 ): |
|
347 flags |= Qt.ItemFlag.ItemIsEditable |
|
348 |
|
349 return flags |
|
350 |
|
351 def supportedDropActions(self): |
|
352 """ |
|
353 Public method to report the supported drop actions. |
|
354 |
|
355 @return supported drop actions (Qt.DropAction) |
|
356 """ |
|
357 return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction |
|
358 |
|
359 def mimeTypes(self): |
|
360 """ |
|
361 Public method to report the supported mime types. |
|
362 |
|
363 @return supported mime types (list of strings) |
|
364 """ |
|
365 return [self.MIMETYPE, "text/uri-list"] |
|
366 |
|
367 def mimeData(self, indexes): |
|
368 """ |
|
369 Public method to return the mime data. |
|
370 |
|
371 @param indexes list of indexes (QModelIndexList) |
|
372 @return mime data (QMimeData) |
|
373 """ |
|
374 from .XbelWriter import XbelWriter |
|
375 |
|
376 data = QByteArray() |
|
377 stream = QDataStream(data, QIODevice.OpenModeFlag.WriteOnly) |
|
378 urls = [] |
|
379 |
|
380 for index in indexes: |
|
381 if index.column() != 0 or not index.isValid(): |
|
382 continue |
|
383 |
|
384 encodedData = QByteArray() |
|
385 buffer = QBuffer(encodedData) |
|
386 buffer.open(QIODevice.OpenModeFlag.ReadWrite) |
|
387 writer = XbelWriter() |
|
388 parentNode = self.node(index) |
|
389 writer.write(buffer, parentNode) |
|
390 stream << encodedData |
|
391 urls.append(index.data(self.UrlRole)) |
|
392 |
|
393 mdata = QMimeData() |
|
394 mdata.setData(self.MIMETYPE, data) |
|
395 mdata.setUrls(urls) |
|
396 return mdata |
|
397 |
|
398 def dropMimeData(self, data, action, row, column, parent): |
|
399 """ |
|
400 Public method to accept the mime data of a drop action. |
|
401 |
|
402 @param data reference to the mime data (QMimeData) |
|
403 @param action drop action requested (Qt.DropAction) |
|
404 @param row row number (integer) |
|
405 @param column column number (integer) |
|
406 @param parent index of the parent node (QModelIndex) |
|
407 @return flag indicating successful acceptance of the data (boolean) |
|
408 """ |
|
409 if action == Qt.DropAction.IgnoreAction: |
|
410 return True |
|
411 |
|
412 if column > 0: |
|
413 return False |
|
414 |
|
415 parentNode = self.node(parent) |
|
416 |
|
417 if not data.hasFormat(self.MIMETYPE): |
|
418 if not data.hasUrls(): |
|
419 return False |
|
420 |
|
421 from .BookmarkNode import BookmarkNode |
|
422 node = BookmarkNode(BookmarkNode.Bookmark, parentNode) |
|
423 node.url = bytes(data.urls()[0].toEncoded()).decode() |
|
424 |
|
425 if data.hasText(): |
|
426 node.title = data.text() |
|
427 else: |
|
428 node.title = node.url |
|
429 |
|
430 self.__bookmarksManager.addBookmark(parentNode, node, row) |
|
431 return True |
|
432 |
|
433 ba = data.data(self.MIMETYPE) |
|
434 stream = QDataStream(ba, QIODevice.OpenModeFlag.ReadOnly) |
|
435 if stream.atEnd(): |
|
436 return False |
|
437 |
|
438 undoStack = self.__bookmarksManager.undoRedoStack() |
|
439 undoStack.beginMacro("Move Bookmarks") |
|
440 |
|
441 from .XbelReader import XbelReader |
|
442 while not stream.atEnd(): |
|
443 encodedData = QByteArray() |
|
444 stream >> encodedData |
|
445 buffer = QBuffer(encodedData) |
|
446 buffer.open(QIODevice.OpenModeFlag.ReadOnly) |
|
447 |
|
448 reader = XbelReader() |
|
449 rootNode = reader.read(buffer) |
|
450 for bookmarkNode in rootNode.children(): |
|
451 rootNode.remove(bookmarkNode) |
|
452 row = max(0, row) |
|
453 self.__bookmarksManager.addBookmark( |
|
454 parentNode, bookmarkNode, row) |
|
455 self.__endMacro = True |
|
456 |
|
457 return True |
|
458 |
|
459 def setData(self, index, value, role=Qt.ItemDataRole.EditRole): |
|
460 """ |
|
461 Public method to set the data of a node cell. |
|
462 |
|
463 @param index index of the node cell (QModelIndex) |
|
464 @param value value to be set |
|
465 @param role role of the data (integer) |
|
466 @return flag indicating success (boolean) |
|
467 """ |
|
468 if ( |
|
469 not index.isValid() or |
|
470 (self.flags(index) & Qt.ItemFlag.ItemIsEditable) == 0 |
|
471 ): |
|
472 return False |
|
473 |
|
474 item = self.node(index) |
|
475 |
|
476 if role in (Qt.ItemDataRole.EditRole, Qt.ItemDataRole.DisplayRole): |
|
477 if index.column() == 0: |
|
478 self.__bookmarksManager.setTitle(item, value) |
|
479 elif index.column() == 1: |
|
480 self.__bookmarksManager.setUrl(item, value) |
|
481 else: |
|
482 return False |
|
483 |
|
484 elif role == BookmarksModel.UrlRole: |
|
485 self.__bookmarksManager.setUrl(item, value.toString()) |
|
486 |
|
487 elif role == BookmarksModel.UrlStringRole: |
|
488 self.__bookmarksManager.setUrl(item, value) |
|
489 |
|
490 elif role == BookmarksModel.VisitCountRole: |
|
491 self.__bookmarksManager.setVisitCount(item, value) |
|
492 |
|
493 else: |
|
494 return False |
|
495 |
|
496 return True |
|
497 |
|
498 def node(self, index): |
|
499 """ |
|
500 Public method to get a bookmark node given its index. |
|
501 |
|
502 @param index index of the node (QModelIndex) |
|
503 @return bookmark node (BookmarkNode) |
|
504 """ |
|
505 itemNode = index.internalPointer() |
|
506 if itemNode is None: |
|
507 return self.__bookmarksManager.bookmarks() |
|
508 else: |
|
509 return itemNode |