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