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