OSDN Git Service

decoration, bold, margin left, uri
[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 itertools
34 import os
35 import sys
36
37 import misc
38 from misc import FileWrap, ThreadInvoker
39 import datfile
40 import barehtmlparser
41 import idxfile
42 import session
43 import board_window
44 import uri_opener
45 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
46 from BbsType import bbs_type_judge_uri
47 from BbsType import bbs_type_exception
48 import config
49 import winwrapbase
50 import bookmark_list
51 import bookmark_window
52
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     strict_uri = winwrap.bbs_type.get_thread_uri()
77     if (winwrap.bbs_type.uri != strict_uri and
78         winwrap.bbs_type.uri.startswith(strict_uri)):
79         resnum = winwrap.bbs_type.uri[len(strict_uri):]
80         match = re.match("\d+", resnum)
81         if match:
82             resnum = int(match.group())
83             winwrap.jump_to_res(resnum)
84
85 class WinWrap(winwrapbase.WinWrapBase):
86     hovering_over_link = False
87     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
88     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
89
90
91     def relayout(self):
92         width = self.drawingarea.allocation.width
93         sum_height = 0
94         for layout in self.pangolayout:
95             layout.set_width((width - layout.marginleft) * pango.SCALE)
96             layout.posY = sum_height
97             x, y = layout.get_pixel_size()
98             sum_height += y
99         self.adjustment.upper = sum_height
100
101     def draw_viewport(self):
102         view_y = self.adjustment.get_value()
103         self.drawingarea.window.draw_rectangle(
104             self.drawingarea.style.base_gc[0],
105             True, 0, 0,
106             self.drawingarea.allocation.width,
107             self.drawingarea.allocation.height)
108
109         gc = self.drawingarea.window.new_gc()
110         for layout in self.pangolayout:
111             w, h = layout.get_pixel_size()
112             layout_top = layout.posY
113             layout_bottom = layout.posY + h
114             area_top = view_y
115             area_bottom = view_y + self.drawingarea.allocation.height
116             if layout_top <= area_bottom and layout_bottom >= area_top:
117                 self.drawingarea.window.draw_layout(
118                     gc, layout.marginleft, layout.posY - int(view_y), layout)
119
120     def on_drawingarea_expose_event(self, widget, event, data=None):
121         self.draw_viewport()
122
123     def on_drawingarea_size_allocate(self, widget, allocation, data=None):
124         if allocation.width != self.drawingarea.prev_width:
125             self.relayout()
126             self.drawingarea.prev_width = allocation.width
127         self.adjustment.page_size = self.drawingarea.allocation.height
128         self.adjustment.page_increment = self.drawingarea.allocation.height
129
130     def on_drawingarea_button_press_event(self, widget, event, data=None):
131         self.drawingarea.queue_draw()
132
133     def on_vscrollbar_value_changed(self, widget, data=None):
134         self.drawingarea.queue_draw()
135
136     def on_drawingarea_scroll_event(self, widget, event, data=None):
137         if event.direction == gtk.gdk.SCROLL_UP:
138             self.adjustment.value -= 66.476200804
139             if self.adjustment.value < self.adjustment.lower:
140                 self.adjustment.value = self.adjustment.lower
141         if event.direction == gtk.gdk.SCROLL_DOWN:
142             self.adjustment.value += 66.476200804
143             max_value = self.adjustment.upper - self.adjustment.page_size
144             if self.adjustment.value > max_value:
145                 self.adjustment.value = max_value
146
147     def __init__(self, uri):
148         self.pangolayout = []
149
150         from BbsType import bbs_type_judge_uri
151         from BbsType import bbs_type_exception
152         self.bbs_type = bbs_type_judge_uri.get_type(uri)
153         if not self.bbs_type.is_thread():
154             raise bbs_type_exception.BbsTypeError, \
155                   "the uri does not represent thread: " + uri
156         self.size = 0
157         self.num = 0
158         self.title = ""
159         self.lock_obj = False
160         self.jump_request_num = 0
161         self.progress = False
162
163         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
164         self.widget_tree = gtk.glade.XML(glade_path)
165         self.window = self.widget_tree.get_widget("thread_window")
166         self.toolbar = self.widget_tree.get_widget("toolbar")
167         self.toolbar.unset_style()
168         self.statusbar = self.widget_tree.get_widget("statusbar")
169         self.drawingarea = self.widget_tree.get_widget("drawingarea")
170         self.vscrollbar = self.widget_tree.get_widget("vscrollbar")
171         self.adjustment = self.vscrollbar.get_adjustment()
172         self.adjustment.step_increment = 20
173
174         self.drawingarea.prev_width = 0
175
176         self.initialize_buffer()
177
178         sigdic = {"on_refresh_activate": self.update,
179                   "on_compose_activate": self.on_compose_clicked,
180                   "on_toolbar_activate": self.on_toolbar_activate,
181                   "on_statusbar_activate": self.on_statusbar_activate,
182                   "on_refresh_activate": self.update,
183                   "on_close_activate": self.on_close_activate,
184                   "on_quit_activate": self.on_quit_activate,
185                   "on_show_board_activate": self.on_show_board_activate,
186                   "on_delete_activate": self.on_delete_activate,
187                   "on_drawingarea_expose_event": self.on_drawingarea_expose_event,
188                   "on_drawingarea_size_allocate":
189                   self.on_drawingarea_size_allocate,
190                   "on_thread_window_delete_event":
191                   self.on_thread_window_delete_event,
192                   "on_drawingarea_button_press_event":
193                   self.on_drawingarea_button_press_event,
194                   "on_drawingarea_scroll_event":
195                   self.on_drawingarea_scroll_event,
196                   "on_vscrollbar_value_changed":
197                   self.on_vscrollbar_value_changed,
198                   "on_add_bookmark_activate": self.on_add_bookmark_activate,
199                   "on_manage_bookmarks_activate": \
200                   self.on_manage_bookmarks_activate,
201                   "on_thread_window_destroy": self.on_thread_window_destroy}
202         self.widget_tree.signal_autoconnect(sigdic)
203
204         self.restore()
205         self.window.show_all()
206
207         self.created()
208
209     def initialize_buffer(self):
210         self.textbuffer = gtk.TextBuffer()
211
212         self.enditer = self.textbuffer.get_end_iter()
213         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
214         self.leftmargintag = self.textbuffer.create_tag()
215         self.leftmargintag.set_property("left-margin", 20)
216
217         self.pangolayout = []
218
219     def destroy(self):
220         self.save()
221         self.window.destroy()
222
223     def get_uri(self):
224         return self.bbs_type.get_thread_uri()
225
226     def on_compose_clicked(self, widget):
227         import submit_window
228         submit_window.open(self.bbs_type.get_thread_uri())
229
230     def on_toolbar_activate(self, widget):
231         if self.toolbar.get_property("visible"):
232             self.toolbar.hide()
233         else:
234             self.toolbar.show()
235
236     def on_statusbar_activate(self, widget):
237         if self.statusbar.get_property("visible"):
238             self.statusbar.hide()
239         else:
240             self.statusbar.show()
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             pass
305 #             gobject.idle_add(
306 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
307         else:
308             headers = res.info()
309 #             gobject.idle_add(
310 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
311
312             maybe_incomplete = False
313             for line in res:
314                 if not line.endswith("\n"):
315                     maybe_incomplete = True
316                     print "does not end with \\n. maybe incomplete"
317                     break
318                 on_get_res(line)
319
320             res.close()
321
322             if maybe_incomplete:
323                 lastmod = None
324                 etag = None
325             else:
326                 if "Last-Modified" in headers:
327                     lastmod = headers["Last-Modified"]
328                 if "ETag" in headers:
329                     etag = headers["Etag"]
330
331             if self.num > 0:
332                 # save idx
333                 idx_dic = {"title": self.title, "lineCount": self.num,
334                        "lastModified": lastmod, "etag": etag}
335                 idxfile.save_idx(self.bbs_type, idx_dic)
336
337                 gobject.idle_add(session.thread_idx_updated,
338                                  self.bbs_type.get_thread_uri(), idx_dic)
339
340     def update(self, widget=None):
341
342         self.jump_request_num = 0
343
344         def load():
345             if self.num == 0:
346                 def create_mark():
347                     self.textbuffer.create_mark("1", self.enditer, True)
348                 gobject.idle_add(create_mark)
349
350             line_count = datfile.get_dat_line_count(self.bbs_type)
351             if line_count < self.num:
352                 self.num = 0
353                 self.size = 0
354
355                 gobject.idle_add(self.initialize_buffer)
356
357             if line_count > self.num:
358                 datfile.load_dat_partly(
359                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
360
361                 def do_jump():
362                     if self.jump_request_num:
363                         if self.jump_request_num <= num:
364                             num = self.jump_request_num
365                             self.jump_request_num = 0
366                             self.jump_to_res(num)
367                     else:
368                         self.jump_to_the_end()
369
370                 gobject.idle_add(do_jump)
371
372         def get():
373             dat_path = misc.get_thread_dat_path(self.bbs_type)
374             dat_file = FileWrap(dat_path)
375
376             def save_line_and_append_to_buffer(line):
377                 dat_file.seek(self.size)
378                 dat_file.write(line)
379                 self.append_rawres_to_buffer(line)
380
381             self.http_get_dat(save_line_and_append_to_buffer)
382             dat_file.close()
383
384             def do_jump():
385                 if self.jump_request_num:
386                     num = self.jump_request_num
387                     self.jump_request_num = 0
388                     self.jump_to_res(num)
389
390             gobject.idle_add(do_jump)
391
392         if self.lock():
393
394             def on_end():
395                 self.un_lock()
396                 self.progress = False
397
398             self.progress = True
399             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
400             t.start()
401
402     def load_dat(self):
403
404         self.size = 0
405         self.num = 0
406         self.jump_request_num = 0
407
408         def load():
409             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
410
411         def jump():
412
413             def do_jump():
414                 if self.jump_request_num:
415                     num = self.jump_request_num
416                     self.jump_request_num = 0
417                     self.jump_to_res(num)
418                 else:
419                     self.jump_to_the_end()
420
421             gobject.idle_add(do_jump)
422
423         if self.lock():
424
425             def on_end():
426                 self.un_lock()
427                 self.progress = False
428
429             self.progress = True
430             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
431             t.start()
432
433     def append_rawres_to_buffer(self, line):
434         self.size += len(line)
435         self.num += 1
436
437         if not self.title and self.num == 1:
438             title = self.bbs_type.get_title_from_dat(line)
439             if title:
440                 self.title = title
441                 gobject.idle_add(self.window.set_title, title)
442
443         self.res_queue = []
444
445         line = line.decode(self.bbs_type.encoding, "replace")
446         m = self.bbs_type.dat_reg.match(line)
447         if m:
448             name = m.group("name")
449             mail = m.group("mail")
450             date = m.group("date")
451             msg = m.group("msg")
452             try:
453                 num = int(m.group("num"))
454             except IndexError:
455                 # use simple counter num
456                 num = self.num
457             else:
458                 # use num in dat
459                 self.num = num
460             try:
461                 id = m.group("id")
462             except IndexError:
463                 pass
464             else:
465                 if id:
466                     date += " ID:" + id
467             self.reselems_to_buffer(num, name, mail, date, msg)
468         else:
469             self.res_queue.append((str(self.num)+"\n", False, None, False))
470             self.res_queue.append((line, False, None, True))
471             print "maybe syntax error.", self.num, line
472
473         def process_res_queue(res_queue, num):
474             self.process_queue(res_queue, num)
475             # for next res
476             #self.textbuffer.create_mark(str(num+1), self.enditer, True)
477
478         gobject.idle_add(
479             process_res_queue, self.res_queue, self.num)
480
481     def reselems_to_buffer(self, num, name, mail, date, msg):
482         p = barehtmlparser.BareHTMLParser(
483             lambda d,b,h: self.res_queue.append((d,b,h,False)))
484         # number
485         p.feed(str(num) + " ")
486
487         # name
488         p.feed("<b>" + name + "</b>")
489
490         # mail
491         p.feed("[" + mail + "]")
492
493         # date
494         p.feed(date)
495         p.feed("<br>")
496
497         # msg
498         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
499         p.feed(msg.lstrip(" "))
500
501         p.feed("<br><br>")
502         p.close()
503
504     def href_tag(self, href):
505         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
506         tag.set_data("href", href)
507         return tag
508
509     def process_queue(self, queue, num):
510         text = ""
511         current_margin = False
512         attrlist = pango.AttrList()
513         for data, bold, href, margin in queue:
514             data = data.encode("utf8")
515             if current_margin != margin:
516                 layout = self.drawingarea.create_pango_layout(text)
517                 layout.set_wrap(pango.WRAP_CHAR)
518                 layout.posY = 0
519                 layout.resnum = num
520                 layout.set_attributes(attrlist)
521                 if current_margin:
522                     layout.marginleft = 20
523                 else:
524                     layout.marginleft = 0
525
526                 self.pangolayout.append(layout)
527
528                 current_margin = margin
529                 text = ""
530                 attrlist = pango.AttrList()
531
532             if bold:
533                 attrlist.insert(pango.AttrWeight(
534                     pango.WEIGHT_BOLD, len(text), len(text) + len(data)))
535             if href:
536                 attrlist.insert(pango.AttrUnderline(
537                     pango.UNDERLINE_SINGLE, len(text), len(text) + len(data)))
538
539             text += data
540
541         if text != "":
542             layout = self.drawingarea.create_pango_layout(text)
543             layout.set_wrap(pango.WRAP_CHAR)
544             layout.posY = 0
545             layout.resnum = num
546             layout.set_attributes(attrlist)
547             if current_margin:
548                 layout.marginleft = 20
549             else:
550                 layout.marginleft = 0
551         self.pangolayout.append(layout)
552         self.relayout()
553         self.drawingarea.queue_draw()
554 #             taglist = []
555 #             if bold:
556 #                 taglist.append(self.boldtag)
557 #             if href:
558 #                 taglist.append(self.href_tag(href))
559 #             if margin:
560 #                 taglist.append(self.leftmargintag)
561 #
562 #            if taglist:
563 #                self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
564 #            else:
565 #                self.textbuffer.insert(self.enditer, data)
566
567     def jump(self, value):
568         def j():
569             if value > self.adjustment.upper - self.adjustment.page_size:
570                 self.jump_to_the_end()
571             else:
572                 self.adjustment.set_value(value)
573         gobject.idle_add(j)
574
575     def jump_to_layout(self, layout):
576         gobject.idle_add(lambda : self.jump(layout.posY))
577         
578     def jump_to_the_end(self):
579         def j():
580             self.adjustment.set_value(
581                 self.adjustment.upper - self.adjustment.page_size)
582         gobject.idle_add(j)
583 #        mark = self.textbuffer.get_mark(str(num+1))
584 #        if mark:
585 #            self.textview.scroll_to_mark(mark, 0)
586
587     def lock(self):
588         if self.lock_obj:
589             print "locked, try later."
590             return False
591         else:
592             print "get lock"
593             self.lock_obj = True
594             return True
595
596     def un_lock(self):
597         self.lock_obj = False
598         print "unlock"
599
600     def jump_to_res(self, resnum):
601         for layout in self.pangolayout:
602             if layout.resnum == resnum:
603                 self.jump_to_layout(layout)
604                 return
605         self.jump_request_num = resnum
606
607     def load(self, update=False):
608         dat_path = misc.get_thread_dat_path(self.bbs_type)
609         dat_exists = os.path.exists(dat_path)
610         if update or not dat_exists:
611             self.update()
612         else:
613             self.load_dat()
614
615     def save(self):
616         try:
617             states_path = misc.get_thread_states_path(self.bbs_type)
618             dat_path = misc.get_thread_dat_path(self.bbs_type)
619
620             # save only if dat file exists.
621             if os.path.exists(dat_path):
622                 window_width, window_height = self.window.get_size()
623                 toolbar_visible = self.toolbar.get_property("visible")
624                 statusbar_visible = self.statusbar.get_property("visible")
625
626                 dirname = os.path.dirname(states_path)
627                 if not os.path.isdir(dirname):
628                     os.makedirs(dirname)
629
630                 f = file(states_path, "w")
631
632                 f.write("window_width=" + str(window_width) + "\n")
633                 f.write("window_height=" + str(window_height) + "\n")
634                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
635                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
636
637                 f.close()
638         except:
639             traceback.print_exc()
640
641     def restore(self):
642         try:
643             window_height = 600
644             window_width = 600
645             toolbar_visible = True
646             statusbar_visible = True
647
648             try:
649                 key_base = config.gconf_app_key_base() + "/thread_states"
650                 gconf_client = gconf.client_get_default()
651                 width = gconf_client.get_int(key_base + "/window_width")
652                 height = gconf_client.get_int(key_base + "/window_height")
653                 toolbar_visible = gconf_client.get_bool(
654                     key_base + "/toolbar")
655                 statusbar_visible = gconf_client.get_bool(
656                     key_base + "/statusbar")
657                 if width != 0:
658                     window_width = width
659                 if height != 0:
660                     window_height = height
661             except:
662                 traceback.print_exc()
663
664             states_path = misc.get_thread_states_path(self.bbs_type)
665             if os.path.exists(states_path):
666                 for line in file(states_path):
667                     if line.startswith("window_height="):
668                         height = window_height
669                         try:
670                             height = int(
671                                 line[len("window_height="):].rstrip("\n"))
672                         except:
673                             pass
674                         else:
675                             window_height = height
676                     elif line.startswith("window_width="):
677                         width = window_width
678                         try:
679                             width = int(
680                                 line[len("window_width="):].rstrip("\n"))
681                         except:
682                             pass
683                         else:
684                             window_width = width
685                     elif line.startswith("toolbar_visible="):
686                         tbar = line[len("toolbar_visible="):].rstrip("\n")
687                         toolbar_visible = tbar == "True"
688                     elif line.startswith("statusbar_visible="):
689                         sbar = line[len("statusbar_visible="):].rstrip("\n")
690                         statusbar_visible = sbar == "True"
691
692             self.window.set_default_size(window_width, window_height)
693
694             if not toolbar_visible:
695                 gobject.idle_add(self.toolbar.hide,
696                                  priority=gobject.PRIORITY_HIGH)
697             if not statusbar_visible:
698                 gobject.idle_add(self.statusbar.hide,
699                                  priority=gobject.PRIORITY_HIGH)
700         except:
701             traceback.print_exc()