|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2016 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 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(): |
|
98 icon = UI.PixmapCache.getIcon("rss16.png") |
|
99 feed = (urlString, title, icon) |
|
100 self.__feeds.append(feed) |
|
101 self.__addFeedItem(feed) |
|
102 self.__save() |
|
103 |
|
104 return True |
|
105 |
|
106 def __addFeedItem(self, feed): |
|
107 """ |
|
108 Private slot to add a top level feed item. |
|
109 |
|
110 @param feed tuple containing feed info (URL, title, icon) |
|
111 (string, string, QIcon) |
|
112 """ |
|
113 itm = QTreeWidgetItem(self.feedsTree, [feed[1]]) |
|
114 itm.setIcon(0, feed[2]) |
|
115 itm.setData(0, FeedsManager.UrlStringRole, feed[0]) |
|
116 |
|
117 def __load(self): |
|
118 """ |
|
119 Private method to load the feeds data. |
|
120 """ |
|
121 self.__feeds = Preferences.getWebBrowser("RssFeeds") |
|
122 self.__loaded = True |
|
123 |
|
124 # populate the feeds tree top level with the feeds |
|
125 self.feedsTree.clear() |
|
126 for feed in self.__feeds: |
|
127 self.__addFeedItem(feed) |
|
128 |
|
129 def __save(self): |
|
130 """ |
|
131 Private method to store the feeds data. |
|
132 """ |
|
133 if not self.__loaded: |
|
134 self.__load() |
|
135 |
|
136 Preferences.setWebBrowser("RssFeeds", self.__feeds) |
|
137 |
|
138 @pyqtSlot() |
|
139 def on_reloadAllButton_clicked(self): |
|
140 """ |
|
141 Private slot to reload all feeds. |
|
142 """ |
|
143 if not self.__loaded: |
|
144 self.__load() |
|
145 |
|
146 for index in range(self.feedsTree.topLevelItemCount()): |
|
147 itm = self.feedsTree.topLevelItem(index) |
|
148 self.__reloadFeed(itm) |
|
149 |
|
150 @pyqtSlot() |
|
151 def on_reloadButton_clicked(self): |
|
152 """ |
|
153 Private slot to reload the selected feed. |
|
154 """ |
|
155 itm = self.feedsTree.selectedItems()[0] |
|
156 self.__reloadFeed(itm) |
|
157 |
|
158 @pyqtSlot() |
|
159 def on_editButton_clicked(self): |
|
160 """ |
|
161 Private slot to edit the selected feed. |
|
162 """ |
|
163 itm = self.feedsTree.selectedItems()[0] |
|
164 origTitle = itm.text(0) |
|
165 origUrlString = itm.data(0, FeedsManager.UrlStringRole) |
|
166 |
|
167 feedToChange = None |
|
168 for feed in self.__feeds: |
|
169 if feed[0] == origUrlString: |
|
170 feedToChange = feed |
|
171 break |
|
172 if feedToChange: |
|
173 feedIndex = self.__feeds.index(feedToChange) |
|
174 |
|
175 from .FeedEditDialog import FeedEditDialog |
|
176 dlg = FeedEditDialog(origUrlString, origTitle) |
|
177 if dlg.exec_() == QDialog.Accepted: |
|
178 urlString, title = dlg.getData() |
|
179 for feed in self.__feeds: |
|
180 if feed[0] == urlString: |
|
181 E5MessageBox.critical( |
|
182 self, |
|
183 self.tr("Duplicate Feed URL"), |
|
184 self.tr( |
|
185 """A feed with the URL {0} exists already.""" |
|
186 """ Aborting...""".format(urlString))) |
|
187 return |
|
188 |
|
189 self.__feeds[feedIndex] = (urlString, title, feedToChange[2]) |
|
190 self.__save() |
|
191 |
|
192 itm.setText(0, title) |
|
193 itm.setData(0, FeedsManager.UrlStringRole, urlString) |
|
194 self.__reloadFeed(itm) |
|
195 |
|
196 @pyqtSlot() |
|
197 def on_deleteButton_clicked(self): |
|
198 """ |
|
199 Private slot to delete the selected feed. |
|
200 """ |
|
201 itm = self.feedsTree.selectedItems()[0] |
|
202 title = itm.text(0) |
|
203 res = E5MessageBox.yesNo( |
|
204 self, |
|
205 self.tr("Delete Feed"), |
|
206 self.tr( |
|
207 """<p>Do you really want to delete the feed""" |
|
208 """ <b>{0}</b>?</p>""".format(title))) |
|
209 if res: |
|
210 urlString = itm.data(0, FeedsManager.UrlStringRole) |
|
211 if urlString: |
|
212 feedToDelete = None |
|
213 for feed in self.__feeds: |
|
214 if feed[0] == urlString: |
|
215 feedToDelete = feed |
|
216 break |
|
217 if feedToDelete: |
|
218 self.__feeds.remove(feedToDelete) |
|
219 self.__save() |
|
220 |
|
221 index = self.feedsTree.indexOfTopLevelItem(itm) |
|
222 if index != -1: |
|
223 self.feedsTree.takeTopLevelItem(index) |
|
224 del itm |
|
225 |
|
226 @pyqtSlot() |
|
227 def on_feedsTree_itemSelectionChanged(self): |
|
228 """ |
|
229 Private slot to enable the various buttons depending on the selection. |
|
230 """ |
|
231 self.__enableButtons() |
|
232 |
|
233 def __enableButtons(self): |
|
234 """ |
|
235 Private slot to disable/enable various buttons. |
|
236 """ |
|
237 selItems = self.feedsTree.selectedItems() |
|
238 if len(selItems) == 1 and \ |
|
239 self.feedsTree.indexOfTopLevelItem(selItems[0]) != -1: |
|
240 enable = True |
|
241 else: |
|
242 enable = False |
|
243 |
|
244 self.reloadButton.setEnabled(enable) |
|
245 self.editButton.setEnabled(enable) |
|
246 self.deleteButton.setEnabled(enable) |
|
247 |
|
248 def __reloadFeed(self, itm): |
|
249 """ |
|
250 Private method to reload the given feed. |
|
251 |
|
252 @param itm feed item to be reloaded (QTreeWidgetItem) |
|
253 """ |
|
254 urlString = itm.data(0, FeedsManager.UrlStringRole) |
|
255 if urlString == "": |
|
256 return |
|
257 |
|
258 for child in itm.takeChildren(): |
|
259 del child |
|
260 |
|
261 from WebBrowser.WebBrowserWindow import WebBrowserWindow |
|
262 request = QNetworkRequest(QUrl(urlString)) |
|
263 reply = WebBrowserWindow.networkManager().get(request) |
|
264 reply.finished.connect(self.__feedLoaded) |
|
265 self.__replies[id(reply)] = (reply, itm) |
|
266 |
|
267 def __feedLoaded(self): |
|
268 """ |
|
269 Private slot to extract the loaded feed data. |
|
270 """ |
|
271 reply = self.sender() |
|
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.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.png")) |
|
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.addSeparator() |
|
346 menu.addAction(self.tr("&Copy URL to Clipboard"), |
|
347 self.__copyUrlToClipboard) |
|
348 menu.exec_(QCursor.pos()) |
|
349 else: |
|
350 errorString = itm.data(0, FeedsManager.ErrorDataRole) |
|
351 if errorString: |
|
352 menu = QMenu() |
|
353 menu.addAction( |
|
354 self.tr("&Show error data"), self.__showError) |
|
355 menu.exec_(QCursor.pos()) |
|
356 |
|
357 def __itemActivated(self, itm, column): |
|
358 """ |
|
359 Private slot to handle the activation of an item. |
|
360 |
|
361 @param itm reference to the activated item (QTreeWidgetItem) |
|
362 @param column column of the activation (integer) |
|
363 """ |
|
364 if self.feedsTree.indexOfTopLevelItem(itm) != -1: |
|
365 return |
|
366 |
|
367 self.__openMessage( |
|
368 QApplication.keyboardModifiers() & |
|
369 Qt.ControlModifier == Qt.ControlModifier) |
|
370 |
|
371 def __openMessageInCurrentTab(self): |
|
372 """ |
|
373 Private slot to open a feed message in the current browser tab. |
|
374 """ |
|
375 self.__openMessage(False) |
|
376 |
|
377 def __openMessageInNewTab(self): |
|
378 """ |
|
379 Private slot to open a feed message in a new browser tab. |
|
380 """ |
|
381 self.__openMessage(True) |
|
382 |
|
383 def __openMessage(self, newTab): |
|
384 """ |
|
385 Private method to open a feed message. |
|
386 |
|
387 @param newTab flag indicating to open the feed message in a new tab |
|
388 (boolean) |
|
389 """ |
|
390 itm = self.feedsTree.currentItem() |
|
391 if itm is None: |
|
392 return |
|
393 |
|
394 urlString = itm.data(0, FeedsManager.UrlStringRole) |
|
395 if urlString: |
|
396 title = itm.text(0) |
|
397 |
|
398 if newTab: |
|
399 self.newUrl.emit(QUrl(urlString), title) |
|
400 else: |
|
401 self.openUrl.emit(QUrl(urlString), title) |
|
402 else: |
|
403 errorString = itm.data(0, FeedsManager.ErrorDataRole) |
|
404 if errorString: |
|
405 self.__showError() |
|
406 |
|
407 def __copyUrlToClipboard(self): |
|
408 """ |
|
409 Private slot to copy the URL of the selected item to the clipboard. |
|
410 """ |
|
411 itm = self.feedsTree.currentItem() |
|
412 if itm is None: |
|
413 return |
|
414 |
|
415 if self.feedsTree.indexOfTopLevelItem(itm) != -1: |
|
416 return |
|
417 |
|
418 urlString = itm.data(0, FeedsManager.UrlStringRole) |
|
419 if urlString: |
|
420 QApplication.clipboard().setText(urlString) |
|
421 |
|
422 def __showError(self): |
|
423 """ |
|
424 Private slot to show error info for a failed load operation. |
|
425 """ |
|
426 itm = self.feedsTree.currentItem() |
|
427 if itm is None: |
|
428 return |
|
429 |
|
430 errorStr = itm.data(0, FeedsManager.ErrorDataRole) |
|
431 if errorStr: |
|
432 E5MessageBox.critical( |
|
433 self, |
|
434 self.tr("Error loading feed"), |
|
435 "{0}".format(errorStr)) |