Implemented "mark all as read".
[straw.git] / straw / Find.py
blob6b0fa1b5031881019e1fabdcbe07001a360dd3bf
1 """ Find.py
3 Module for searching items.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """ GNU General Public License
8 Straw is free software; you can redistribute it and/or modify it under the
9 terms of the GNU General Public License as published by the Free Software
10 Foundation; either version 2 of the License, or (at your option) any later
11 version.
13 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License along with
18 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
19 Place - Suite 330, Boston, MA 02111-1307, USA. """
22 import pygtk
23 pygtk.require('2.0')
24 import gtk
25 import gtk.glade
26 import gobject
27 import pango
28 import time
29 import Event
30 import utils
31 import feeds
32 import MessageManager
33 from ItemList import ItemsView, ItemsPresenter
34 import MVP
35 import error
37 class Column:
38 TITLE = 0
39 FEED_TITLE = 1
40 ITEM = 2
41 BOLD = 3
43 class FindLimit(object):
44 __slots__ = ('text', 'start', 'end')
46 def __init__(self, text, start = None, end = None):
47 self.text = text
48 self.start = start
49 self.end = end
51 def is_subset(self, fl):
52 if self.text.find(fl.text) == -1:
53 return 0
54 if fl.end and (self.end or self.end > fl.end):
55 return 0
56 if fl.start and (self.start or self.start < fl.start):
57 return 0
58 return 1
60 def equals(self, fl):
61 return (self.text == fl.text and
62 self.end == fl.end and
63 self.start == fl.start)
65 def time_contains(self, item):
66 rval = 1
67 if item.pub_date: # pub_date hasn't been initialized in older versions of straw
68 if self.start and self.start > item.pub_date:
69 rval = 0
70 if self.end and self.end < item.pub_date:
71 rval = 0
72 return rval
74 class Finder:
75 def __init__(self):
76 self._stack = list()
77 self._feedlist = feeds.get_instance()
79 def find_matching(self, limit, items):
80 if not len(items):
81 for feed in self._feedlist:
82 match = False
83 for item in feed.items:
84 if limit.time_contains(item) and item.match(limit.text):
85 match = True
86 yield item
87 if not match:
88 feed.unload_contents()
89 while gtk.events_pending(): gtk.main_iteration(False)
90 else:
91 return
92 else:
93 error.log("ITEMS IS NOT NONE.!",items)
94 # we expect this to be quicker than the above since the items are
95 # already loaded in the memory.
96 for item in items:
97 if limit.time_contains(item) and item.match(limit.text):
98 yield item
99 return
102 def find(self, limit):
103 if len(limit.text) < 1:
104 del self._stack[:]
105 return []
106 items = []
107 if len(self._stack):
108 if limit.is_subset(self._stack[-1][0]):
109 # the new search string is longer than the previous
110 if len(self._stack[-1][1]):
111 # return existing items
112 items = self._stack[-1][1]
113 else:
114 # text was deleted
115 length = len(self._stack)
116 foundprev = 0
117 while length > 0:
118 length -= 1
119 if limit.equals(self._stack[length][0]):
120 foundprev = 1
121 break
122 if limit.is_subset(self._stack[length][0]):
123 break
124 self._stack = self._stack[:length+1]
125 if foundprev or len(self._stack):
126 items = self._stack[-1][1]
127 return (limit, items)
129 def append_matches(self, limit, matches):
130 lm = (limit,matches)
131 self._stack.append(lm)
132 return
134 class FindResultListView(ItemsView):
136 Widget: gtk.TreeView
137 Model: Result Liststore
138 Presenter: ItemListPresenter
140 def _initialize(self):
141 ItemsView._initialize(self)
142 self._widget.set_rules_hint(True)
143 self._create_columns()
144 selection = self._widget.get_selection()
145 selection.connect("changed", self._item_selection_changed)
146 self._create_popup()
148 def _model_set(self):
149 self._widget.set_model(self._model)
151 def _create_columns(self):
152 renderer = gtk.CellRendererText()
153 column = gtk.TreeViewColumn('Title', renderer, text=Column.TITLE,
154 weight=Column.BOLD)
155 self._widget.append_column(column)
156 renderer = gtk.CellRendererText()
157 column = gtk.TreeViewColumn('Feed', renderer, text=Column.FEED_TITLE,
158 weight=Column.BOLD)
159 self._widget.append_column(column)
160 return
162 def clear(self):
163 self._widget.get_model().clear()
165 class FindResultPresenter(ItemsPresenter):
166 def _initialize(self):
167 self._connect()
168 model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING,
169 gobject.TYPE_PYOBJECT, gobject.TYPE_INT)
170 self.model = model
171 self._finder = Finder()
172 self._interrupted = False
173 self._matches=list()
175 def _mark_item(self, item, index=None):
176 if not index:
177 index = self._matches.index(item)
178 self._set_column_weight(item, index)
180 def find(self, limit):
181 self._clear_matches()
183 limit_items = self._finder.find(limit)
184 if not len(limit_items):
185 return self._matches
186 limit, items = limit_items
187 for item in self._finder.find_matching(limit, items):
188 if self._interrupted:
189 self._interrupted = True
190 break
191 treeiter = self._model.append()
192 weight = pango.WEIGHT_NORMAL
193 if not item.seen:
194 weight = pango.WEIGHT_BOLD
195 self._matches.append(item)
196 self._model.set(treeiter, Column.TITLE, item.title,
197 Column.FEED_TITLE, item.feed.title,
198 Column.ITEM, item,
199 Column.BOLD, weight)
201 error.log("MATCHES: %d %s %s" %(len(self._matches),limit.text,items))
202 self._finder.append_matches(limit, self._matches)
203 return self._matches
205 def stop(self, signal):
206 error.log("stopping...")
207 self._interrupted = True
208 return
210 def clear(self):
211 self._interrupted = False
212 self._model.clear()
213 self._clear_matches()
214 return
216 def _clear_matches(self):
217 for feed in dict.fromkeys((item.feed for item in self._matches)).keys():
218 feed.unload_contents()
219 del self._matches[:]
221 class FindView(MVP.WidgetView):
222 SECS_DAY = 86400
224 def _initialize(self):
225 self._widget_tree = gtk.glade.get_widget_tree(self._widget)
226 self._text_entry = self._widget_tree.get_widget("find_text_entry")
228 def _on_find_text_entry_insert_text(self, *args):
229 gobject.timeout_add(700, self._after_change)
230 return
232 def _on_find_text_entry_delete_text(self, *args):
233 gobject.timeout_add(700, self._after_change)
234 return
236 def _on_find_start_time_limit_check_toggled(self, button, *args):
237 state = button.get_active()
238 start_date_widget = self._widget_tree.get_widget('find_start_date_edit')
239 start_date_widget.set_sensitive(state)
240 stime = self._determine_time(state, start_date_widget.get_time())
241 self._presenter.start_date(stime)
242 return
244 def _on_find_end_time_limit_check_toggled(self, button, *args):
245 state = button.get_active()
246 end_date_widget = self._widget_tree.get_widget('find_end_date_edit')
247 end_date_widget.set_sensitive(state)
248 etime = self._determine_time(state, end_date_widget.get_time() + self.SECS_DAY)
249 self._presenter.end_date(etime)
250 return
252 def _on_find_start_date_edit_date_changed(self, widget):
254 Precondition: start_date_toggle is toggled (ON) or else this signal
255 won't be triggered in the first place
257 stime = self._determine_time(True, widget.get_time())
258 self._presenter.start_date(stime)
259 return
261 def _on_find_end_date_edit_date_changed(self, widget):
263 Precondition: end_date_toggle is toggled (ON) or else this signal
264 won't be triggered in the first place
266 etime = self._determine_time(True, widget.get_time() + self.SECS_DAY)
267 self._presenter.end_date(etime)
268 return
270 def _after_change(self):
271 newtext = self._text_entry.get_text()
272 self._presenter.text_changed(newtext)
273 return
275 def _determine_time(self, togglestate, dtime):
276 xtime = None
277 if togglestate:
278 xtime = time.gmtime(dtime)
279 return xtime
281 def _get_text(self): return self._text
282 text = property(_get_text)
284 def get_widget(self):
285 return self._widget
287 def clear(self):
288 self._text_entry.delete_text(0,-1)
289 return
291 class FindPresenter(MVP.BasicPresenter):
292 def _initialize(self):
293 self.initialize_slots(Event.FindInterruptSignal)
294 widget_tree = gtk.glade.get_widget_tree(self._view.get_widget())
295 self._find_result_pres = FindResultPresenter(view =
296 FindResultListView(widget_tree.get_widget("find_results_treeview")))
297 self.signal_connect(Event.FindInterruptSignal,
298 self._find_result_pres.stop)
299 self._resultscount = widget_tree.get_widget("find_results_count_display")
300 self._rendering = False
301 self._text = ''
302 self._start_date = None
303 self._end_date = None
305 def start_date(self, stime):
306 self._start_date = stime
307 if stime and stime is not self._start_date:
308 limit = FindLimit(self._text,
309 stime,
310 self._end_date)
311 self._find_items(limit)
312 return
314 def end_date(self, etime):
315 self._end_date = etime
316 if etime and etime is not self._end_date:
317 limit = FindLimit(self._text,
318 self._start_date,
319 etime)
320 self._find_items(limit)
321 return
323 def text_changed(self, newtext):
324 # terminate an existing result rendering in progress
325 if self._rendering:
326 self.emit_signal(Event.FindInterruptSignal(self))
327 changed = newtext is not self._text
328 if changed:
329 self._text = newtext
330 self._rendering = False
331 if changed and not self._rendering:
332 self._rendering = True
333 limit = FindLimit(self._text,
334 self._start_date,
335 self._end_date)
336 self._find_items(limit)
337 self._rendering = False
338 return
340 def _find_items(self, limit):
341 matches = self._find_result_pres.find(limit)
342 nmatches = len(matches)
343 if nmatches <= 0: self._find_result_pres.clear()
344 self._resultscount.set_text(_("%d items found") % nmatches or 0)
345 return
347 def clear(self):
348 self._find_result_pres.clear()
349 self._view.clear()
351 def _find_result_presenter(self): return self._find_result_pres
352 item_list = property(_find_result_presenter)