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
43 pixbuf
, name
, foreground
, object = range(4)
46 # STATUS_FLAG = 3 <- is it polling? processing images?
51 ''' Abstracts a popup widget '''
53 def __init__(self
, listener
):
54 self
.manager
= gtk
.UIManager()
56 ("refresh", gtk
.STOCK_REFRESH
, _("_Refresh"), None, _("Update this feed"),
57 listener
.on_menu_poll_selected_activate
),
58 ("mark_as_read", gtk
.STOCK_CLEAR
, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
59 listener
.on_menu_mark_all_as_read_activate
),
60 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
61 listener
.on_menu_stop_poll_selected_activate
),
62 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
63 listener
.on_remove_selected_feed
),
64 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
65 ("ascending", gtk
.STOCK_SORT_ASCENDING
, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
66 listener
.on_sort_ascending
),
67 ("descending", gtk
.STOCK_SORT_DESCENDING
, _("Re_verse Order"), None, _("Sort in reverse order"),
68 listener
.on_sort_descending
),
69 ("properties", gtk
.STOCK_INFO
, _("_Information"), None, _("Feed-specific properties"),
70 listener
.on_display_properties_feed
)
72 ag
= gtk
.ActionGroup('FeedListPopupActions')
73 ag
.add_actions(actions
)
74 self
.manager
.insert_action_group(ag
,0)
75 popupui
= os
.path
.join(utils
.find_image_dir(), 'ui.xml')
76 self
.manager
.add_ui_from_file(popupui
)
80 return self
.manager
.get_widget('/feed_list_popup')
84 ''' The model for the feed list view '''
87 # name, pixbuf, unread, foreground
88 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, gobject
.TYPE_PYOBJECT
)
89 # unread, weight, status_flag feed object, allow_children
90 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
91 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
92 self
.pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
96 def _init_model(self
):
97 fclist
= FeedCategoryList
.get_instance()
100 # build the user categories and its feeds
101 for category
in fclist
.user_categories
:
102 parent
= self
.store
.append(parent
)
103 self
.store
.set(parent
, Column
.pixbuf
, self
.pixbuf
,
104 Column
.name
, category
.title
,
106 for f
in category
.feeds
:
107 rowiter
= self
.store
.append(parent
)
108 self
.store
.set(rowiter
, Column
.pixbuf
, self
.get_pixbuf(f
),
109 Column
.name
, f
.title
,
110 Column
.foreground
, self
.get_foreground(f
.n_items_unread
),
111 Column
.object, f
) # maybe use create_adapter(f) here?
113 # build the feeds that do not belong to any other category
114 for feed
in fclist
._un
_category
.feeds
:
115 self
.store
.append(None, [self
.get_pixbuf(feed
), feed
.title
,
116 self
.get_foreground(feed
.n_items_unread
),
119 print "finished initializing feed list vie model"
121 def get_foreground(self
, unread
):
122 ''' gets the foreground color according to the number of unread items'''
123 return ('black', 'blue')[(unread
> 0) and 1 or 0]
125 def get_pixbuf(self
, feed
):
126 ''' gets the pixbuf to display according to the status of the feed '''
128 # ignore why above is a gtk.Label. We just need
129 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
130 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
132 if feed
.process_status
is not Feed
.Feed
.STATUS_IDLE
:
133 return widget
.render_icon(gtk
.STOCK_EXECUTE
. gtk
.ICON_SIZE_MENU
)
135 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
143 def get_feeds(self
, pathlist
):
144 ''' Returns a list of feed objects that live in the pathlist '''
146 for path
in pathlist
:
147 treeiter
= self
.store
.get_iter(path
)
148 object = self
.store
.get_value(treeiter
, Column
.object)
150 selected_feeds
.append(object)
151 return selected_feeds
154 class FeedsView(MVP
.WidgetView
):
155 def _initialize(self
):
156 self
._widget
.set_search_column(Column
.name
)
159 column
= gtk
.TreeViewColumn()
160 status_renderer
= gtk
.CellRendererPixbuf()
161 column
.pack_start(status_renderer
, False)
162 column
.set_attributes(status_renderer
,
163 pixbuf
=Column
.pixbuf
)
165 # feed title renderer
166 title_renderer
= gtk
.CellRendererText()
167 column
.pack_start(title_renderer
, False)
168 column
.set_attributes(title_renderer
,
169 foreground
=Column
.foreground
,
170 text
=Column
.name
) #, weight=Column.BOLD)
173 #unread_renderer = gtk.CellRendererText()
174 #column.pack_start(unread_renderer, False)
175 #column.set_attributes(unread_renderer,
176 # text=Column.unread)
178 self
._widget
.append_column(column
)
180 selection
= self
._widget
.get_selection()
181 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
183 selection
.connect("changed", self
._selection
_changed
)
185 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
186 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
188 self
._popup
= FeedListPopup(self
).popup
190 def _model_set(self
):
191 self
._widget
.set_model(self
._model
.model
)
193 def add_selection_changed_listener(self
, listener
):
194 selection
= self
._widget
.get_selection()
195 selection
.connect('changed', listener
.feedlist_selection_changed
, self
._model
)
197 def _on_popup_menu(self
, treeview
, *args
):
198 self
._popup
.popup(None, None, None, 0, 0)
201 def foreach_selected(self
, func
):
202 selection
= self
._widget
.get_selection()
203 (model
, pathlist
) = selection
.get_selected_rows()
204 iters
= [model
.get_iter(path
) for path
in pathlist
]
205 for treeiter
in iters
:
206 object = model
.get_value(treeiter
, Column
.object)
207 func(object, model
, treeiter
)
211 def _on_button_press_event(self
, treeview
, event
):
213 if event
.button
== 3:
216 time
= gtk
.get_current_event_time()
217 path
= treeview
.get_path_at_pos(x
, y
)
220 path
, col
, cellx
, celly
= path
221 treeview
.grab_focus()
222 self
._popup
.popup(None, None, None, event
.button
, time
)
226 def on_menu_poll_selected_activate(self
, *args
):
227 config
= Config
.get_instance()
230 response
= dialogs
.report_offline_status()
231 if not (response
== gtk
.RESPONSE_OK
):
232 #config.offline = not config.offline
236 config
.offline
= not config
.offline
237 #PollManager.get_instance().poll([self._curr_feed])
241 def on_menu_stop_poll_selected_activate(self
, *args
):
242 self
.foreach_selected(lambda o
,*args
: o
.router
.stop_polling())
244 def on_menu_mark_all_as_read_activate(self
, *args
):
245 ### XXX don't do feed related tasks when path is a category
246 ### this applies to every operation I suppose
247 #def mark_read(iter, object): object.mark_all_read()
248 self
.foreach_selected(lambda o
,*args
: o
.mark_all_read())
250 def on_remove_selected_feed(self
, *args
):
252 (object, model
, treeiter
) = args
253 model
.remove(treeiter
)
254 #feedlist = FeedList.get_instance()
255 #idx = feedlist.index(object)
257 self
.foreach_selected(remove
)
261 def on_sort_ascending(self
, *args
):
262 ## XXX this will not work with tree list
263 self
._presenter
.sort_category()
265 def on_sort_descending(self
, *args
):
266 ### XXX this will not work with tree list
267 self
._presenter
.sort_category(reverse
=True)
269 def on_display_properties_feed(self
, *args
):
270 self
._presenter
.show_feed_properties()
273 def _selection_changed(self
, selection
):
275 Called when the current feed selection changed
277 (model
, pathlist
) = selection
.get_selected_rows()
278 print "feed list view ", pathlist
279 #model, rowiter = selection.get_selected()
282 #object = model.get_value(rowiter, column)
284 #self._presenter.selection_changed(None) #object
287 #def set_cursor(self, treeiter, col_id=None, edit=False):
288 # path = self._model.get_path(treeiter)
290 # column = self._widget.get_column(col_id)
293 # self._widget.set_cursor(path, column, edit)
294 # self._widget.scroll_to_cell(path, column)
295 # self._widget.grab_focus()
299 #def get_location(self):
300 # model, iter = self._widget.get_selection().get_selected()
302 # return (None, None)
303 # path = model.get_path(iter)
304 # return self.get_parent_with_path(FeedList.get_instance(), path)
306 class FeedsPresenter(MVP
.BasicPresenter
):
307 def _initialize(self
):
308 self
.model
= FeedListModel()
309 # self._init_signals()
310 # self._curr_feed = None
311 # self._curr_category = None
315 def _init_signals(self
):
316 flist
= FeedList
.get_instance()
317 flist
.signal_connect(Event
.ItemReadSignal
,
318 self
._feed
_item
_read
)
319 flist
.signal_connect(Event
.AllItemsReadSignal
,
320 self
._feed
_all
_items
_read
)
321 flist
.signal_connect(Event
.FeedsChangedSignal
,
323 flist
.signal_connect(Event
.FeedDetailChangedSignal
,
324 self
._feed
_detail
_changed
)
325 fclist
= FeedCategoryList
.get_instance()
326 fclist
.signal_connect(Event
.FeedCategorySortedSignal
,
327 self
._feeds
_sorted
_cb
)
328 fclist
.signal_connect(Event
.FeedCategoryChangedSignal
,
329 self
._fcategory
_changed
_cb
)
331 # def _sort_func(self, model, a, b):
333 Sorts the feeds lexically.
335 From the gtk.TreeSortable.set_sort_func doc:
337 The comparison callback should return -1 if the iter1 row should come before
338 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
342 fa = model.get_value(a, Column.OBJECT)
343 fb = model.get_value(b, Column.OBJECT)
346 retval = locale.strcoll(fa.title, fb.title)
347 elif fa is not None: retval = -1
348 elif fb is not None: retval = 1
351 def get_selected_feed(self):
352 return self._curr_feed
355 def sort_category(self, reverse=False):
356 self._curr_category.sort()
358 self._curr_category.reverse()
361 def show_feed_properties(self):
362 FeedPropertiesDialog.show_feed_properties(None, self._curr_feed)
365 def select_first_feed(self):
366 treeiter = self._model.get_iter_first()
367 if not treeiter or not self._model.iter_is_valid(treeiter):
369 self._view.set_cursor(treeiter)
372 # def select_next_feed(self):
374 # Scrolls to the next feed in the feed list
376 """ selection = self._view.get_selection()
377 model, treeiter = selection.get_selected()
379 treeiter = model.get_iter_first()
380 next_feed_iter = model.iter_next(treeiter)
381 if not next_feed_iter or not self._model.iter_is_valid(next_feed_iter):
383 self._view.set_cursor(next_feed_iter)
386 def select_previous_feed(self):
388 Scrolls to the previous feeds in the feed list.
390 selection = self._view.get_selection()
391 model, treeiter = selection.get_selected()
393 treeiter = model.get_iter_first()
394 path = model.get_path(treeiter)
395 # check if there's a feed in the path
398 prev_path = path[-1]-1
400 # go to the last feed in the list
401 prev_path = len(self._model) - 1
402 self._view.set_cursor(self._model.get_iter(prev_path))
405 def select_next_unread_feed(self):
408 treerow = self._model[0]
409 selection = self._view.get_selection()
410 srow = selection.get_selected()
412 model, treeiter = srow
413 nextiter = model.iter_next(treeiter)
415 treerow = self._model[model.get_path(nextiter)]
417 feedrow = treerow[Column.OBJECT]
418 if feedrow.feed.n_items_unread:
419 self._view.set_cursor(treerow.iter)
422 treerow = treerow.next
423 if not treerow and mark_treerow:
424 # should only do this once.
425 mark_treerow = treerow
426 treerow = self._model[0]
429 def display_feed(self, feed):
430 # set_cursor will emit a 'changed' event in the treeview
431 # and then feed_selection_changed above will be called.
432 path = self._curr_category.index_feed(feed)
433 treeiter = self._model.get_iter(path)
434 self._view.set_cursor(treeiter, Column.NAME)
436 def display_category_feeds(self, category):
437 self._curr_category = category
438 feeds = self._curr_category.feeds
439 self._model.foreach(self._disconnect)
441 curr_feed_iter = self._display_feeds(feeds)
443 self._view.set_cursor(curr_feed_iter)
445 it = self._model.get_iter_first()
447 self._view.set_cursor(it)
449 self.emit_signal(Event.FeedsEmptySignal(self))
452 def _display_feeds(self, feeds, parent=None):
453 def _connect_adapter(adapter, feedindex):
454 adapter.signal_connect(Event.ItemsAddedSignal,
455 self._adapter_updated_handler, feedindex)
456 adapter.signal_connect(Event.FeedPolledSignal,
457 self._adapter_updated_handler, feedindex)
458 adapter.signal_connect(Event.FeedStatusChangedSignal,
459 self._adapter_updated_handler, feedindex)
460 adapter.signal_connect(Event.ItemReadSignal,
461 self._adapter_updated_handler, feedindex)
462 adapter.signal_connect(Event.AllItemsReadSignal,
463 self._adapter_updated_handler, feedindex)
464 curr_feed_iter = None
466 adapter = create_adapter(f)
467 rowiter = self._model.append(parent)
468 self._model.set(rowiter,
469 Column.NAME, adapter.title,
470 Column.OBJECT, adapter)
472 idx = self._curr_category.index_feed(adapter.feed)
473 _connect_adapter(adapter, idx) # we need this to get the rest of the data
474 new_string, new = adapter.unread
475 weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0]
476 status, pixbuf = adapter.status_icon
477 self._model.set(rowiter,
478 Column.UNREAD, new_string,
480 Column.STATUS_FLAG, status,
481 Column.STATUS_PIXBUF, pixbuf,
482 Column.ALLOW_CHILDREN, False)
483 if adapter.feed is self._curr_feed:
484 curr_feed_iter = rowiter
485 return curr_feed_iter
487 def display_empty_category(self):
488 self._model.foreach(self._disconnect)
492 def _disconnect(self, model, path, iter, user_data=None):
493 ob = model[path][Column.OBJECT]
494 self._disconnect_adapter(ob)
499 def _disconnect_adapter(self, adapter):
501 adapter.signal_disconnect(Event.ItemsAddedSignal,
502 self._adapter_updated_handler)
503 adapter.signal_disconnect(Event.FeedPolledSignal,
504 self._adapter_updated_handler)
505 adapter.signal_disconnect(Event.FeedStatusChangedSignal,
506 self._adapter_updated_handler)
509 def _adapter_updated_handler(self, signal, feed_index):
510 self._update_adapter_view(signal.sender, feed_index)
512 def _update_adapter_view(self, adapter, feed_index):
514 row = self._model[feed_index]
515 row[Column.NAME] = adapter.title
516 row[Column.UNREAD] = new[0]
517 row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0]
518 row[Column.OBJECT] = adapter
519 status, pixbuf = adapter.status_icon
520 row[Column.STATUS_FLAG] = status
522 row[Column.STATUS_PIXBUF] = pixbuf
523 self._view.queue_draw()
525 def _feeds_changed(self, signal):
526 self._feed_view_update(signal.feed)
528 def _feed_detail_changed(self, signal):
529 self._feed_view_update(signal.sender)
531 def _feed_item_read(self, signal):
533 selection = self._view.get_selection()
534 selected_row = selection.get_selected()
536 model, treeiter = selected_row
537 path = model.get_path(treeiter)
538 treerow = self._model[path]
539 adapter = treerow[Column.OBJECT]
540 self._update_adapter_view(adapter, path)
542 def _feed_all_items_read(self, signal):
545 def _feed_view_update(self, feed):
546 for index, f in enumerate(self._model):
547 adapter = self._model[index][Column.OBJECT]
548 if adapter.feed is feed:
549 self._update_adapter_view(adapter, index)
553 def _feeds_sorted_cb(self, signal):
554 self.model.set_sort_func(Column.NAME, self._sort_func)
555 if not signal.descending:
556 self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING)
558 self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING)
559 self.model.sort_column_changed()
562 def _fcategory_changed_cb(self,signal):
563 if signal.sender is self._curr_category:
564 self._curr_category = signal.sender
565 self.display_category_feeds(self._curr_category)
568 def expand_row(self, obj):
571 def collapse_row(self, obj):
574 def move_feed(self, sidx, tidx):
575 self._curr_category.move_feed(sidx, tidx)
578 class DisplayAdapter(object, Event
.SignalEmitter
):
580 View adapter for feeds and categories
582 def __init__(self
, ob
):
584 Event
.SignalEmitter
.__init
__(self
)
585 self
.initialize_slots(Event
.ItemReadSignal
,
586 Event
.ItemsAddedSignal
,
587 Event
.AllItemsReadSignal
,
588 Event
.FeedPolledSignal
,
589 Event
.FeedStatusChangedSignal
)
591 def equals(self
, ob
):
592 return self
._ob
is ob
594 class FeedDisplayAdapter(DisplayAdapter
):
595 """Adapter for displaying Feed objects in the tree"""
596 def __init__(self
, ob
):
597 DisplayAdapter
.__init
__(self
, ob
)
598 ob
.signal_connect(Event
.ItemReadSignal
, self
.resend_signal
)
599 ob
.signal_connect(Event
.ItemsAddedSignal
, self
.resend_signal
)
600 ob
.signal_connect(Event
.AllItemsReadSignal
, self
.resend_signal
)
601 ob
.signal_connect(Event
.FeedPolledSignal
, self
.resend_signal
)
602 ob
.signal_connect(Event
.FeedStatusChangedSignal
, self
.resend_signal
)
604 def disconnect(self
):
605 self
._ob
.signal_disconnect(
606 Event
.ItemReadSignal
, self
.resend_signal
)
607 self
._ob
.signal_disconnect(
608 Event
.ItemsAddedSignal
, self
.resend_signal
)
609 self
._ob
.signal_disconnect(
610 Event
.AllItemsReadSignal
, self
.resend_signal
)
611 self
._ob
.signal_disconnect(
612 Event
.FeedPolledSignal
, self
.resend_signal
)
613 self
._ob
.signal_disconnect(
614 Event
.FeedStatusChangedSignal
, self
.resend_signal
)
616 def resend_signal(self
, signal
):
617 new
= copy
.copy(signal
)
619 self
.emit_signal(new
)
623 return self
._ob
.title
627 nu
= self
._ob
.n_items_unread
629 return ("%s" % nu
, nu
)
634 def status_icon(self
):
635 if self
._ob
.process_status
is not Feed
.Feed
.STATUS_IDLE
:
636 return (1, gtk
.STOCK_EXECUTE
)
638 return (1, gtk
.STOCK_DIALOG_ERROR
)
645 contents
= property(lambda x
: None)
646 open = property(lambda x
: None)
648 def create_adapter(ob
):
649 if isinstance(ob
, Feed
.Feed
):
650 return FeedDisplayAdapter(ob
)