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