3 Module for displaying the feeds in the Feeds TreeView.
5 __copyright__
= "Copyright (c) 2002-2005 Free Software Foundation, Inc."
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
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. """
22 from logging
import debug
, error
, warn
, info
, critical
, exception
29 import FeedCategoryList
30 import FeedPropertiesDialog
41 pixbuf
, name
, foreground
, object = range(4)
45 ''' Abstracts a popup widget '''
47 def __init__(self
, listener
):
48 self
.manager
= gtk
.UIManager()
50 ("refresh", gtk
.STOCK_REFRESH
, _("_Refresh"), None, _("Update this feed"),
51 listener
.on_menu_poll_selected_activate
),
52 ("mark_as_read", gtk
.STOCK_CLEAR
, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
53 listener
.on_menu_mark_all_as_read_activate
),
54 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
55 listener
.on_menu_stop_poll_selected_activate
),
56 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
57 listener
.on_remove_selected_feed
),
58 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
59 ("ascending", gtk
.STOCK_SORT_ASCENDING
, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
60 listener
.on_sort_ascending
),
61 ("descending", gtk
.STOCK_SORT_DESCENDING
, _("Re_verse Order"), None, _("Sort in reverse order"),
62 listener
.on_sort_descending
),
63 ("properties", gtk
.STOCK_INFO
, _("_Information"), None, _("Feed-specific properties"),
64 listener
.on_display_properties_feed
)
66 ag
= gtk
.ActionGroup('FeedListPopupActions')
67 ag
.add_actions(actions
)
68 self
.manager
.insert_action_group(ag
,0)
69 popupui
= os
.path
.join(utils
.find_image_dir(), 'ui.xml')
70 self
.manager
.add_ui_from_file(popupui
)
74 return self
.manager
.get_widget('/feed_list_popup')
78 ''' The model for the feed list view '''
81 # name, pixbuf, unread, foreground
82 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, gobject
.TYPE_PYOBJECT
)
83 # unread, weight, status_flag feed object, allow_children
84 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
88 def __getattribute__(self
, name
):
91 attr
= getattr(self
, name
)
92 except AttributeError, ae
:
93 attr
= getattr(self
.store
, name
)
97 def _init_model(self
):
98 fclist
= FeedCategoryList
.get_instance()
101 # build the user categories and its feeds
102 for category
in fclist
.user_categories
:
103 parent
= self
.store
.append(parent
)
104 node_adapter
= TreeNodeAdapter(category
)
105 self
.store
.set(parent
, Column
.pixbuf
, node_adapter
.pixbuf
,
106 Column
.name
, node_adapter
.title
,
107 Column
.object, node_adapter
)
108 for f
in category
.feeds
:
109 rowiter
= self
.store
.append(parent
)
110 node_adapter
= TreeNodeAdapter(f
)
111 self
.store
.set(rowiter
, Column
.pixbuf
, node_adapter
.pixbuf
,
112 Column
.name
, node_adapter
.title
,
113 Column
.foreground
, self
.get_foreground(node_adapter
.num_unread_items
),
114 Column
.object, node_adapter
) # maybe use create_adapter(f) here?
116 # build the feeds that do not belong to any other category
117 for feed
in fclist
._un
_category
.feeds
:
118 node_adapter
= TreeNodeAdapter(feed
)
119 self
.store
.append(None, [node_adapter
.pixbuf
, node_adapter
.title
,
120 self
.get_foreground(node_adapter
.num_unread_items
),
122 print "finished initializing feed list vie model"
124 def get_foreground(self
, unread
):
125 ''' gets the foreground color according to the number of unread items'''
126 return ('black', 'blue')[(unread
> 0) and 1 or 0]
132 def get_selected_nodes(self
, pathlist
):
133 ''' Returns a list of feed objects that live in the pathlist '''
135 for path
in pathlist
:
136 treeiter
= self
.store
.get_iter(path
)
137 object = self
.store
.get_value(treeiter
, Column
.object)
138 selected
.append(object)
141 class FeedsView(MVP
.WidgetView
):
142 def _initialize(self
):
143 self
._widget
.set_search_column(Column
.name
)
146 column
= gtk
.TreeViewColumn()
147 status_renderer
= gtk
.CellRendererPixbuf()
148 column
.pack_start(status_renderer
, False)
149 column
.set_attributes(status_renderer
,
150 pixbuf
=Column
.pixbuf
)
152 # feed title renderer
153 title_renderer
= gtk
.CellRendererText()
154 column
.pack_start(title_renderer
, False)
155 column
.set_attributes(title_renderer
,
156 foreground
=Column
.foreground
,
157 text
=Column
.name
) #, weight=Column.BOLD)
159 self
._widget
.append_column(column
)
161 selection
= self
._widget
.get_selection()
162 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
164 #selection.connect("changed", self._selection_changed)
166 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
167 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
169 self
._popup
= FeedListPopup(self
).popup
171 def _model_set(self
):
172 self
._widget
.set_model(self
._model
.model
)
174 def add_selection_changed_listener(self
, listener
):
175 selection
= self
._widget
.get_selection()
176 selection
.connect('changed', listener
.feedlist_selection_changed
, self
._model
)
178 def _on_popup_menu(self
, treeview
, *args
):
179 self
._popup
.popup(None, None, None, 0, 0)
182 def foreach_selected(self
, func
):
183 selection
= self
._widget
.get_selection()
184 (model
, pathlist
) = selection
.get_selected_rows()
185 iters
= [model
.get_iter(path
) for path
in pathlist
]
186 for treeiter
in iters
:
187 object = model
.get_value(treeiter
, Column
.object)
188 func(object, model
, treeiter
)
192 def _on_button_press_event(self
, treeview
, event
):
194 if event
.button
== 3:
197 time
= gtk
.get_current_event_time()
198 path
= treeview
.get_path_at_pos(x
, y
)
201 path
, col
, cellx
, celly
= path
202 treeview
.grab_focus()
203 self
._popup
.popup(None, None, None, event
.button
, time
)
207 ### XXX don't do feed related tasks when path is a category
208 ### this applies to every operation I suppose
209 def on_menu_poll_selected_activate(self
, *args
):
210 config
= Config
.get_instance()
213 config
.offline
= not config
.offline
214 # XXX pass 'list' of feeds
215 #PollManager.get_instance().poll([self._curr_feed])
219 def on_menu_stop_poll_selected_activate(self
, *args
):
220 self
.foreach_selected(lambda o
,*args
: o
.router
.stop_polling())
222 def on_menu_mark_all_as_read_activate(self
, *args
):
223 self
.foreach_selected(lambda o
,*args
: o
.mark_all_read())
225 def on_remove_selected_feed(self
, *args
):
227 (object, model
, treeiter
) = args
228 model
.remove(treeiter
)
229 #feedlist = FeedList.get_instance()
230 #idx = feedlist.index(object)
232 self
.foreach_selected(remove
)
236 def on_sort_ascending(self
, *args
):
237 ## XXX this will not work with tree list
238 self
._presenter
.sort_category()
240 def on_sort_descending(self
, *args
):
241 ### XXX this will not work with tree list
242 self
._presenter
.sort_category(reverse
=True)
244 def on_display_properties_feed(self
, *args
):
245 self
._presenter
.show_feed_properties()
248 def select_first_feed(self
):
249 treeiter
= self
._model
.get_iter_first()
250 if not treeiter
or not self
._model
.iter_is_valid(treeiter
):
252 self
._view
.set_cursor(treeiter
)
255 def select_next_feed(self
, unread
=False):
256 ''' Scrolls to the next feed in the feed list
258 If there is no selection, selects the first feed. If multiple feeds
259 are selected, selects the feed after the last selected feed.
261 If unread is True, selects the next unread with unread items.
263 If the selection next-to-be is a category, go to the iter its first
264 child. If current selection is a child, then go to (parent + 1),
265 provided that (parent + 1) is not a category.
267 def next(model
, current
):
268 treeiter
= model
.iter_next(current
)
269 if not treeiter
and model
.iter_depth(current
):
270 next(model
, model
.iter_parent(current
))
271 self
.set_cursor(treeiter
)
272 selection
= self
._widget
.get_selection()
273 (model
, pathlist
) = selection
.get_selected_rows()
274 iters
= [model
.get_iter(path
) for path
in pathlist
]
276 current
= iters
.pop()
277 if model
.iter_has_child(current
):
278 iterchild
= model
.iter_children(current
)
279 # make the row visible
280 path
= model
.get_path(iterchild
)
281 for i
in range(len(path
)):
282 self
._widget
.expand_row(path
[:i
+1], False)
283 self
.set_cursor(iterchild
)
287 self
.set_cursor(model
.get_iter_first())
289 def select_previous_feed(self
):
290 ''' Scrolls to the previous feed in the feed list.
292 If there is no selection, selects the first feed. If there's multiple
293 selection, selects the feed before the first selected feed.
295 If the previous selection is a category, select the last node in that
296 category. If the current selection is a child, then go to (parent -
297 1). If parent is the first feed, wrap and select the last feed or
298 category in the list.
300 def previous(model
, current
):
301 path
= model
.get_path(current
)
303 #path_prev = path[:path_len
304 print "\tpath is -> %s , prev_path -> %s", (path
, prev_path
)
305 treeiter
= model
.get_iter(prev_path
)
306 self
.set_cursor(treeiter
)
307 selection
= self
._widget
.get_selection()
308 (model
, pathlist
) = selection
.get_selected_rows()
309 iters
= [model
.get_iter(path
) for path
in pathlist
]
311 current
= iters
.pop(0)
312 if model
.iter_has_child(current
):
313 kids
= model
.iter_n_children(current
)
314 iter = model
.iter_nth_child(kids
- 1)
315 self
.set_cursor(iter)
317 previous(model
, current
)
319 self
.set_cursor(model
.get_iter_first())
321 def select_next_unread_feed(self
):
324 treerow
= self
._model
[0]
325 selection
= self
._view
.get_selection()
326 srow
= selection
.get_selected()
328 model
, treeiter
= srow
329 nextiter
= model
.iter_next(treeiter
)
331 treerow
= self
._model
[model
.get_path(nextiter
)]
333 feedrow
= treerow
[Column
.OBJECT
]
334 if feedrow
.feed
.n_items_unread
:
335 self
._view
.set_cursor(treerow
.iter)
338 treerow
= treerow
.next
339 if not treerow
and mark_treerow
:
340 # should only do this once.
341 mark_treerow
= treerow
342 treerow
= self
._model
[0]
346 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
350 path
= self
._model
.model
.get_path(treeiter
)
352 column
= self
._widget
.get_column(col_id
)
353 self
._widget
.set_cursor(path
, column
, edit
)
354 self
._widget
.scroll_to_cell(path
, column
)
355 self
._widget
.grab_focus()
358 class FeedsPresenter(MVP
.BasicPresenter
):
359 def _initialize(self
):
360 self
.model
= FeedListModel()
361 # self._init_signals()
364 def _init_signals(self
):
365 flist
= feeds
.get_instance()
366 #flist.signal_connect(Event.ItemReadSignal,
367 # self._feed_item_read)
368 #flist.signal_connect(Event.AllItemsReadSignal,
369 # self._feed_all_items_read)
370 #flist.signal_connect(Event.FeedsChangedSignal,
371 # self._feeds_changed)
372 #flist.signal_connect(Event.FeedDetailChangedSignal,
373 # self._feed_detail_changed)
374 #fclist = FeedCategoryList.get_instance()
375 #fclist.signal_connect(Event.FeedCategorySortedSignal,
376 # self._feeds_sorted_cb)
377 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
378 # self._fcategory_changed_cb)
380 def select_next_feed(self
, unread
=False):
381 self
.view
.select_next_feed(unread
)
383 def select_previous_feed(self
):
384 self
.view
.select_previous_feed()
386 # def _sort_func(self, model, a, b):
388 Sorts the feeds lexically.
390 From the gtk.TreeSortable.set_sort_func doc:
392 The comparison callback should return -1 if the iter1 row should come before
393 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
397 fa = model.get_value(a, Column.OBJECT)
398 fb = model.get_value(b, Column.OBJECT)
401 retval = locale.strcoll(fa.title, fb.title)
402 elif fa is not None: retval = -1
403 elif fb is not None: retval = 1
406 #def sort_category(self, reverse=False):
407 # self._curr_category.sort()
409 # self._curr_category.reverse()
412 def show_feed_properties(self):
413 FeedPropertiesDialog.show_feed_properties(None, self._curr_feed)
417 def _display_feeds(self, feeds, parent=None):
418 def _connect_adapter(adapter, feedindex):
419 adapter.signal_connect(Event.ItemsAddedSignal,
420 self._adapter_updated_handler, feedindex)
421 adapter.signal_connect(Event.FeedPolledSignal,
422 self._adapter_updated_handler, feedindex)
423 adapter.signal_connect(Event.FeedStatusChangedSignal,
424 self._adapter_updated_handler, feedindex)
425 adapter.signal_connect(Event.ItemReadSignal,
426 self._adapter_updated_handler, feedindex)
427 adapter.signal_connect(Event.AllItemsReadSignal,
428 self._adapter_updated_handler, feedindex)
429 curr_feed_iter = None
431 adapter = create_adapter(f)
432 rowiter = self._model.append(parent)
433 self._model.set(rowiter,
434 Column.NAME, adapter.title,
435 Column.OBJECT, adapter)
437 idx = self._curr_category.index_feed(adapter.feed)
438 _connect_adapter(adapter, idx) # we need this to get the rest of the data
439 new_string, new = adapter.unread
440 weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0]
441 status, pixbuf = adapter.status_icon
442 self._model.set(rowiter,
443 Column.UNREAD, new_string,
445 Column.STATUS_FLAG, status,
446 Column.STATUS_PIXBUF, pixbuf,
447 Column.ALLOW_CHILDREN, False)
448 if adapter.feed is self._curr_feed:
449 curr_feed_iter = rowiter
450 return curr_feed_iter
452 def _disconnect(self, model, path, iter, user_data=None):
453 ob = model[path][Column.OBJECT]
454 self._disconnect_adapter(ob)
459 def _disconnect_adapter(self, adapter):
461 adapter.signal_disconnect(Event.ItemsAddedSignal,
462 self._adapter_updated_handler)
463 adapter.signal_disconnect(Event.FeedPolledSignal,
464 self._adapter_updated_handler)
465 adapter.signal_disconnect(Event.FeedStatusChangedSignal,
466 self._adapter_updated_handler)
469 def _adapter_updated_handler(self, signal, feed_index):
470 self._update_adapter_view(signal.sender, feed_index)
472 def _update_adapter_view(self, adapter, feed_index):
474 row = self._model[feed_index]
475 row[Column.NAME] = adapter.title
476 row[Column.UNREAD] = new[0]
477 row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0]
478 row[Column.OBJECT] = adapter
479 status, pixbuf = adapter.status_icon
480 row[Column.STATUS_FLAG] = status
482 row[Column.STATUS_PIXBUF] = pixbuf
483 self._view.queue_draw()
485 def _feeds_changed(self, signal):
486 self._feed_view_update(signal.feed)
488 def _feed_detail_changed(self, signal):
489 self._feed_view_update(signal.sender)
491 def _feed_item_read(self, signal):
493 selection = self._view.get_selection()
494 selected_row = selection.get_selected()
496 model, treeiter = selected_row
497 path = model.get_path(treeiter)
498 treerow = self._model[path]
499 adapter = treerow[Column.OBJECT]
500 self._update_adapter_view(adapter, path)
502 def _feed_all_items_read(self, signal):
505 def _feed_view_update(self, feed):
506 for index, f in enumerate(self._model):
507 adapter = self._model[index][Column.OBJECT]
508 if adapter.feed is feed:
509 self._update_adapter_view(adapter, index)
513 def _feeds_sorted_cb(self, signal):
514 self.model.set_sort_func(Column.NAME, self._sort_func)
515 if not signal.descending:
516 self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING)
518 self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING)
519 self.model.sort_column_changed()
522 def _fcategory_changed_cb(self,signal):
523 if signal.sender is self._curr_category:
524 self._curr_category = signal.sender
525 self.display_category_feeds(self._curr_category)
528 def move_feed(self, sidx, tidx):
529 self._curr_category.move_feed(sidx, tidx)
532 class DisplayAdapter(object, Event.SignalEmitter):
534 def __init__(self, ob):
536 Event.SignalEmitter.__init__(self)
537 self.initialize_slots(Event.ItemReadSignal,
538 Event.ItemsAddedSignal,
539 Event.AllItemsReadSignal,
540 Event.FeedPolledSignal,
541 Event.FeedStatusChangedSignal)
543 def equals(self, ob):
544 return self._ob is ob
546 class FeedDisplayAdapter(DisplayAdapter):
548 def __init__(self, ob):
549 DisplayAdapter.__init__(self, ob)
550 ob.signal_connect(Event.ItemReadSignal, self.resend_signal)
551 ob.signal_connect(Event.ItemsAddedSignal, self.resend_signal)
552 ob.signal_connect(Event.AllItemsReadSignal, self.resend_signal)
553 ob.signal_connect(Event.FeedPolledSignal, self.resend_signal)
554 ob.signal_connect(Event.FeedStatusChangedSignal, self.resend_signal)
556 def disconnect(self):
557 self._ob.signal_disconnect(
558 Event.ItemReadSignal, self.resend_signal)
559 self._ob.signal_disconnect(
560 Event.ItemsAddedSignal, self.resend_signal)
561 self._ob.signal_disconnect(
562 Event.AllItemsReadSignal, self.resend_signal)
563 self._ob.signal_disconnect(
564 Event.FeedPolledSignal, self.resend_signal)
565 self._ob.signal_disconnect(
566 Event.FeedStatusChangedSignal, self.resend_signal)
568 def resend_signal(self, signal):
569 new = copy.copy(signal)
571 self.emit_signal(new)
575 return self._ob.title
579 nu = self._ob.n_items_unread
581 return ("%s" % nu, nu)
586 def status_icon(self):
587 if self._ob.process_status is not Feed.Feed.STATUS_IDLE:
588 return (1, gtk.STOCK_EXECUTE)
590 return (1, gtk.STOCK_DIALOG_ERROR)
597 contents = property(lambda x: None)
598 open = property(lambda x: None)
601 class TreeNodeAdapter
:
602 ''' A Node Adapter which encapsulates either a Category or a Feed '''
604 def __init__(self
, object):
606 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
607 self
.default_pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
609 def has_children(self
):
610 ''' Checks if the node has children. Essentially this means the object
614 has_child
= self
.obj
.feeds
and True or False
615 except AttributeError:
621 ''' The title of the node be it a category or a feed '''
622 return self
.obj
.title
625 def num_unread_items(self
):
626 ''' The number of unread items of the feed or if it's a category,
627 the aggregate number of unread items of the feeds belonging to the
631 unread_items
= self
.obj
.n_items_unread
632 except AttributeError:
633 unread_items
= reduce(lambda a
,b
: a
.n_items_unread
+ b
.n_items_unread
,
635 print "number of unread of category is", unread_items
640 ''' gets the pixbuf to display according to the status of the feed '''
642 # ignore why above is a gtk.Label. We just need
643 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
644 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
647 if self
.obj
.process_status
is not feeds
.Feed
.STATUS_IDLE
:
648 return widget
.render_icon(gtk
.STOCK_EXECUTE
. gtk
.ICON_SIZE_MENU
)
650 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
651 except AttributeError:
652 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
653 return self
.default_pixbuf
657 ''' An alias to a Feed object '''
658 if not isinstance(self
.obj
, feeds
.Feed
):
659 raise TypeError, _("object is not of a Feed")
664 ''' An alias to a Category object '''
665 if not isinstance(self
.obj
, FeedCategoryList
.FeedCategory
):
666 raise TypeError, _("object is not a Category")