src/eric7/WebBrowser/Feeds/FeedsManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a RSS feeds manager dialog.
8 """
9
10 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QXmlStreamReader
11 from PyQt6.QtGui import QCursor
12 from PyQt6.QtWidgets import QDialog, QTreeWidgetItem, QMenu, QApplication
13 from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
14
15 from EricWidgets import EricMessageBox
16
17 from .Ui_FeedsManager import Ui_FeedsManager
18
19 import Preferences
20 import UI.PixmapCache
21
22
23 class FeedsManager(QDialog, Ui_FeedsManager):
24 """
25 Class implementing a RSS feeds manager dialog.
26
27 @signal openUrl(QUrl, str) emitted to open a URL in the current tab
28 @signal newTab(QUrl, str) emitted to open a URL in a new tab
29 @signal newBackgroundTab(QUrl, str) emitted to open a URL in a new
30 background tab
31 @signal newWindow(QUrl, str) emitted to open a URL in a new window
32 @signal newPrivateWindow(QUrl, str) emitted to open a URL in a new
33 private window
34 """
35 openUrl = pyqtSignal(QUrl, str)
36 newTab = pyqtSignal(QUrl, str)
37 newBackgroundTab = pyqtSignal(QUrl, str)
38 newWindow = pyqtSignal(QUrl, str)
39 newPrivateWindow = pyqtSignal(QUrl, str)
40
41 UrlStringRole = Qt.ItemDataRole.UserRole
42 ErrorDataRole = Qt.ItemDataRole.UserRole + 1
43
44 def __init__(self, parent=None):
45 """
46 Constructor
47
48 @param parent reference to the parent widget (QWidget)
49 """
50 super().__init__(parent)
51 self.setupUi(self)
52 self.setWindowFlags(Qt.WindowType.Window)
53
54 self.__wasShown = False
55 self.__loaded = False
56 self.__feeds = []
57 self.__replies = {}
58 # dict key is the id of the request object
59 # dict value is a tuple of request and tree item
60
61 self.feedsTree.setContextMenuPolicy(
62 Qt.ContextMenuPolicy.CustomContextMenu)
63 self.feedsTree.customContextMenuRequested.connect(
64 self.__customContextMenuRequested)
65 self.feedsTree.itemActivated.connect(self.__itemActivated)
66
67 def show(self):
68 """
69 Public slot to show the feeds manager dialog.
70 """
71 super().show()
72
73 if not self.__wasShown:
74 self.__enableButtons()
75 self.on_reloadAllButton_clicked()
76 self.__wasShown = True
77
78 def addFeed(self, urlString, title, icon):
79 """
80 Public method to add a feed.
81
82 @param urlString URL of the feed (string)
83 @param title title of the feed (string)
84 @param icon icon for the feed (QIcon)
85 @return flag indicating a successful addition of the feed (boolean)
86 """
87 if urlString == "":
88 return False
89
90 if not self.__loaded:
91 self.__load()
92
93 # step 1: check, if feed was already added
94 if any(feed[0] == urlString for feed in self.__feeds):
95 return False
96
97 # step 2: add the feed
98 if icon.isNull():
99 icon = UI.PixmapCache.getIcon("rss16")
100 feed = (urlString, title, icon)
101 self.__feeds.append(feed)
102 self.__addFeedItem(feed)
103 self.__save()
104
105 return True
106
107 def __addFeedItem(self, feed):
108 """
109 Private slot to add a top level feed item.
110
111 @param feed tuple containing feed info (URL, title, icon)
112 (string, string, QIcon)
113 """
114 itm = QTreeWidgetItem(self.feedsTree, [feed[1]])
115 itm.setIcon(0, feed[2])
116 itm.setData(0, FeedsManager.UrlStringRole, feed[0])
117
118 def __load(self):
119 """
120 Private method to load the feeds data.
121 """
122 self.__feeds = Preferences.getWebBrowser("RssFeeds")
123 self.__loaded = True
124
125 # populate the feeds tree top level with the feeds
126 self.feedsTree.clear()
127 for feed in self.__feeds:
128 self.__addFeedItem(feed)
129
130 def __save(self):
131 """
132 Private method to store the feeds data.
133 """
134 if not self.__loaded:
135 self.__load()
136
137 Preferences.setWebBrowser("RssFeeds", self.__feeds)
138
139 @pyqtSlot()
140 def on_reloadAllButton_clicked(self):
141 """
142 Private slot to reload all feeds.
143 """
144 if not self.__loaded:
145 self.__load()
146
147 for index in range(self.feedsTree.topLevelItemCount()):
148 itm = self.feedsTree.topLevelItem(index)
149 self.__reloadFeed(itm)
150
151 @pyqtSlot()
152 def on_reloadButton_clicked(self):
153 """
154 Private slot to reload the selected feed.
155 """
156 itm = self.feedsTree.selectedItems()[0]
157 self.__reloadFeed(itm)
158
159 @pyqtSlot()
160 def on_editButton_clicked(self):
161 """
162 Private slot to edit the selected feed.
163 """
164 itm = self.feedsTree.selectedItems()[0]
165 origTitle = itm.text(0)
166 origUrlString = itm.data(0, FeedsManager.UrlStringRole)
167
168 feedToChange = None
169 for feed in self.__feeds:
170 if feed[0] == origUrlString:
171 feedToChange = feed
172 break
173 if feedToChange:
174 feedIndex = self.__feeds.index(feedToChange)
175
176 from .FeedEditDialog import FeedEditDialog
177 dlg = FeedEditDialog(origUrlString, origTitle)
178 if dlg.exec() == QDialog.DialogCode.Accepted:
179 urlString, title = dlg.getData()
180 for feed in self.__feeds:
181 if feed[0] == urlString:
182 EricMessageBox.critical(
183 self,
184 self.tr("Duplicate Feed URL"),
185 self.tr(
186 """A feed with the URL {0} exists already."""
187 """ Aborting...""".format(urlString)))
188 return
189
190 self.__feeds[feedIndex] = (urlString, title, feedToChange[2])
191 self.__save()
192
193 itm.setText(0, title)
194 itm.setData(0, FeedsManager.UrlStringRole, urlString)
195 self.__reloadFeed(itm)
196
197 @pyqtSlot()
198 def on_deleteButton_clicked(self):
199 """
200 Private slot to delete the selected feed.
201 """
202 itm = self.feedsTree.selectedItems()[0]
203 title = itm.text(0)
204 res = EricMessageBox.yesNo(
205 self,
206 self.tr("Delete Feed"),
207 self.tr(
208 """<p>Do you really want to delete the feed"""
209 """ <b>{0}</b>?</p>""".format(title)))
210 if res:
211 urlString = itm.data(0, FeedsManager.UrlStringRole)
212 if urlString:
213 feedToDelete = None
214 for feed in self.__feeds:
215 if feed[0] == urlString:
216 feedToDelete = feed
217 break
218 if feedToDelete:
219 self.__feeds.remove(feedToDelete)
220 self.__save()
221
222 index = self.feedsTree.indexOfTopLevelItem(itm)
223 if index != -1:
224 self.feedsTree.takeTopLevelItem(index)
225 del itm
226
227 @pyqtSlot()
228 def on_feedsTree_itemSelectionChanged(self):
229 """
230 Private slot to enable the various buttons depending on the selection.
231 """
232 self.__enableButtons()
233
234 def __enableButtons(self):
235 """
236 Private slot to disable/enable various buttons.
237 """
238 selItems = self.feedsTree.selectedItems()
239 enable = (len(selItems) == 1 and
240 self.feedsTree.indexOfTopLevelItem(selItems[0]) != -1)
241
242 self.reloadButton.setEnabled(enable)
243 self.editButton.setEnabled(enable)
244 self.deleteButton.setEnabled(enable)
245
246 def __reloadFeed(self, itm):
247 """
248 Private method to reload the given feed.
249
250 @param itm feed item to be reloaded (QTreeWidgetItem)
251 """
252 urlString = itm.data(0, FeedsManager.UrlStringRole)
253 if urlString == "":
254 return
255
256 for child in itm.takeChildren():
257 del child
258
259 from WebBrowser.WebBrowserWindow import WebBrowserWindow
260 request = QNetworkRequest(QUrl(urlString))
261 reply = WebBrowserWindow.networkManager().get(request)
262 reply.finished.connect(lambda: self.__feedLoaded(reply))
263 self.__replies[id(reply)] = (reply, itm)
264
265 def __feedLoaded(self, reply):
266 """
267 Private slot to extract the loaded feed data.
268
269 @param reply reference to the network reply
270 @type QNetworkReply
271 """
272 if id(reply) not in self.__replies:
273 return
274
275 topItem = self.__replies[id(reply)][1]
276 del self.__replies[id(reply)]
277
278 if reply.error() == QNetworkReply.NetworkError.NoError:
279 linkString = ""
280 titleString = ""
281
282 xml = QXmlStreamReader()
283 xmlData = reply.readAll()
284 xml.addData(xmlData)
285
286 while not xml.atEnd():
287 xml.readNext()
288 if xml.isStartElement():
289 if xml.name() == "item":
290 linkString = xml.attributes().value("rss:about")
291 elif xml.name() == "link":
292 linkString = xml.attributes().value("href")
293 currentTag = xml.name()
294 elif xml.isEndElement():
295 if xml.name() in ["item", "entry"]:
296 itm = QTreeWidgetItem(topItem)
297 itm.setText(0, titleString)
298 itm.setData(0, FeedsManager.UrlStringRole, linkString)
299 itm.setIcon(0, UI.PixmapCache.getIcon("rss16"))
300
301 linkString = ""
302 titleString = ""
303 elif xml.isCharacters() and not xml.isWhitespace():
304 if currentTag == "title":
305 titleString = xml.text()
306 elif currentTag == "link":
307 linkString += xml.text()
308
309 if topItem.childCount() == 0:
310 itm = QTreeWidgetItem(topItem)
311 itm.setText(0, self.tr("Error fetching feed"))
312 itm.setData(0, FeedsManager.UrlStringRole, "")
313 itm.setData(0, FeedsManager.ErrorDataRole,
314 str(xmlData, encoding="utf-8"))
315
316 topItem.setExpanded(True)
317 else:
318 linkString = ""
319 titleString = reply.errorString()
320 itm = QTreeWidgetItem(topItem)
321 itm.setText(0, titleString)
322 itm.setData(0, FeedsManager.UrlStringRole, linkString)
323 topItem.setExpanded(True)
324
325 def __customContextMenuRequested(self, pos):
326 """
327 Private slot to handle the context menu request for the feeds tree.
328
329 @param pos position the context menu was requested (QPoint)
330 """
331 itm = self.feedsTree.currentItem()
332 if itm is None:
333 return
334
335 if self.feedsTree.indexOfTopLevelItem(itm) != -1:
336 return
337
338 urlString = itm.data(0, FeedsManager.UrlStringRole)
339 if urlString:
340 menu = QMenu()
341 menu.addAction(
342 self.tr("&Open"), self.__openMessageInCurrentTab)
343 menu.addAction(
344 self.tr("Open in New &Tab"), self.__openMessageInNewTab)
345 menu.addAction(
346 self.tr("Open in New &Background Tab"),
347 self.__openMessageInNewBackgroundTab)
348 menu.addAction(
349 self.tr("Open in New &Window"), self.__openMessageInNewWindow)
350 menu.addAction(
351 self.tr("Open in New Pri&vate Window"),
352 self.__openMessageInPrivateWindow)
353 menu.addSeparator()
354 menu.addAction(self.tr("&Copy URL to Clipboard"),
355 self.__copyUrlToClipboard)
356 menu.exec(QCursor.pos())
357 else:
358 errorString = itm.data(0, FeedsManager.ErrorDataRole)
359 if errorString:
360 menu = QMenu()
361 menu.addAction(
362 self.tr("&Show error data"), self.__showError)
363 menu.exec(QCursor.pos())
364
365 def __itemActivated(self, itm, column):
366 """
367 Private slot to handle the activation of an item.
368
369 @param itm reference to the activated item (QTreeWidgetItem)
370 @param column column of the activation (integer)
371 """
372 if self.feedsTree.indexOfTopLevelItem(itm) != -1:
373 return
374
375 if (
376 QApplication.keyboardModifiers() &
377 Qt.KeyboardModifier.ControlModifier
378 ):
379 self.__openMessageInNewTab()
380 elif (
381 QApplication.keyboardModifiers() &
382 Qt.KeyboardModifier.ShiftModifier
383 ):
384 self.__openMessageInNewWindow()
385 else:
386 self.__openMessageInCurrentTab()
387
388 def __openMessageInCurrentTab(self):
389 """
390 Private slot to open a feed message in the current browser tab.
391 """
392 self.__openMessage()
393
394 def __openMessageInNewTab(self):
395 """
396 Private slot to open a feed message in a new browser tab.
397 """
398 self.__openMessage(newTab=True)
399
400 def __openMessageInNewBackgroundTab(self):
401 """
402 Private slot to open a feed message in a new background tab.
403 """
404 self.__openMessage(newTab=True, background=True)
405
406 def __openMessageInNewWindow(self):
407 """
408 Private slot to open a feed message in a new browser window.
409 """
410 self.__openMessage(newWindow=True)
411
412 def __openMessageInPrivateWindow(self):
413 """
414 Private slot to open a feed message in a new private browser window.
415 """
416 self.__openMessage(newWindow=True, privateWindow=True)
417
418 def __openMessage(self, newTab=False, background=False,
419 newWindow=False, privateWindow=False):
420 """
421 Private method to open a feed message.
422
423 @param newTab flag indicating to open the feed message in a new tab
424 @type bool
425 @param background flag indicating to open the bookmark in a new
426 background tab
427 @type bool
428 @param newWindow flag indicating to open the bookmark in a new window
429 @type bool
430 @param privateWindow flag indicating to open the bookmark in a new
431 private window
432 @type bool
433 """
434 itm = self.feedsTree.currentItem()
435 if itm is None:
436 return
437
438 urlString = itm.data(0, FeedsManager.UrlStringRole)
439 if urlString:
440 title = itm.text(0)
441
442 if newTab:
443 if background:
444 self.newBackgroundTab.emit(QUrl(urlString), title)
445 else:
446 self.newTab.emit(QUrl(urlString), title)
447 elif newWindow:
448 if privateWindow:
449 self.newPrivateWindow.emit(QUrl(urlString), title)
450 else:
451 self.newWindow.emit(QUrl(urlString), title)
452 else:
453 self.openUrl.emit(QUrl(urlString), title)
454 else:
455 errorString = itm.data(0, FeedsManager.ErrorDataRole)
456 if errorString:
457 self.__showError()
458
459 def __copyUrlToClipboard(self):
460 """
461 Private slot to copy the URL of the selected item to the clipboard.
462 """
463 itm = self.feedsTree.currentItem()
464 if itm is None:
465 return
466
467 if self.feedsTree.indexOfTopLevelItem(itm) != -1:
468 return
469
470 urlString = itm.data(0, FeedsManager.UrlStringRole)
471 if urlString:
472 QApplication.clipboard().setText(urlString)
473
474 def __showError(self):
475 """
476 Private slot to show error info for a failed load operation.
477 """
478 itm = self.feedsTree.currentItem()
479 if itm is None:
480 return
481
482 errorStr = itm.data(0, FeedsManager.ErrorDataRole)
483 if errorStr:
484 EricMessageBox.critical(
485 self,
486 self.tr("Error loading feed"),
487 "{0}".format(errorStr))

eric ide

mercurial