OSDN Git Service

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