OSDN Git Service

Add HTTP header output.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / 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 import gconf
32 import traceback
33
34 import misc
35 from misc import FileWrap, ThreadInvoker
36 import datfile
37 import barehtmlparser
38 import idxfile
39 import session
40 import board_window
41 import uri_opener
42 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
43 from BbsType import bbs_type_judge_uri
44 from BbsType import bbs_type_exception
45 import config
46 import winwrapbase
47
48 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
49                          "..", "data")
50 GLADE_FILENAME = "thread_window.glade"
51
52 def open_thread(uri, update=False):
53     if not uri:
54         raise ValueError, "parameter must not be empty"
55
56     bbs_type = bbs_type_judge_uri.get_type(uri)
57     if not bbs_type.is_thread():
58         raise bbs_type_exception.BbsTypeError, \
59               "the uri does not represent thread: " + uri
60     uri = bbs_type.get_thread_uri()  # use strict thread uri
61
62     winwrap = session.get_window(uri)
63     if winwrap:
64         # already opened
65         winwrap.window.present()
66         if update:
67             winwrap.load(update)
68     else:
69         winwrap = WinWrap(bbs_type.uri)  # pass original uri
70         winwrap.load(update)
71
72     # jump to the res if necessary.
73     winwrap.jump_to_res(bbs_type.uri)
74
75
76 class WinWrap(winwrapbase.WinWrapBase):
77     hovering_over_link = False
78     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
79     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
80
81
82     def __init__(self, uri):
83         from BbsType import bbs_type_judge_uri
84         from BbsType import bbs_type_exception
85         self.bbs_type = bbs_type_judge_uri.get_type(uri)
86         if not self.bbs_type.is_thread():
87             raise bbs_type_exception.BbsTypeError, \
88                   "the uri does not represent thread: " + uri
89         self.bbs = self.bbs_type.bbs_type
90         self.board = self.bbs_type.board
91         self.thread = self.bbs_type.thread
92         self.host = self.bbs_type.host
93         self.uri = self.bbs_type.uri
94         self.size = 0
95         self.num = 0
96         self.title = ""
97         self.lock_obj = False
98         self.jump_request_num = 0
99         self.progress = False
100
101         glade_path = os.path.join(GLADE_DIR, GLADE_FILENAME)
102         self.widget_tree = gtk.glade.XML(glade_path)
103         self.window = self.widget_tree.get_widget("thread_window")
104         self.toolbar = self.widget_tree.get_widget("toolbar")
105         self.toolbar.unset_style()
106         self.statusbar = self.widget_tree.get_widget("appbar")
107         self.textview = self.widget_tree.get_widget("textview")
108         self.textview.drag_dest_unset()
109         self.textbuffer = self.textview.get_buffer()
110         self.enditer = self.textbuffer.get_end_iter()
111         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
112         self.leftmargintag = self.textbuffer.create_tag()
113         self.leftmargintag.set_property("left-margin", 20)
114
115         sigdic = {"on_refresh_activate": self.update,
116                   "on_compose_activate": self.on_compose_clicked,
117                   "on_toolbar_activate": self.on_toolbar_activate,
118                   "on_statusbar_activate": self.on_statusbar_activate,
119                   "on_refresh_activate": self.update,
120                   "on_close_activate": self.on_close_activate,
121                   "on_quit_activate": self.on_quit_activate,
122                   "on_show_board_activate": self.on_show_board_activate,
123                   "on_thread_window_delete_event":
124                   self.on_thread_window_delete_event,
125                   "on_thread_window_destroy": self.on_thread_window_destroy}
126         self.widget_tree.signal_autoconnect(sigdic)
127
128         self.textview.connect("event-after", self.on_event_after)
129         self.textview.connect("motion-notify-event",
130                               self.on_motion_notify_event)
131         self.textview.connect("visibility-notify-event",
132                               self.on_visibility_notify_event)
133         self.restore()
134         self.window.show()
135
136         self.created()
137
138     def destroy(self):
139         self.save()
140         self.window.destroy()
141
142     def get_uri(self):
143         return self.bbs_type.get_thread_uri()
144
145     def on_compose_clicked(self, widget):
146         import submit_window
147         submit_window.open(self.bbs_type.get_thread_uri())
148
149     def on_toolbar_activate(self, widget):
150         if self.toolbar.parent.get_property("visible"):
151             self.toolbar.parent.hide()
152         else:
153             self.toolbar.parent.show()
154
155     def on_statusbar_activate(self, widget):
156         if self.statusbar.get_property("visible"):
157             self.statusbar.hide()
158         else:
159             self.statusbar.show()
160
161     def on_event_after(self, widget, event):
162         if event.type != gtk.gdk.BUTTON_RELEASE:
163             return False
164         if event.button != 1:
165             return False
166         buffer = widget.get_buffer()
167
168         try:
169             start, end = buffer.get_selection_bounds()
170         except ValueError:
171             pass
172         else:
173             if start.get_offset() != end.get_offset():
174                 return False
175
176         x, y = widget.window_to_buffer_coords(
177             gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
178         iter = widget.get_iter_at_location(x, y)
179         if not iter.has_tag(self.leftmargintag) or x > 20:
180             tags = iter.get_tags()
181             for tag in tags:
182                 href = tag.get_data("href")
183                 if href:
184                     self.on_link_clicked(widget, href)
185         return False
186
187     def on_link_clicked(self, widget, href):
188
189         if not href.startswith("http://"):
190             # maybe a relative uri.
191             href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
192
193         try:
194             uri_opener.open_uri(href)
195         except bbs_type_exception.BbsTypeError:
196             # not supported, show with the web browser.
197             gnome.url_show(href)
198
199     def on_motion_notify_event(self, widget, event):
200         x, y = widget.window_to_buffer_coords(
201             gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
202         self.set_cursor_if_appropriate(widget, x, y)
203         widget.window.get_pointer()
204         return False
205
206     def on_visibility_notify_event(self, widget, event):
207         wx, wy, mod = widget.window.get_pointer()
208         bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
209
210         self.set_cursor_if_appropriate(widget, bx, by)
211         return False
212
213     def set_cursor_if_appropriate(self, widget, x, y):
214         hovering = False
215
216         buffer = widget.get_buffer()
217         iter = widget.get_iter_at_location(x, y)
218         if not iter.has_tag(self.leftmargintag) or x > 20:
219             tags = iter.get_tags()
220             for tag in tags:
221                 href = tag.get_data("href")
222                 if href:
223                     hovering = True
224
225         if hovering != self.hovering_over_link:
226             self.hovering_over_link = hovering
227
228         if self.hovering_over_link:
229             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
230                 self.hand_cursor)
231         else:
232             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
233                 self.regular_cursor)
234
235     def on_close_activate(self, widget):
236         self.destroy()
237
238     def on_thread_window_delete_event(self, widget, event):
239         self.save()
240         return False
241         
242     def on_thread_window_destroy(self, widget):
243         self.destroyed()
244
245     def on_quit_activate(self, widget):
246         session.main_quit()
247
248     def on_show_board_activate(self, widget):
249         board_window.open_board(self.bbs_type.get_uri_base())
250
251     def http_get_dat(self, on_get_res):
252         datfile_url = self.bbs_type.get_dat_uri()
253
254         idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
255         lastmod = idx_dic["lastModified"]
256         etag = idx_dic["etag"]
257
258         req = urllib2.Request(datfile_url)
259         if self.size > 0:
260             req.add_header("Range", "bytes=" + str(self.size) + "-")
261         if lastmod:
262             req.add_header("If-Modified-Since", lastmod)
263         if etag:
264             req.add_header("If-None-Match", etag)
265
266         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
267         try:
268             res = opener.open(req)
269         except urllib2.HTTPError, e:
270             gobject.idle_add(
271                 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
272         else:
273             headers = res.info()
274             gobject.idle_add(
275                 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
276
277             line = res.readline()
278             maybe_incomplete = False
279             while line:
280                 if not line.endswith("\n"):
281                     maybe_incomplete = True
282                     print "does not end with \\n. maybe incomplete"
283                     break
284                 on_get_res(line)
285                 line = res.readline()
286
287             res.close()
288
289             if maybe_incomplete:
290                 lastmod = None
291                 etag = None
292             else:
293                 if "Last-Modified" in headers:
294                     lastmod = headers["Last-Modified"]
295                 if "ETag" in headers:
296                     etag = headers["Etag"]
297
298             if self.num > 0:
299                 if not self.title:
300                     title = datfile.get_title_from_dat(
301                         self.bbs, self.board, self.thread)
302                     if title:
303                         self.title = title
304                         gobject.idle_add(self.window.set_title, title)
305                 # save idx
306                 idx_dic = {"title": self.title, "lineCount": self.num,
307                        "lastModified": lastmod, "etag": etag}
308                 idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
309
310                 gobject.idle_add(session.thread_idx_updated,
311                                  self.bbs_type.get_thread_uri(), idx_dic)
312
313     def update(self, widget=None):
314
315         self.jump_request_num = 0
316
317         def load():
318             if self.num == 0:
319                 def create_mark():
320                     self.textbuffer.create_mark("1", self.enditer, True)
321                 gobject.idle_add(create_mark)
322
323             line_count = datfile.get_dat_line_count(
324                 self.bbs, self.board, self.thread)
325             if line_count > self.num:
326                 datfile.load_dat_partly(
327                     self.bbs, self.board, self.thread,
328                     self.append_rawres_to_buffer, self.num+1)
329
330                 def do_jump(num):
331                     if self.jump_request_num:
332                         if self.jump_request_num <= num:
333                             # jump if enable, otherwize jump later.
334                             num = self.jump_request_num
335                             self.jump_request_num = 0
336                             mark = self.textbuffer.get_mark(str(num))
337                             if mark:
338                                 self.textview.scroll_to_mark(
339                                     mark, 0, True, 0, 0)
340                     else:
341                         self.jump_to_the_end(num)
342
343                 gobject.idle_add(do_jump, self.num)
344
345         def get():
346             dat_path = misc.get_thread_dat_path(
347                 self.bbs, self.board, self.thread)
348             dat_file = FileWrap(dat_path)
349
350             def save_line_and_append_to_buffer(line):
351                 dat_file.seek(self.size)
352                 dat_file.write(line)
353                 self.append_rawres_to_buffer(line)
354
355             self.http_get_dat(save_line_and_append_to_buffer)
356             dat_file.close()
357
358             def do_jump():
359                 if self.jump_request_num:
360                     num = self.jump_request_num
361                     self.jump_request_num = 0
362                     mark = self.textbuffer.get_mark(str(num))
363                     if mark:
364                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
365
366             gobject.idle_add(do_jump)
367
368         if self.lock():
369
370             def on_end():
371                 self.un_lock()
372                 self.progress = False
373
374             self.progress = True
375             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
376             t.start()
377
378     def load_dat(self):
379
380         self.size = 0
381         self.num = 0
382         self.jump_request_num = 0
383
384         def load():
385
386             def create_mark():
387                 self.textbuffer.create_mark("1", self.enditer, True)
388             gobject.idle_add(create_mark)
389
390             datfile.load_dat(self.bbs, self.board, self.thread,
391                              self.append_rawres_to_buffer)
392         def jump():
393
394             def do_jump(num):
395                 if self.jump_request_num:
396                     num = self.jump_request_num
397                     self.jump_request_num = 0
398                     mark = self.textbuffer.get_mark(str(num))
399                     if mark:
400                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
401                 else:
402                     self.jump_to_the_end(num)
403
404             gobject.idle_add(do_jump, self.num)
405
406         if self.lock():
407
408             def on_end():
409                 self.un_lock()
410                 self.progress = False
411
412             self.progress = True
413             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
414             t.start()
415
416     def append_rawres_to_buffer(self, line):
417         self.size += len(line)
418         self.num += 1
419
420         if not self.title and self.num == 1:
421             title = datfile.do_get_title_from_dat(line)
422             if title:
423                 self.title = title
424                 gobject.idle_add(self.window.set_title, title)
425
426         h = lambda name,mail,date,msg: self.reselems_to_buffer(
427             self.num, name, mail, date, msg)
428
429         self.res_queue = []
430         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
431
432         def process_res_queue(res_queue, num):
433             self.process_queue(res_queue)
434             # for next res
435             self.textbuffer.create_mark(str(num+1), self.enditer, True)
436
437         gobject.idle_add(
438             process_res_queue, self.res_queue, self.num)
439
440     def reselems_to_buffer(self, num, name, mail, date, msg):
441         p = barehtmlparser.BareHTMLParser(
442             lambda d,b,h: self.res_queue.append((d,b,h,False)))
443         # number
444         p.feed(str(num) + " ")
445
446         # name
447         p.feed("<b>" + name + "</b>")
448
449         # mail
450         p.feed("[" + mail + "]")
451
452         # date
453         p.feed(date)
454         p.feed("<br>")
455
456         # msg
457         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
458         p.feed(msg.lstrip(" "))
459
460         p.feed("<br><br>")
461         p.close()
462
463     def href_tag(self, href):
464         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
465         tag.set_data("href", href)
466         return tag
467
468     def process_queue(self, queue):
469         for data, bold, href, margin in queue:
470             taglist = []
471             if bold:
472                 taglist.append(self.boldtag)
473             if href:
474                 taglist.append(self.href_tag(href))
475             if margin:
476                 taglist.append(self.leftmargintag)
477
478             if taglist:
479                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
480             else:
481                 self.textbuffer.insert(self.enditer, data)
482
483     def jump_to_the_end(self, num):
484         mark = self.textbuffer.get_mark(str(num+1))
485         if mark:
486             self.textview.scroll_to_mark(mark, 0)
487
488     def lock(self):
489         if self.lock_obj:
490             print "locked, try later."
491             return False
492         else:
493             print "get lock"
494             self.lock_obj = True
495             return True
496
497     def un_lock(self):
498         self.lock_obj = False
499         print "unlock"
500
501     def jump_to_res(self, uri):
502         strict_uri = self.bbs_type.get_thread_uri()
503         if uri != strict_uri and uri.startswith(strict_uri):
504             resnum = uri[len(strict_uri):]
505             match = re.match("\d+", resnum)
506             if match:
507                 resnum = match.group()
508                 mark = self.textbuffer.get_mark(resnum)
509                 if mark:
510                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
511                 elif self.progress:
512                     # try later.
513                     self.jump_request_num = int(resnum)
514
515     def load(self, update=False):
516         dat_path = misc.get_thread_dat_path(
517             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
518         dat_exists = os.path.exists(dat_path)
519         if update or not dat_exists:
520             self.update()
521         else:
522             self.load_dat()
523
524     def save(self):
525         try:
526             states_path = misc.get_thread_states_path(
527                 self.bbs_type.bbs_type, self.bbs_type.board,
528                 self.bbs_type.thread)
529             dat_path = misc.get_thread_dat_path(
530                 self.bbs_type.bbs_type, self.bbs_type.board,
531                 self.bbs_type.thread)
532
533             # save only if dat file exists.
534             if os.path.exists(dat_path):
535                 window_width, window_height = self.window.get_size()
536                 toolbar_visible = self.toolbar.parent.get_property("visible")
537                 statusbar_visible = self.statusbar.get_property("visible")
538
539                 dirname = os.path.dirname(states_path)
540                 if not os.path.isdir(dirname):
541                     os.makedirs(dirname)
542
543                 f = file(states_path, "w")
544
545                 f.write("window_width=" + str(window_width) + "\n")
546                 f.write("window_height=" + str(window_height) + "\n")
547                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
548                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
549
550                 f.close()
551         except:
552             traceback.print_exc()
553
554     def restore(self):
555         try:
556             window_height = 600
557             window_width = 600
558             toolbar_visible = True
559             statusbar_visible = True
560
561             try:
562                 key_base = config.gconf_app_key_base() + "/thread_states"
563                 gconf_client = gconf.client_get_default()
564                 width = gconf_client.get_int(key_base + "/window_width")
565                 height = gconf_client.get_int(key_base + "/window_height")
566                 toolbar_visible = gconf_client.get_bool(
567                     key_base + "/toolbar")
568                 statusbar_visible = gconf_client.get_bool(
569                     key_base + "/statusbar")
570                 if width != 0:
571                     window_width = width
572                 if height != 0:
573                     window_height = height
574             except:
575                 traceback.print_exc()
576
577             states_path = misc.get_thread_states_path(
578                 self.bbs_type.bbs_type, self.bbs_type.board,
579                 self.bbs_type.thread)
580             if os.path.exists(states_path):
581                 for line in file(states_path):
582                     if line.startswith("window_height="):
583                         height = window_height
584                         try:
585                             height = int(
586                                 line[len("window_height="):].rstrip("\n"))
587                         except:
588                             pass
589                         else:
590                             window_height = height
591                     elif line.startswith("window_width="):
592                         width = window_width
593                         try:
594                             width = int(
595                                 line[len("window_width="):].rstrip("\n"))
596                         except:
597                             pass
598                         else:
599                             window_width = width
600                     elif line.startswith("toolbar_visible="):
601                         tbar = line[len("toolbar_visible="):].rstrip("\n")
602                         toolbar_visible = tbar == "True"
603                     elif line.startswith("statusbar_visible="):
604                         sbar = line[len("statusbar_visible="):].rstrip("\n")
605                         statusbar_visible = sbar == "True"
606
607             self.window.set_default_size(window_width, window_height)
608
609             if not toolbar_visible:
610                 gobject.idle_add(self.toolbar.parent.hide)
611             if not statusbar_visible:
612                 gobject.idle_add(self.statusbar.hide)
613         except:
614             traceback.print_exc()