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. """
27 import FeedCategoryList
28 from FeedPropertiesDialog
import FeedPropertiesDialog
46 class FeedsView(MVP
.WidgetView
):
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\"/>
55 <menu name=\"Sort\" action=\"category-sort\">
56 <menuitem action=\"ascending\"/>
57 <menuitem action=\"descending\"/>
59 <menuitem action=\"properties\"/>
63 def _initialize(self
):
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
)
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
)
100 def _create_popup(self
):
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')
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
):
138 if event
.button
== 3:
141 time
= gtk
.get_current_event_time()
142 path
= treeview
.get_path_at_pos(x
, y
)
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
)
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()
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()
180 def _feed_selection_changed(self
, selection
, column
):
182 Called when the current feed selection changed
184 model
, rowiter
= selection
.get_selected()
187 adapter
= model
.get_value(rowiter
, column
)
189 self
._presenter
.feed_selection_changed(adapter
.feed
)
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
)
203 column
= self
._widget
.get_column(col_id
)
206 self
._widget
.set_cursor(path
, column
, edit
)
207 self
._widget
.scroll_to_cell(path
, column
)
208 self
._widget
.grab_focus()
211 def queue_draw(self
):
212 self
._widget
.queue_draw()
216 def get_location(self
):
217 model
, iter = self
._widget
.get_selection().get_selected()
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
)
234 self
._curr
_feed
= None
235 self
._curr
_category
= None
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
,
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
265 fa
= model
.get_value(a
, Column
.OBJECT
)
266 fb
= model
.get_value(b
, Column
.OBJECT
)
269 retval
= locale
.strcoll(fa
.title
, fb
.title
)
270 elif fa
is not None: retval
= -1
271 elif fb
is not None: retval
= 1
274 def poll_current_feed(self
):
275 config
= Config
.get_instance()
278 response
= dialogs
.report_offline_status()
279 if response
== gtk
.RESPONSE_OK
:
280 config
.offline
= not config
.offline
285 PollManager
.get_instance().poll([self
._curr
_feed
])
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()
301 adapter
= model
.get_value(rowiter
, Column
.OBJECT
)
302 it
= model
.remove(rowiter
)
303 feedlist
= FeedList
.get_instance()
304 idx
= feedlist
.index(adapter
.feed
)
308 def sort_category(self
, reverse
=False):
309 self
._curr
_category
.sort()
311 self
._curr
_category
.reverse()
314 def show_feed_properties(self
):
315 fpd
= FeedPropertiesDialog
.show_feed_properties(None, self
._curr
_feed
)
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
):
322 self
._view
.set_cursor(treeiter
)
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()
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
):
336 self
._view
.set_cursor(next_feed_iter
)
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()
346 treeiter
= model
.get_iter_first()
347 path
= model
.get_path(treeiter
)
348 # check if there's a feed in the path
351 prev_path
= path
[-1]-1
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
))
358 def select_next_unread_feed(self
):
361 treerow
= self
._model
[0]
362 selection
= self
._view
.get_selection()
363 srow
= selection
.get_selected()
365 model
, treeiter
= srow
366 nextiter
= model
.iter_next(treeiter
)
368 treerow
= self
._model
[model
.get_path(nextiter
)]
370 feedrow
= treerow
[Column
.OBJECT
]
371 if feedrow
.feed
.n_items_unread
:
372 self
._view
.set_cursor(treerow
.iter)
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]
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
390 self
._curr
_feed
= feed
392 self
._curr
_feed
= None
393 if oldfeed
is not self
._curr
_feed
:
394 self
.emit_signal(Event
.FeedSelectionChangedSignal(self
,oldfeed
,self
._curr
_feed
))
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
)
409 curr_feed_iter
= self
._display
_feeds
(feeds
)
411 self
._view
.set_cursor(curr_feed_iter
)
413 it
= self
._model
.get_iter_first()
415 self
._view
.set_cursor(it
)
417 self
.emit_signal(Event
.FeedsEmptySignal(self
))
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
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
,
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
)
460 def _disconnect(self
, model
, path
, iter, user_data
=None):
461 ob
= model
[path
][Column
.OBJECT
]
462 self
._disconnect
_adapter
(ob
)
467 def _disconnect_adapter(self
, adapter
):
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
)
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
):
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
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
):
501 selection
= self
._view
.get_selection()
502 selected_row
= selection
.get_selected()
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
):
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
)
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
)
526 self
.model
.set_sort_column_id(Column
.NAME
, gtk
.SORT_DESCENDING
)
527 self
.model
.sort_column_changed()
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
)
536 def expand_row(self
, obj
):
539 def collapse_row(self
, obj
):
542 def move_feed(self
, sidx
, tidx
):
543 self
._curr
_category
.move_feed(sidx
, tidx
)
546 class DisplayAdapter(object, Event
.SignalEmitter
):
548 View adapter for feeds and categories
550 def __init__(self
, 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
)
587 self
.emit_signal(new
)
591 return self
._ob
.title
595 nu
= self
._ob
.n_items_unread
597 return ("%s" % nu
, nu
)
602 def status_icon(self
):
603 if self
._ob
.process_status
is not Feed
.Feed
.STATUS_IDLE
:
604 return (1, gtk
.STOCK_EXECUTE
)
606 return (1, gtk
.STOCK_DIALOG_ERROR
)
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
)