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
42 pixbuf
, name
, foreground
, object = range(4)
46 ''' Abstracts a popup widget '''
48 def __init__(self
, listener
):
49 self
.manager
= gtk
.UIManager()
51 ("refresh", gtk
.STOCK_REFRESH
, _("_Refresh"), None, _("Update this feed"),
52 listener
.on_menu_poll_selected_activate
),
53 ("mark_as_read", gtk
.STOCK_CLEAR
, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
54 listener
.on_menu_mark_all_as_read_activate
),
55 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
56 listener
.on_menu_stop_poll_selected_activate
),
57 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
58 listener
.on_remove_selected_feed
),
59 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
60 ("ascending", gtk
.STOCK_SORT_ASCENDING
, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
61 listener
.on_sort_ascending
),
62 ("descending", gtk
.STOCK_SORT_DESCENDING
, _("Re_verse Order"), None, _("Sort in reverse order"),
63 listener
.on_sort_descending
),
64 ("properties", gtk
.STOCK_INFO
, _("_Information"), None, _("Feed-specific properties"),
65 listener
.on_display_properties_feed
)
67 ag
= gtk
.ActionGroup('FeedListPopupActions')
68 ag
.add_actions(actions
)
69 self
.manager
.insert_action_group(ag
,0)
70 popupui
= os
.path
.join(utils
.find_image_dir(), 'ui.xml')
71 self
.manager
.add_ui_from_file(popupui
)
75 return self
.manager
.get_widget('/feed_list_popup')
79 ''' The model for the feed list view '''
82 # name, pixbuf, unread, foreground
83 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, gobject
.TYPE_PYOBJECT
)
84 # unread, weight, status_flag feed object, allow_children
85 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
86 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
87 self
.pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
91 def _init_model(self
):
92 fclist
= FeedCategoryList
.get_instance()
95 # build the user categories and its feeds
96 for category
in fclist
.user_categories
:
97 parent
= self
.store
.append(parent
)
98 self
.store
.set(parent
, Column
.pixbuf
, self
.pixbuf
,
99 Column
.name
, category
.title
,
101 for f
in category
.feeds
:
102 rowiter
= self
.store
.append(parent
)
103 self
.store
.set(rowiter
, Column
.pixbuf
, self
.get_pixbuf(f
),
104 Column
.name
, f
.title
,
105 Column
.foreground
, self
.get_foreground(f
.n_items_unread
),
106 Column
.object, f
) # maybe use create_adapter(f) here?
108 # build the feeds that do not belong to any other category
109 for feed
in fclist
._un
_category
.feeds
:
110 self
.store
.append(None, [self
.get_pixbuf(feed
), feed
.title
,
111 self
.get_foreground(feed
.n_items_unread
),
114 print "finished initializing feed list vie model"
116 def get_foreground(self
, unread
):
117 ''' gets the foreground color according to the number of unread items'''
118 return ('black', 'blue')[(unread
> 0) and 1 or 0]
120 def get_pixbuf(self
, feed
):
121 ''' gets the pixbuf to display according to the status of the feed '''
123 # ignore why above is a gtk.Label. We just need
124 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
125 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
127 if feed
.process_status
is not Feed
.Feed
.STATUS_IDLE
:
128 return widget
.render_icon(gtk
.STOCK_EXECUTE
. gtk
.ICON_SIZE_MENU
)
130 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
138 def get_feeds(self
, pathlist
):
139 ''' Returns a list of feed objects that live in the pathlist '''
141 for path
in pathlist
:
142 treeiter
= self
.store
.get_iter(path
)
143 object = self
.store
.get_value(treeiter
, Column
.object)
145 selected_feeds
.append(object)
146 return selected_feeds
149 class FeedsView(MVP
.WidgetView
):
150 def _initialize(self
):
151 self
._widget
.set_search_column(Column
.name
)
154 column
= gtk
.TreeViewColumn()
155 status_renderer
= gtk
.CellRendererPixbuf()
156 column
.pack_start(status_renderer
, False)
157 column
.set_attributes(status_renderer
,
158 pixbuf
=Column
.pixbuf
)
160 # feed title renderer
161 title_renderer
= gtk
.CellRendererText()
162 column
.pack_start(title_renderer
, False)
163 column
.set_attributes(title_renderer
,
164 foreground
=Column
.foreground
,
165 text
=Column
.name
) #, weight=Column.BOLD)
167 self
._widget
.append_column(column
)
169 selection
= self
._widget
.get_selection()
170 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
172 selection
.connect("changed", self
._selection
_changed
)
174 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
175 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
177 self
._popup
= FeedListPopup(self
).popup
179 def _model_set(self
):
180 self
._widget
.set_model(self
._model
.model
)
182 def add_selection_changed_listener(self
, listener
):
183 selection
= self
._widget
.get_selection()
184 selection
.connect('changed', listener
.feedlist_selection_changed
, self
._model
)
186 def _on_popup_menu(self
, treeview
, *args
):
187 self
._popup
.popup(None, None, None, 0, 0)
190 def foreach_selected(self
, func
):
191 selection
= self
._widget
.get_selection()
192 (model
, pathlist
) = selection
.get_selected_rows()
193 iters
= [model
.get_iter(path
) for path
in pathlist
]
194 for treeiter
in iters
:
195 object = model
.get_value(treeiter
, Column
.object)
196 func(object, model
, treeiter
)
200 def _on_button_press_event(self
, treeview
, event
):
202 if event
.button
== 3:
205 time
= gtk
.get_current_event_time()
206 path
= treeview
.get_path_at_pos(x
, y
)
209 path
, col
, cellx
, celly
= path
210 treeview
.grab_focus()
211 self
._popup
.popup(None, None, None, event
.button
, time
)
215 ### XXX don't do feed related tasks when path is a category
216 ### this applies to every operation I suppose
217 def on_menu_poll_selected_activate(self
, *args
):
218 config
= Config
.get_instance()
221 config
.offline
= not config
.offline
222 # XXX pass 'list' of feeds
223 #PollManager.get_instance().poll([self._curr_feed])
227 def on_menu_stop_poll_selected_activate(self
, *args
):
228 self
.foreach_selected(lambda o
,*args
: o
.router
.stop_polling())
230 def on_menu_mark_all_as_read_activate(self
, *args
):
231 self
.foreach_selected(lambda o
,*args
: o
.mark_all_read())
233 def on_remove_selected_feed(self
, *args
):
235 (object, model
, treeiter
) = args
236 model
.remove(treeiter
)
237 #feedlist = FeedList.get_instance()
238 #idx = feedlist.index(object)
240 self
.foreach_selected(remove
)
244 def on_sort_ascending(self
, *args
):
245 ## XXX this will not work with tree list
246 self
._presenter
.sort_category()
248 def on_sort_descending(self
, *args
):
249 ### XXX this will not work with tree list
250 self
._presenter
.sort_category(reverse
=True)
252 def on_display_properties_feed(self
, *args
):
253 self
._presenter
.show_feed_properties()
256 def _selection_changed(self
, selection
):
258 Called when the current feed selection changed
260 (model
, pathlist
) = selection
.get_selected_rows()
261 print "feed list view ", pathlist
262 #model, rowiter = selection.get_selected()
265 #object = model.get_value(rowiter, column)
267 #self._presenter.selection_changed(None) #object
270 #def set_cursor(self, treeiter, col_id=None, edit=False):
271 # path = self._model.get_path(treeiter)
273 # column = self._widget.get_column(col_id)
276 # self._widget.set_cursor(path, column, edit)
277 # self._widget.scroll_to_cell(path, column)
278 # self._widget.grab_focus()
282 #def get_location(self):
283 # model, iter = self._widget.get_selection().get_selected()
285 # return (None, None)
286 # path = model.get_path(iter)
287 # return self.get_parent_with_path(FeedList.get_instance(), path)
289 class FeedsPresenter(MVP
.BasicPresenter
):
290 def _initialize(self
):
291 self
.model
= FeedListModel()
292 # self._init_signals()
293 # self._curr_feed = None
294 # self._curr_category = None
298 def _init_signals(self
):
299 flist
= FeedList
.get_instance()
300 flist
.signal_connect(Event
.ItemReadSignal
,
301 self
._feed
_item
_read
)
302 flist
.signal_connect(Event
.AllItemsReadSignal
,
303 self
._feed
_all
_items
_read
)
304 flist
.signal_connect(Event
.FeedsChangedSignal
,
306 flist
.signal_connect(Event
.FeedDetailChangedSignal
,
307 self
._feed
_detail
_changed
)
308 fclist
= FeedCategoryList
.get_instance()
309 fclist
.signal_connect(Event
.FeedCategorySortedSignal
,
310 self
._feeds
_sorted
_cb
)
311 fclist
.signal_connect(Event
.FeedCategoryChangedSignal
,
312 self
._fcategory
_changed
_cb
)
314 # def _sort_func(self, model, a, b):
316 Sorts the feeds lexically.
318 From the gtk.TreeSortable.set_sort_func doc:
320 The comparison callback should return -1 if the iter1 row should come before
321 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
325 fa = model.get_value(a, Column.OBJECT)
326 fb = model.get_value(b, Column.OBJECT)
329 retval = locale.strcoll(fa.title, fb.title)
330 elif fa is not None: retval = -1
331 elif fb is not None: retval = 1
334 def get_selected_feed(self):
335 return self._curr_feed
338 def sort_category(self, reverse=False):
339 self._curr_category.sort()
341 self._curr_category.reverse()
344 def show_feed_properties(self):
345 FeedPropertiesDialog.show_feed_properties(None, self._curr_feed)
348 def select_first_feed(self):
349 treeiter = self._model.get_iter_first()
350 if not treeiter or not self._model.iter_is_valid(treeiter):
352 self._view.set_cursor(treeiter)
355 # def select_next_feed(self):
357 # Scrolls to the next feed in the feed list
359 """ selection = self._view.get_selection()
360 model, treeiter = selection.get_selected()
362 treeiter = model.get_iter_first()
363 next_feed_iter = model.iter_next(treeiter)
364 if not next_feed_iter or not self._model.iter_is_valid(next_feed_iter):
366 self._view.set_cursor(next_feed_iter)
369 def select_previous_feed(self):
371 Scrolls to the previous feeds in the feed list.
373 selection = self._view.get_selection()
374 model, treeiter = selection.get_selected()
376 treeiter = model.get_iter_first()
377 path = model.get_path(treeiter)
378 # check if there's a feed in the path
381 prev_path = path[-1]-1
383 # go to the last feed in the list
384 prev_path = len(self._model) - 1
385 self._view.set_cursor(self._model.get_iter(prev_path))
388 def select_next_unread_feed(self):
391 treerow = self._model[0]
392 selection = self._view.get_selection()
393 srow = selection.get_selected()
395 model, treeiter = srow
396 nextiter = model.iter_next(treeiter)
398 treerow = self._model[model.get_path(nextiter)]
400 feedrow = treerow[Column.OBJECT]
401 if feedrow.feed.n_items_unread:
402 self._view.set_cursor(treerow.iter)
405 treerow = treerow.next
406 if not treerow and mark_treerow:
407 # should only do this once.
408 mark_treerow = treerow
409 treerow = self._model[0]
412 def display_feed(self, feed):
413 # set_cursor will emit a 'changed' event in the treeview
414 # and then feed_selection_changed above will be called.
415 path = self._curr_category.index_feed(feed)
416 treeiter = self._model.get_iter(path)
417 self._view.set_cursor(treeiter, Column.NAME)
419 def display_category_feeds(self, category):
420 self._curr_category = category
421 feeds = self._curr_category.feeds
422 self._model.foreach(self._disconnect)
424 curr_feed_iter = self._display_feeds(feeds)
426 self._view.set_cursor(curr_feed_iter)
428 it = self._model.get_iter_first()
430 self._view.set_cursor(it)
432 self.emit_signal(Event.FeedsEmptySignal(self))
435 def _display_feeds(self, feeds, parent=None):
436 def _connect_adapter(adapter, feedindex):
437 adapter.signal_connect(Event.ItemsAddedSignal,
438 self._adapter_updated_handler, feedindex)
439 adapter.signal_connect(Event.FeedPolledSignal,
440 self._adapter_updated_handler, feedindex)
441 adapter.signal_connect(Event.FeedStatusChangedSignal,
442 self._adapter_updated_handler, feedindex)
443 adapter.signal_connect(Event.ItemReadSignal,
444 self._adapter_updated_handler, feedindex)
445 adapter.signal_connect(Event.AllItemsReadSignal,
446 self._adapter_updated_handler, feedindex)
447 curr_feed_iter = None
449 adapter = create_adapter(f)
450 rowiter = self._model.append(parent)
451 self._model.set(rowiter,
452 Column.NAME, adapter.title,
453 Column.OBJECT, adapter)
455 idx = self._curr_category.index_feed(adapter.feed)
456 _connect_adapter(adapter, idx) # we need this to get the rest of the data
457 new_string, new = adapter.unread
458 weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0]
459 status, pixbuf = adapter.status_icon
460 self._model.set(rowiter,
461 Column.UNREAD, new_string,
463 Column.STATUS_FLAG, status,
464 Column.STATUS_PIXBUF, pixbuf,
465 Column.ALLOW_CHILDREN, False)
466 if adapter.feed is self._curr_feed:
467 curr_feed_iter = rowiter
468 return curr_feed_iter
470 def display_empty_category(self):
471 self._model.foreach(self._disconnect)
475 def _disconnect(self, model, path, iter, user_data=None):
476 ob = model[path][Column.OBJECT]
477 self._disconnect_adapter(ob)
482 def _disconnect_adapter(self, adapter):
484 adapter.signal_disconnect(Event.ItemsAddedSignal,
485 self._adapter_updated_handler)
486 adapter.signal_disconnect(Event.FeedPolledSignal,
487 self._adapter_updated_handler)
488 adapter.signal_disconnect(Event.FeedStatusChangedSignal,
489 self._adapter_updated_handler)
492 def _adapter_updated_handler(self, signal, feed_index):
493 self._update_adapter_view(signal.sender, feed_index)
495 def _update_adapter_view(self, adapter, feed_index):
497 row = self._model[feed_index]
498 row[Column.NAME] = adapter.title
499 row[Column.UNREAD] = new[0]
500 row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0]
501 row[Column.OBJECT] = adapter
502 status, pixbuf = adapter.status_icon
503 row[Column.STATUS_FLAG] = status
505 row[Column.STATUS_PIXBUF] = pixbuf
506 self._view.queue_draw()
508 def _feeds_changed(self, signal):
509 self._feed_view_update(signal.feed)
511 def _feed_detail_changed(self, signal):
512 self._feed_view_update(signal.sender)
514 def _feed_item_read(self, signal):
516 selection = self._view.get_selection()
517 selected_row = selection.get_selected()
519 model, treeiter = selected_row
520 path = model.get_path(treeiter)
521 treerow = self._model[path]
522 adapter = treerow[Column.OBJECT]
523 self._update_adapter_view(adapter, path)
525 def _feed_all_items_read(self, signal):
528 def _feed_view_update(self, feed):
529 for index, f in enumerate(self._model):
530 adapter = self._model[index][Column.OBJECT]
531 if adapter.feed is feed:
532 self._update_adapter_view(adapter, index)
536 def _feeds_sorted_cb(self, signal):
537 self.model.set_sort_func(Column.NAME, self._sort_func)
538 if not signal.descending:
539 self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING)
541 self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING)
542 self.model.sort_column_changed()
545 def _fcategory_changed_cb(self,signal):
546 if signal.sender is self._curr_category:
547 self._curr_category = signal.sender
548 self.display_category_feeds(self._curr_category)
551 def expand_row(self, obj):
554 def collapse_row(self, obj):
557 def move_feed(self, sidx, tidx):
558 self._curr_category.move_feed(sidx, tidx)
561 class DisplayAdapter(object, Event
.SignalEmitter
):
563 View adapter for feeds and categories
565 def __init__(self
, ob
):
567 Event
.SignalEmitter
.__init
__(self
)
568 self
.initialize_slots(Event
.ItemReadSignal
,
569 Event
.ItemsAddedSignal
,
570 Event
.AllItemsReadSignal
,
571 Event
.FeedPolledSignal
,
572 Event
.FeedStatusChangedSignal
)
574 def equals(self
, ob
):
575 return self
._ob
is ob
577 class FeedDisplayAdapter(DisplayAdapter
):
578 """Adapter for displaying Feed objects in the tree"""
579 def __init__(self
, ob
):
580 DisplayAdapter
.__init
__(self
, ob
)
581 ob
.signal_connect(Event
.ItemReadSignal
, self
.resend_signal
)
582 ob
.signal_connect(Event
.ItemsAddedSignal
, self
.resend_signal
)
583 ob
.signal_connect(Event
.AllItemsReadSignal
, self
.resend_signal
)
584 ob
.signal_connect(Event
.FeedPolledSignal
, self
.resend_signal
)
585 ob
.signal_connect(Event
.FeedStatusChangedSignal
, self
.resend_signal
)
587 def disconnect(self
):
588 self
._ob
.signal_disconnect(
589 Event
.ItemReadSignal
, self
.resend_signal
)
590 self
._ob
.signal_disconnect(
591 Event
.ItemsAddedSignal
, self
.resend_signal
)
592 self
._ob
.signal_disconnect(
593 Event
.AllItemsReadSignal
, self
.resend_signal
)
594 self
._ob
.signal_disconnect(
595 Event
.FeedPolledSignal
, self
.resend_signal
)
596 self
._ob
.signal_disconnect(
597 Event
.FeedStatusChangedSignal
, self
.resend_signal
)
599 def resend_signal(self
, signal
):
600 new
= copy
.copy(signal
)
602 self
.emit_signal(new
)
606 return self
._ob
.title
610 nu
= self
._ob
.n_items_unread
612 return ("%s" % nu
, nu
)
617 def status_icon(self
):
618 if self
._ob
.process_status
is not Feed
.Feed
.STATUS_IDLE
:
619 return (1, gtk
.STOCK_EXECUTE
)
621 return (1, gtk
.STOCK_DIALOG_ERROR
)
628 contents
= property(lambda x
: None)
629 open = property(lambda x
: None)
631 def create_adapter(ob
):
632 if isinstance(ob
, Feed
.Feed
):
633 return FeedDisplayAdapter(ob
)