OSDN Git Service

Add async loading.
[fukui-no-namari/fukui-no-namari.git] / src / Hage1 / thread_window.py
1 # Copyright (C) 2006 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18 import pygtk
19 pygtk.require('2.0')
20 import gtk
21 import gtk.glade
22 import os.path
23 import codecs
24 import re
25 import pango
26 import urllib2
27 import urlparse
28 import gnome
29 import gobject
30 import threading
31
32 import misc
33 import datfile
34 import barehtmlparser
35 import idxfile
36 import session
37 import board_window
38 import uri_opener
39 from http_sub import HTTPRedirectHandler302
40 from BbsType import bbs_type_judge_uri
41 from BbsType import bbs_type_exception
42
43 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
44                          "..", "data")
45 GLADE_FILENAME = "thread_window.glade"
46
47 def open_thread(uri, update=False):
48     if not uri:
49         raise ValueError, "parameter must not be empty"
50
51     bbs_type = bbs_type_judge_uri.get_type(uri)
52     if not bbs_type.is_thread():
53         raise bbs_type_exception.BbsTypeError, \
54               "the uri does not represent thread: " + uri
55     uri = bbs_type.get_thread_uri()  # use strict thread uri
56
57     winwrap = session.get_window(uri)
58     if winwrap:
59         # already opened
60         winwrap.window.present()
61         if update:
62             winwrap.load(update)
63     else:
64         winwrap = WinWrap(bbs_type.uri)  # pass original uri
65         session.window_created(uri, winwrap)
66         winwrap.load(update)
67
68     # jump to the res if necessary.
69     winwrap.jump_to_res(bbs_type.uri)
70
71
72 class ThreadInvoker(threading.Thread):
73     def __init__(self, on_end, *methods):
74         super(ThreadInvoker, self).__init__()
75         self.on_end = on_end
76         self.methods = methods
77     def run(self):
78         try:
79             for m in self.methods:
80                 m()
81         finally:
82             self.on_end()
83
84
85 class FileWrap:
86     def __init__(self, path):
87         self._file = None
88         self._path = path
89     def __del__(self):
90         self.close()
91     def seek(self, size):
92         self.file().seek(size)
93     def write(self, data):
94         self.file().write(data)
95     def close(self):
96         if self._file:
97             self._file.close()
98             self._file = None
99     def file(self):
100         if not self._file:
101             basedir = os.path.dirname(self._path)
102             if not os.path.isdir(basedir):
103                 os.makedirs(basedir)
104             self._file = file(self._path, "a+")
105         return self._file
106
107
108 class WinWrap:
109     hovering_over_link = False
110     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
111     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
112
113
114     def __init__(self, uri):
115         from BbsType import bbs_type_judge_uri
116         from BbsType import bbs_type_exception
117         self.bbs_type = bbs_type_judge_uri.get_type(uri)
118         if not self.bbs_type.is_thread():
119             raise bbs_type_exception.BbsTypeError, \
120                   "the uri does not represent thread: " + uri
121         self.bbs = self.bbs_type.bbs_type
122         self.board = self.bbs_type.board
123         self.thread = self.bbs_type.thread
124         self.host = self.bbs_type.host
125         self.uri = self.bbs_type.uri
126         self.size = 0
127         self.num = 0
128         self.title = None
129         self.lock_obj = False
130         self.jump_request_num = 0
131         self.progress = False
132
133         glade_path = os.path.join(GLADE_DIR, GLADE_FILENAME)
134         self.widget_tree = gtk.glade.XML(glade_path)
135         self.window = self.widget_tree.get_widget("thread_window")
136         self.textview = self.widget_tree.get_widget("textview")
137         self.textbuffer = self.textview.get_buffer()
138         self.enditer = self.textbuffer.get_end_iter()
139         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
140         self.leftmargintag = self.textbuffer.create_tag()
141         self.leftmargintag.set_property("left-margin", 20)
142
143         sigdic = {"on_toolbutton_refresh_clicked": self.update,
144                   "on_toolbutton_compose_clicked": self.on_compose_clicked,
145                   "on_refresh_activate": self.update,
146                   "on_close_activate": self.on_close_activate,
147                   "on_quit_activate": self.on_quit_activate,
148                   "on_show_board_activate": self.on_show_board_activate,
149                   "on_thread_window_destroy": self.on_thread_window_destroy}
150         self.widget_tree.signal_autoconnect(sigdic)
151
152         self.textview.connect("event-after", self.on_event_after)
153         self.textview.connect("motion-notify-event",
154                               self.on_motion_notify_event)
155         self.textview.connect("visibility-notify-event",
156                               self.on_visibility_notify_event)
157
158     def on_compose_clicked(self, widget):
159         import submit_window
160         submit_window.open(self.bbs_type.get_thread_uri())
161
162     def on_event_after(self, widget, event):
163         if event.type != gtk.gdk.BUTTON_RELEASE:
164             return False
165         if event.button != 1:
166             return False
167         buffer = widget.get_buffer()
168
169         try:
170             start, end = buffer.get_selection_bounds()
171         except ValueError:
172             pass
173         else:
174             if start.get_offset() != end.get_offset():
175                 return False
176
177         x, y = widget.window_to_buffer_coords(
178             gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
179         iter = widget.get_iter_at_location(x, y)
180         if not iter.has_tag(self.leftmargintag) or x > 20:
181             tags = iter.get_tags()
182             for tag in tags:
183                 href = tag.get_data("href")
184                 if href:
185                     self.on_link_clicked(widget, href)
186         return False
187
188     def on_link_clicked(self, widget, href):
189
190         if not href.startswith("http://"):
191             # maybe a relative uri.
192             href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
193
194         try:
195             uri_opener.open_uri(href)
196         except bbs_type_exception.BbsTypeError:
197             # not supported, show with the web browser.
198             gnome.url_show(href)
199
200     def on_motion_notify_event(self, widget, event):
201         x, y = widget.window_to_buffer_coords(
202             gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
203         self.set_cursor_if_appropriate(widget, x, y)
204         widget.window.get_pointer()
205         return False
206
207     def on_visibility_notify_event(self, widget, event):
208         wx, wy, mod = widget.window.get_pointer()
209         bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
210
211         self.set_cursor_if_appropriate(widget, bx, by)
212         return False
213
214     def set_cursor_if_appropriate(self, widget, x, y):
215         hovering = False
216
217         buffer = widget.get_buffer()
218         iter = widget.get_iter_at_location(x, y)
219         if not iter.has_tag(self.leftmargintag) or x > 20:
220             tags = iter.get_tags()
221             for tag in tags:
222                 href = tag.get_data("href")
223                 if href:
224                     hovering = True
225
226         if hovering != self.hovering_over_link:
227             self.hovering_over_link = hovering
228
229         if self.hovering_over_link:
230             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
231                 self.hand_cursor)
232         else:
233             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
234                 self.regular_cursor)
235
236     def on_close_activate(self, widget):
237         self.window.destroy()
238
239     def on_thread_window_destroy(self, widget):
240         -1
241
242     def on_quit_activate(self, widget):
243         session.main_quit()
244
245     def on_show_board_activate(self, widget):
246         board_window.open_board(self.bbs_type.get_uri_base())
247
248     def http_get_dat(self, on_get_res):
249         datfile_url = self.bbs_type.get_dat_uri()
250
251         idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
252         lastmod = idx_dic["lastModified"]
253         etag = idx_dic["etag"]
254
255         req = urllib2.Request(datfile_url)
256         if self.size > 0:
257             req.add_header("Range", "bytes=" + str(self.size) + "-")
258         if lastmod:
259             req.add_header("If-Modified-Since", lastmod)
260         if etag:
261             req.add_header("If-None-Match", etag)
262         print req.headers
263
264         opener = urllib2.build_opener(HTTPRedirectHandler302)
265         res = opener.open(req)
266         headers = res.info()
267         print headers
268
269         line = res.readline()
270         maybe_incomplete = False
271         while line:
272             if not line.endswith("\n"):
273                 maybe_incomplete = True
274                 print "does not end with \\n. maybe incomplete"
275                 break
276             on_get_res(line)
277             line = res.readline()
278
279         res.close()
280
281         if maybe_incomplete:
282             lastmod = None
283             etag = None
284         else:
285             if "Last-Modified" in headers:
286                 lastmod = headers["Last-Modified"]
287             if "ETag" in headers:
288                 etag = headers["Etag"]
289
290         if self.num > 0:
291             if not self.title:
292                 title = datfile.get_title_from_dat(
293                     self.bbs, self.board, self.thread)
294                 if title:
295                     self.title = title
296                     gobject.idle_add(self.window.set_title, title)
297             # save idx
298             idx_dic = {"title": self.title, "lineCount": self.num,
299                    "lastModified": lastmod, "etag": etag}
300             idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
301
302             gobject.idle_add(session.thread_idx_updated,
303                              self.bbs_type.get_thread_uri(), idx_dic)
304
305     def update(self, widget=None):
306
307         self.jump_request_num = 0
308
309         def load():
310             if self.num == 0:
311                 def create_mark():
312                     self.textbuffer.create_mark("1", self.enditer, True)
313                 gobject.idle_add(create_mark)
314
315             line_count = datfile.get_dat_line_count(
316                 self.bbs, self.board, self.thread)
317             if line_count > self.num:
318                 datfile.load_dat_partly(
319                     self.bbs, self.board, self.thread,
320                     self.append_rawres_to_buffer, self.num+1)
321
322                 def do_jump(num):
323                     if self.jump_request_num:
324                         if self.jump_request_num <= num:
325                             # jump if enable, otherwize jump later.
326                             num = self.jump_request_num
327                             self.jump_request_num = 0
328                             mark = self.textbuffer.get_mark(str(num))
329                             if mark:
330                                 self.textview.scroll_to_mark(
331                                     mark, 0, True, 0, 0)
332                     else:
333                         self.jump_to_the_end(num)
334
335                 gobject.idle_add(do_jump, self.num)
336
337         def get():
338             dat_path = misc.get_thread_dat_path(
339                 self.bbs, self.board, self.thread)
340             dat_file = FileWrap(dat_path)
341
342             def save_line_and_append_to_buffer(line):
343                 dat_file.seek(self.size)
344                 dat_file.write(line)
345                 self.append_rawres_to_buffer(line)
346
347             self.http_get_dat(save_line_and_append_to_buffer)
348             dat_file.close()
349
350             def do_jump():
351                 if self.jump_request_num:
352                     num = self.jump_request_num
353                     self.jump_request_num = 0
354                     mark = self.textbuffer.get_mark(str(num))
355                     if mark:
356                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
357
358             gobject.idle_add(do_jump)
359
360         if self.lock():
361
362             def on_end():
363                 self.un_lock()
364                 self.progress = False
365
366             self.progress = True
367             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
368             t.start()
369
370     def load_dat(self):
371
372         self.size = 0
373         self.num = 0
374         self.jump_request_num = 0
375
376         def load():
377             title = datfile.get_title_from_dat(
378                 self.bbs, self.board, self.thread)
379             if title:
380                 self.title = title
381                 gobject.idle_add(self.window.set_title, title)
382
383             def create_mark():
384                 self.textbuffer.create_mark("1", self.enditer, True)
385             gobject.idle_add(create_mark)
386
387             datfile.load_dat(self.bbs, self.board, self.thread,
388                              self.append_rawres_to_buffer)
389         def jump():
390
391             def do_jump(num):
392                 if self.jump_request_num:
393                     num = self.jump_request_num
394                     self.jump_request_num = 0
395                     mark = self.textbuffer.get_mark(str(num))
396                     if mark:
397                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
398                 else:
399                     self.jump_to_the_end(num)
400
401             gobject.idle_add(do_jump, self.num)
402
403         if self.lock():
404
405             def on_end():
406                 self.un_lock()
407                 self.progress = False
408
409             self.progress = True
410             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
411             t.start()
412
413     def append_rawres_to_buffer(self, line):
414         self.size += len(line)
415         self.num += 1
416
417         h = lambda name,mail,date,msg: self.reselems_to_buffer(
418             self.num, name, mail, date, msg)
419
420         self.res_queue = []
421         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
422
423         def process_res_queue(res_queue, num):
424             self.process_queue(res_queue)
425             # for next res
426             self.textbuffer.create_mark(str(num+1), self.enditer, True)
427
428         gobject.idle_add(
429             process_res_queue, self.res_queue, self.num)
430
431     def reselems_to_buffer(self, num, name, mail, date, msg):
432         p = barehtmlparser.BareHTMLParser(
433             lambda d,b,h: self.res_queue.append((d,b,h,False)))
434         # number
435         p.feed(str(num) + " ")
436
437         # name
438         p.feed("<b>" + name + "</b>")
439
440         # mail
441         p.feed("[" + mail + "]")
442
443         # date
444         p.feed(date)
445         p.feed("<br>")
446
447         # msg
448         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
449         p.feed(msg.lstrip(" "))
450
451         p.feed("<br><br>")
452         p.close()
453
454     def href_tag(self, href):
455         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
456         tag.set_data("href", href)
457         return tag
458
459     def process_queue(self, queue):
460         for data, bold, href, margin in queue:
461             taglist = []
462             if bold:
463                 taglist.append(self.boldtag)
464             if href:
465                 taglist.append(self.href_tag(href))
466             if margin:
467                 taglist.append(self.leftmargintag)
468
469             if taglist:
470                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
471             else:
472                 self.textbuffer.insert(self.enditer, data)
473
474     def jump_to_the_end(self, num):
475         mark = self.textbuffer.get_mark(str(num+1))
476         if mark:
477             self.textview.scroll_to_mark(mark, 0)
478
479     def lock(self):
480         if self.lock_obj:
481             print "locked, try later."
482             return False
483         else:
484             print "get lock"
485             self.lock_obj = True
486             return True
487
488     def un_lock(self):
489         self.lock_obj = False
490         print "unlock"
491
492     def jump_to_res(self, uri):
493         strict_uri = self.bbs_type.get_thread_uri()
494         if uri != strict_uri and uri.startswith(strict_uri):
495             resnum = uri[len(strict_uri):]
496             match = re.match("\d+", resnum)
497             if match:
498                 resnum = match.group()
499                 mark = self.textbuffer.get_mark(resnum)
500                 if mark:
501                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
502                 elif self.progress:
503                     # try later.
504                     self.jump_request_num = int(resnum)
505
506     def load(self, update=False):
507         dat_path = misc.get_thread_dat_path(
508             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
509         dat_exists = os.path.exists(dat_path)
510         if update or not dat_exists:
511             self.update()
512         else:
513             self.load_dat()