Updated Arabic Translation by Djihed Afifi.
[straw.git] / src / lib / FeedListView.py
blob281d6e94e70b34f380bcea65c46809cbcf7795e7
1 """ FeedListView.py
3 Module for displaying the feeds in the Feeds TreeView.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """
7 Straw is free software; you can redistribute it and/or modify it under the
8 terms of the GNU General Public License as published by the Free Software
9 Foundation; either version 2 of the License, or (at your option) any later
10 version.
12 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License along with
17 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 Place - Suite 330, Boston, MA 02111-1307, USA. """
20 import copy
21 import locale
22 import pygtk
23 pygtk.require('2.0')
24 import gobject
25 import gtk
26 import pango
27 import FeedCategoryList
28 from FeedPropertiesDialog import FeedPropertiesDialog
29 import FeedList
30 import Feed
31 import Event
32 import dialogs
33 import Config
34 import PollManager
35 import MVP
37 class Column:
38 NAME = 0
39 UNREAD = 1
40 BOLD = 2
41 STATUS_FLAG = 3
42 STATUS_PIXBUF = 4
43 OBJECT = 5
44 ALLOW_CHILDREN = 6
46 class FeedsView(MVP.WidgetView):
47 popup_ui = """
48 <ui>
49 <popup name=\"feed_list_popup\">
50 <menuitem action=\"refresh\"/>
51 <menuitem action=\"mark_as_read\"/>
52 <menuitem action=\"stop_refresh\"/>
53 <menuitem action=\"remove\"/>
54 <separator/>
55 <menu name=\"Sort\" action=\"category-sort\">
56 <menuitem action=\"ascending\"/>
57 <menuitem action=\"descending\"/>
58 </menu>
59 <menuitem action=\"properties\"/>
60 </popup>
61 </ui>
62 """
63 def _initialize(self):
64 self._popup = None
65 self._widget.set_rules_hint(False)
66 self._widget.set_search_column(Column.NAME)
67 self._create_columns()
68 selection = self._widget.get_selection()
69 selection.connect("changed", self._feed_selection_changed, Column.OBJECT)
70 self._widget.connect("button_press_event", self._on_button_press_event)
71 self._widget.connect("popup-menu", self._on_popup_menu)
72 self._create_popup()
74 def _create_columns(self):
75 # hide the expander. This should remove the extra space at the left
76 # of the the treeview.
77 expander = gtk.TreeViewColumn()
78 expander.set_visible(False)
79 self._widget.append_column(expander)
80 self._widget.set_expander_column(expander)
82 column = gtk.TreeViewColumn()
83 status_renderer = gtk.CellRendererPixbuf()
84 column.pack_start(status_renderer, False)
85 column.set_attributes(status_renderer,
86 stock_id=Column.STATUS_PIXBUF,
87 visible=Column.STATUS_FLAG)
88 unread_renderer = gtk.CellRendererText()
89 column.pack_start(unread_renderer, False)
90 column.set_attributes(unread_renderer,
91 text=Column.UNREAD, weight=Column.BOLD)
92 column.set_title("_Subscriptions")
93 title_renderer = gtk.CellRendererText()
94 column.pack_end(title_renderer, False)
95 column.set_attributes(title_renderer,
96 text=Column.NAME, weight=Column.BOLD)
97 self._widget.append_column(column)
98 return
100 def _create_popup(self):
101 actions = [
102 ("refresh", gtk.STOCK_REFRESH, _("_Refresh"), None,
103 _("Update this feed"), self._on_menu_poll_selected_activate),
104 ("mark_as_read", gtk.STOCK_CLEAR, _("_Mark As Read"), None,
105 _("Mark all items in this feed as read"), self._on_menu_mark_all_as_read_activate),
106 ("stop_refresh", None, _("_Stop Refresh"), None,
107 _("Stop updating this feed"), self._on_menu_stop_poll_selected_activate),
108 ("remove", None, _("Remo_ve Feed"), None,
109 _("Remove this feed from my subscription"), self._remove_selected_feed),
110 ("category-sort",None,_("_Arrange Feeds"), None,
111 _("Sort the current category")),
112 ("ascending", gtk.STOCK_SORT_ASCENDING, _("Alpha_betical Order"), None,
113 _("Sort in alphabetical order"), self._sort_ascending),
114 ("descending", gtk.STOCK_SORT_DESCENDING, _("Re_verse Order"), None,
115 _("Sort in reverse order"), self._sort_descending),
116 ("properties", gtk.STOCK_INFO, _("_Information"), None,
117 _("Feed-specific properties"), self._display_properties_feed)
119 ag = gtk.ActionGroup('FeedListPopupActions')
120 ag.add_actions(actions)
121 uimanager = gtk.UIManager()
122 uimanager.insert_action_group(ag,0)
123 uimanager.add_ui_from_string(FeedsView.popup_ui)
124 self._popup = uimanager.get_widget('/feed_list_popup')
125 return
127 def _model_set(self):
128 self._widget.set_model(self._model)
130 def get_selection(self):
131 return self._widget.get_selection()
133 def _on_popup_menu(self, treeview, *args):
134 self._popup.popup(None, None, None, 0, 0)
136 def _on_button_press_event(self, treeview, event):
137 retval = 0
138 if event.button == 3:
139 x = int(event.x)
140 y = int(event.y)
141 time = gtk.get_current_event_time()
142 path = treeview.get_path_at_pos(x, y)
143 if path is None:
144 return 1
145 path, col, cellx, celly = path
146 treeview.grab_focus()
147 #treeview.set_cursor( path, col, 0)
148 self._popup.popup(None, None, None, event.button, time)
149 retval = 1
150 return retval
152 def _on_menu_poll_selected_activate(self, *args):
153 self._presenter.poll_current_feed()
155 def _on_menu_stop_poll_selected_activate(self, *args):
156 self._presenter.stop_polling_current_feed()
158 def _on_menu_mark_all_as_read_activate(self, *args):
159 self._presenter.mark_current_feed_as_read()
161 def _remove_selected_feed(self, *args):
162 title = self._presenter.get_selected_feed().title
163 response = dialogs.confirm_delete(_("Delete %s?") % title,
164 _("Deleting %s will remove it from your subscription list.") % title)
165 if (response == gtk.RESPONSE_OK):
166 selection = self._widget.get_selection()
167 self._presenter.remove_selected_feed()
168 return
170 def _sort_ascending(self, *args):
171 self._presenter.sort_category()
173 def _sort_descending(self, *args):
174 self._presenter.sort_category(reverse=True)
176 def _display_properties_feed(self, *args):
177 self._presenter.show_feed_properties()
178 return
180 def _feed_selection_changed(self, selection, column):
182 Called when the current feed selection changed
184 model, rowiter = selection.get_selected()
185 if not rowiter:
186 return
187 adapter = model.get_value(rowiter, column)
188 if adapter:
189 self._presenter.feed_selection_changed(adapter.feed)
190 return
192 def _on_feed_selection_treeview_row_expanded(self, widget,iter,path,*data):
193 obj = self._model[path][Column.OBJECT]
194 self._presenter.expand_row(obj)
196 def _on_feed_selection_treeview_row_collapsed(self, widget,iter,path,*data):
197 obj = self._model[path][Column.OBJECT]
198 self._presenter.collapse_row(obj)
200 def set_cursor(self, treeiter, col_id=None, edit=False):
201 path = self._model.get_path(treeiter)
202 if col_id:
203 column = self._widget.get_column(col_id)
204 else:
205 column = None
206 self._widget.set_cursor(path, column, edit)
207 self._widget.scroll_to_cell(path, column)
208 self._widget.grab_focus()
209 return
211 def queue_draw(self):
212 self._widget.queue_draw()
213 return
216 def get_location(self):
217 model, iter = self._widget.get_selection().get_selected()
218 if iter is None:
219 return (None, None)
220 path = model.get_path(iter)
221 return self.get_parent_with_path(FeedList.get_instance(), path)
223 class FeedsPresenter(MVP.BasicPresenter):
224 def _initialize(self):
225 self.initialize_slots(Event.FeedSelectionChangedSignal,
226 Event.FeedsEmptySignal)
227 model = gtk.TreeStore(
228 gobject.TYPE_STRING, gobject.TYPE_STRING,
229 gobject.TYPE_INT, gobject.TYPE_BOOLEAN,
230 gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
231 gobject.TYPE_BOOLEAN)
232 self.model = model
233 self._init_signals()
234 self._curr_feed = None
235 self._curr_category = None
236 return
238 def _init_signals(self):
239 flist = FeedList.get_instance()
240 flist.signal_connect(Event.ItemReadSignal,
241 self._feed_item_read)
242 flist.signal_connect(Event.AllItemsReadSignal,
243 self._feed_all_items_read)
244 flist.signal_connect(Event.FeedsChangedSignal,
245 self._feeds_changed)
246 flist.signal_connect(Event.FeedDetailChangedSignal,
247 self._feed_detail_changed)
248 fclist = FeedCategoryList.get_instance()
249 fclist.signal_connect(Event.FeedCategorySortedSignal,
250 self._feeds_sorted_cb)
251 fclist.signal_connect(Event.FeedCategoryChangedSignal,
252 self._fcategory_changed_cb)
254 def _sort_func(self, model, a, b):
256 Sorts the feeds lexically.
258 From the gtk.TreeSortable.set_sort_func doc:
260 The comparison callback should return -1 if the iter1 row should come before
261 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
262 after the iter2 row.
264 retval = 0
265 fa = model.get_value(a, Column.OBJECT)
266 fb = model.get_value(b, Column.OBJECT)
268 if fa and fb:
269 retval = locale.strcoll(fa.title, fb.title)
270 elif fa is not None: retval = -1
271 elif fb is not None: retval = 1
272 return retval
274 def poll_current_feed(self):
275 config = Config.get_instance()
276 poll = False
277 if config.offline:
278 response = dialogs.report_offline_status()
279 if response == gtk.RESPONSE_OK:
280 config.offline = not config.offline
281 poll = True
282 else:
283 poll = True
284 if poll:
285 PollManager.get_instance().poll([self._curr_feed])
286 return
288 def get_selected_feed(self):
289 return self._curr_feed
291 def stop_polling_current_feed(self):
292 self._curr_feed.router.stop_polling()
294 def mark_current_feed_as_read(self):
295 self._curr_feed.mark_all_read()
297 def remove_selected_feed(self):
298 selection = self._view.get_selection()
299 model, rowiter = selection.get_selected()
300 if rowiter:
301 adapter = model.get_value(rowiter, Column.OBJECT)
302 it = model.remove(rowiter)
303 feedlist = FeedList.get_instance()
304 idx = feedlist.index(adapter.feed)
305 del feedlist[idx]
306 return
308 def sort_category(self, reverse=False):
309 self._curr_category.sort()
310 if reverse:
311 self._curr_category.reverse()
312 return
314 def show_feed_properties(self):
315 fpd = FeedPropertiesDialog.show_feed_properties(None, self._curr_feed)
316 return
318 def select_first_feed(self):
319 treeiter = self._model.get_iter_first()
320 if not treeiter or not self._model.iter_is_valid(treeiter):
321 return False
322 self._view.set_cursor(treeiter)
323 return True
325 def select_next_feed(self):
327 Scrolls to the next feed in the feed list
329 selection = self._view.get_selection()
330 model, treeiter = selection.get_selected()
331 if not treeiter:
332 treeiter = model.get_iter_first()
333 next_feed_iter = model.iter_next(treeiter)
334 if not next_feed_iter or not self._model.iter_is_valid(next_feed_iter):
335 return False
336 self._view.set_cursor(next_feed_iter)
337 return True
339 def select_previous_feed(self):
341 Scrolls to the previous feeds in the feed list.
343 selection = self._view.get_selection()
344 model, treeiter = selection.get_selected()
345 if not treeiter:
346 treeiter = model.get_iter_first()
347 path = model.get_path(treeiter)
348 # check if there's a feed in the path
349 if not path:
350 return False
351 prev_path = path[-1]-1
352 if prev_path < 0:
353 # go to the last feed in the list
354 prev_path = len(self._model) - 1
355 self._view.set_cursor(self._model.get_iter(prev_path))
356 return True
358 def select_next_unread_feed(self):
359 has_unread = False
360 mark_treerow = 1
361 treerow = self._model[0]
362 selection = self._view.get_selection()
363 srow = selection.get_selected()
364 if srow:
365 model, treeiter = srow
366 nextiter = model.iter_next(treeiter)
367 if nextiter:
368 treerow = self._model[model.get_path(nextiter)]
369 while(treerow):
370 feedrow = treerow[Column.OBJECT]
371 if feedrow.feed.n_items_unread:
372 self._view.set_cursor(treerow.iter)
373 has_unread = True
374 break
375 treerow = treerow.next
376 if not treerow and mark_treerow:
377 # should only do this once.
378 mark_treerow = treerow
379 treerow = self._model[0]
380 return has_unread
382 def feed_selection_changed(self, feed):
384 Called when the current feed selection was changed.
386 This is called everytime a 'changed' signal in the treeview occurs
388 oldfeed = self._curr_feed
389 if feed:
390 self._curr_feed = feed
391 else:
392 self._curr_feed = None
393 if oldfeed is not self._curr_feed:
394 self.emit_signal(Event.FeedSelectionChangedSignal(self,oldfeed,self._curr_feed))
395 return
397 def display_feed(self, feed):
398 # set_cursor will emit a 'changed' event in the treeview
399 # and then feed_selection_changed above will be called.
400 path = self._curr_category.index_feed(feed)
401 treeiter = self._model.get_iter(path)
402 self._view.set_cursor(treeiter, Column.NAME)
404 def display_category_feeds(self, category):
405 self._curr_category = category
406 feeds = self._curr_category.feeds
407 self._model.foreach(self._disconnect)
408 self._model.clear()
409 curr_feed_iter = self._display_feeds(feeds)
410 if curr_feed_iter:
411 self._view.set_cursor(curr_feed_iter)
412 else:
413 it = self._model.get_iter_first()
414 if it:
415 self._view.set_cursor(it)
416 else:
417 self.emit_signal(Event.FeedsEmptySignal(self))
418 return
420 def _display_feeds(self, feeds, parent=None):
421 def _connect_adapter(adapter, feedindex):
422 adapter.signal_connect(Event.ItemsAddedSignal,
423 self._adapter_updated_handler, feedindex)
424 adapter.signal_connect(Event.FeedPolledSignal,
425 self._adapter_updated_handler, feedindex)
426 adapter.signal_connect(Event.FeedStatusChangedSignal,
427 self._adapter_updated_handler, feedindex)
428 adapter.signal_connect(Event.ItemReadSignal,
429 self._adapter_updated_handler, feedindex)
430 adapter.signal_connect(Event.AllItemsReadSignal,
431 self._adapter_updated_handler, feedindex)
432 curr_feed_iter = None
433 for f in feeds:
434 adapter = create_adapter(f)
435 rowiter = self._model.append(parent)
436 self._model.set(rowiter,
437 Column.NAME, adapter.title,
438 Column.OBJECT, adapter)
440 idx = self._curr_category.index_feed(adapter.feed)
441 _connect_adapter(adapter, idx) # we need this to get the rest of the data
442 new_string, new = adapter.unread
443 weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0]
444 status, pixbuf = adapter.status_icon
445 self._model.set(rowiter,
446 Column.UNREAD, new_string,
447 Column.BOLD, weight,
448 Column.STATUS_FLAG, status,
449 Column.STATUS_PIXBUF, pixbuf,
450 Column.ALLOW_CHILDREN, False)
451 if adapter.feed is self._curr_feed:
452 curr_feed_iter = rowiter
453 return curr_feed_iter
455 def display_empty_category(self):
456 self._model.foreach(self._disconnect)
457 self._model.clear()
458 return
460 def _disconnect(self, model, path, iter, user_data=None):
461 ob = model[path][Column.OBJECT]
462 self._disconnect_adapter(ob)
463 if not len(model):
464 return True
465 return False
467 def _disconnect_adapter(self, adapter):
468 adapter.disconnect()
469 adapter.signal_disconnect(Event.ItemsAddedSignal,
470 self._adapter_updated_handler)
471 adapter.signal_disconnect(Event.FeedPolledSignal,
472 self._adapter_updated_handler)
473 adapter.signal_disconnect(Event.FeedStatusChangedSignal,
474 self._adapter_updated_handler)
475 del adapter
477 def _adapter_updated_handler(self, signal, feed_index):
478 self._update_adapter_view(signal.sender, feed_index)
480 def _update_adapter_view(self, adapter, feed_index):
481 new = adapter.unread
482 row = self._model[feed_index]
483 row[Column.NAME] = adapter.title
484 row[Column.UNREAD] = new[0]
485 row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0]
486 row[Column.OBJECT] = adapter
487 status, pixbuf = adapter.status_icon
488 row[Column.STATUS_FLAG] = status
489 if pixbuf:
490 row[Column.STATUS_PIXBUF] = pixbuf
491 self._view.queue_draw()
493 def _feeds_changed(self, signal):
494 self._feed_view_update(signal.feed)
496 def _feed_detail_changed(self, signal):
497 self._feed_view_update(signal.sender)
499 def _feed_item_read(self, signal):
500 path = (0,)
501 selection = self._view.get_selection()
502 selected_row = selection.get_selected()
503 if selected_row:
504 model, treeiter = selected_row
505 path = model.get_path(treeiter)
506 treerow = self._model[path]
507 adapter = treerow[Column.OBJECT]
508 self._update_adapter_view(adapter, path)
510 def _feed_all_items_read(self, signal):
511 pass
513 def _feed_view_update(self, feed):
514 for index, f in enumerate(self._model):
515 adapter = self._model[index][Column.OBJECT]
516 if adapter.feed is feed:
517 self._update_adapter_view(adapter, index)
518 break
519 return
521 def _feeds_sorted_cb(self, signal):
522 self.model.set_sort_func(Column.NAME, self._sort_func)
523 if not signal.descending:
524 self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING)
525 else:
526 self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING)
527 self.model.sort_column_changed()
528 return
530 def _fcategory_changed_cb(self,signal):
531 if signal.sender is self._curr_category:
532 self._curr_category = signal.sender
533 self.display_category_feeds(self._curr_category)
534 return
536 def expand_row(self, obj):
537 obj.open = True
539 def collapse_row(self, obj):
540 obj.open = False
542 def move_feed(self, sidx, tidx):
543 self._curr_category.move_feed(sidx, tidx)
544 return
546 class DisplayAdapter(object, Event.SignalEmitter):
548 View adapter for feeds and categories
550 def __init__(self, ob):
551 self._ob = ob
552 Event.SignalEmitter.__init__(self)
553 self.initialize_slots(Event.ItemReadSignal,
554 Event.ItemsAddedSignal,
555 Event.AllItemsReadSignal,
556 Event.FeedPolledSignal,
557 Event.FeedStatusChangedSignal)
559 def equals(self, ob):
560 return self._ob is ob
562 class FeedDisplayAdapter(DisplayAdapter):
563 """Adapter for displaying Feed objects in the tree"""
564 def __init__(self, ob):
565 DisplayAdapter.__init__(self, ob)
566 ob.signal_connect(Event.ItemReadSignal, self.resend_signal)
567 ob.signal_connect(Event.ItemsAddedSignal, self.resend_signal)
568 ob.signal_connect(Event.AllItemsReadSignal, self.resend_signal)
569 ob.signal_connect(Event.FeedPolledSignal, self.resend_signal)
570 ob.signal_connect(Event.FeedStatusChangedSignal, self.resend_signal)
572 def disconnect(self):
573 self._ob.signal_disconnect(
574 Event.ItemReadSignal, self.resend_signal)
575 self._ob.signal_disconnect(
576 Event.ItemsAddedSignal, self.resend_signal)
577 self._ob.signal_disconnect(
578 Event.AllItemsReadSignal, self.resend_signal)
579 self._ob.signal_disconnect(
580 Event.FeedPolledSignal, self.resend_signal)
581 self._ob.signal_disconnect(
582 Event.FeedStatusChangedSignal, self.resend_signal)
584 def resend_signal(self, signal):
585 new = copy.copy(signal)
586 new.sender = self
587 self.emit_signal(new)
589 @property
590 def title(self):
591 return self._ob.title
593 @property
594 def unread(self):
595 nu = self._ob.n_items_unread
596 if nu != 0:
597 return ("%s" % nu, nu)
598 else:
599 return ("", nu)
601 @property
602 def status_icon(self):
603 if self._ob.process_status is not Feed.Feed.STATUS_IDLE:
604 return (1, gtk.STOCK_EXECUTE)
605 elif self._ob.error:
606 return (1, gtk.STOCK_DIALOG_ERROR)
607 return (0, None)
609 @property
610 def feed(self):
611 return self._ob
613 contents = property(lambda x: None)
614 open = property(lambda x: None)
616 def create_adapter(ob):
617 if isinstance(ob, Feed.Feed):
618 return FeedDisplayAdapter(ob)