OSDN Git Service

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