OSDN Git Service

Fix a thread title is not loaded.
[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 = ""
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
378             def create_mark():
379                 self.textbuffer.create_mark("1", self.enditer, True)
380             gobject.idle_add(create_mark)
381
382             datfile.load_dat(self.bbs, self.board, self.thread,
383                              self.append_rawres_to_buffer)
384         def jump():
385
386             def do_jump(num):
387                 if self.jump_request_num:
388                     num = self.jump_request_num
389                     self.jump_request_num = 0
390                     mark = self.textbuffer.get_mark(str(num))
391                     if mark:
392                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
393                 else:
394                     self.jump_to_the_end(num)
395
396             gobject.idle_add(do_jump, self.num)
397
398         if self.lock():
399
400             def on_end():
401                 self.un_lock()
402                 self.progress = False
403
404             self.progress = True
405             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
406             t.start()
407
408     def append_rawres_to_buffer(self, line):
409         self.size += len(line)
410         self.num += 1
411
412         if not self.title and self.num == 1:
413             title = datfile.do_get_title_from_dat(line)
414             if title:
415                 self.title = title
416                 gobject.idle_add(self.window.set_title, title)
417
418         h = lambda name,mail,date,msg: self.reselems_to_buffer(
419             self.num, name, mail, date, msg)
420
421         self.res_queue = []
422         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
423
424         def process_res_queue(res_queue, num):
425             self.process_queue(res_queue)
426             # for next res
427             self.textbuffer.create_mark(str(num+1), self.enditer, True)
428
429         gobject.idle_add(
430             process_res_queue, self.res_queue, self.num)
431
432     def reselems_to_buffer(self, num, name, mail, date, msg):
433         p = barehtmlparser.BareHTMLParser(
434             lambda d,b,h: self.res_queue.append((d,b,h,False)))
435         # number
436         p.feed(str(num) + " ")
437
438         # name
439         p.feed("<b>" + name + "</b>")
440
441         # mail
442         p.feed("[" + mail + "]")
443
444         # date
445         p.feed(date)
446         p.feed("<br>")
447
448         # msg
449         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
450         p.feed(msg.lstrip(" "))
451
452         p.feed("<br><br>")
453         p.close()
454
455     def href_tag(self, href):
456         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
457         tag.set_data("href", href)
458         return tag
459
460     def process_queue(self, queue):
461         for data, bold, href, margin in queue:
462             taglist = []
463             if bold:
464                 taglist.append(self.boldtag)
465             if href:
466                 taglist.append(self.href_tag(href))
467             if margin:
468                 taglist.append(self.leftmargintag)
469
470             if taglist:
471                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
472             else:
473                 self.textbuffer.insert(self.enditer, data)
474
475     def jump_to_the_end(self, num):
476         mark = self.textbuffer.get_mark(str(num+1))
477         if mark:
478             self.textview.scroll_to_mark(mark, 0)
479
480     def lock(self):
481         if self.lock_obj:
482             print "locked, try later."
483             return False
484         else:
485             print "get lock"
486             self.lock_obj = True
487             return True
488
489     def un_lock(self):
490         self.lock_obj = False
491         print "unlock"
492
493     def jump_to_res(self, uri):
494         strict_uri = self.bbs_type.get_thread_uri()
495         if uri != strict_uri and uri.startswith(strict_uri):
496             resnum = uri[len(strict_uri):]
497             match = re.match("\d+", resnum)
498             if match:
499                 resnum = match.group()
500                 mark = self.textbuffer.get_mark(resnum)
501                 if mark:
502                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
503                 elif self.progress:
504                     # try later.
505                     self.jump_request_num = int(resnum)
506
507     def load(self, update=False):
508         dat_path = misc.get_thread_dat_path(
509             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
510         dat_exists = os.path.exists(dat_path)
511         if update or not dat_exists:
512             self.update()
513         else:
514             self.load_dat()