OSDN Git Service

replace scrolled window and viewport with hbox and scrollbar
[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 * 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, 0, 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         for data, bold, href, margin in queue:
512             text += data
513         layout = self.drawingarea.create_pango_layout(text)
514         layout.set_wrap(pango.WRAP_CHAR)
515         layout.posY = 0
516         layout.resnum = num
517         self.pangolayout.append(layout)
518         self.relayout()
519         self.drawingarea.queue_draw()
520 #             taglist = []
521 #             if bold:
522 #                 taglist.append(self.boldtag)
523 #             if href:
524 #                 taglist.append(self.href_tag(href))
525 #             if margin:
526 #                 taglist.append(self.leftmargintag)
527 #
528 #            if taglist:
529 #                self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
530 #            else:
531 #                self.textbuffer.insert(self.enditer, data)
532
533     def jump(self, value):
534         def j():
535             if value > self.adjustment.upper - self.adjustment.page_size:
536                 self.jump_to_the_end()
537             else:
538                 self.adjustment.set_value(value)
539         gobject.idle_add(j)
540
541     def jump_to_layout(self, layout):
542         gobject.idle_add(lambda : self.jump(layout.posY))
543         
544     def jump_to_the_end(self):
545         def j():
546             self.adjustment.set_value(
547                 self.adjustment.upper - self.adjustment.page_size)
548         gobject.idle_add(j)
549 #        mark = self.textbuffer.get_mark(str(num+1))
550 #        if mark:
551 #            self.textview.scroll_to_mark(mark, 0)
552
553     def lock(self):
554         if self.lock_obj:
555             print "locked, try later."
556             return False
557         else:
558             print "get lock"
559             self.lock_obj = True
560             return True
561
562     def un_lock(self):
563         self.lock_obj = False
564         print "unlock"
565
566     def jump_to_res(self, resnum):
567         for layout in self.pangolayout:
568             if layout.resnum == resnum:
569                 self.jump_to_layout(layout)
570                 return
571         self.jump_request_num = resnum
572
573     def load(self, update=False):
574         dat_path = misc.get_thread_dat_path(self.bbs_type)
575         dat_exists = os.path.exists(dat_path)
576         if update or not dat_exists:
577             self.update()
578         else:
579             self.load_dat()
580
581     def save(self):
582         try:
583             states_path = misc.get_thread_states_path(self.bbs_type)
584             dat_path = misc.get_thread_dat_path(self.bbs_type)
585
586             # save only if dat file exists.
587             if os.path.exists(dat_path):
588                 window_width, window_height = self.window.get_size()
589                 toolbar_visible = self.toolbar.get_property("visible")
590                 statusbar_visible = self.statusbar.get_property("visible")
591
592                 dirname = os.path.dirname(states_path)
593                 if not os.path.isdir(dirname):
594                     os.makedirs(dirname)
595
596                 f = file(states_path, "w")
597
598                 f.write("window_width=" + str(window_width) + "\n")
599                 f.write("window_height=" + str(window_height) + "\n")
600                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
601                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
602
603                 f.close()
604         except:
605             traceback.print_exc()
606
607     def restore(self):
608         try:
609             window_height = 600
610             window_width = 600
611             toolbar_visible = True
612             statusbar_visible = True
613
614             try:
615                 key_base = config.gconf_app_key_base() + "/thread_states"
616                 gconf_client = gconf.client_get_default()
617                 width = gconf_client.get_int(key_base + "/window_width")
618                 height = gconf_client.get_int(key_base + "/window_height")
619                 toolbar_visible = gconf_client.get_bool(
620                     key_base + "/toolbar")
621                 statusbar_visible = gconf_client.get_bool(
622                     key_base + "/statusbar")
623                 if width != 0:
624                     window_width = width
625                 if height != 0:
626                     window_height = height
627             except:
628                 traceback.print_exc()
629
630             states_path = misc.get_thread_states_path(self.bbs_type)
631             if os.path.exists(states_path):
632                 for line in file(states_path):
633                     if line.startswith("window_height="):
634                         height = window_height
635                         try:
636                             height = int(
637                                 line[len("window_height="):].rstrip("\n"))
638                         except:
639                             pass
640                         else:
641                             window_height = height
642                     elif line.startswith("window_width="):
643                         width = window_width
644                         try:
645                             width = int(
646                                 line[len("window_width="):].rstrip("\n"))
647                         except:
648                             pass
649                         else:
650                             window_width = width
651                     elif line.startswith("toolbar_visible="):
652                         tbar = line[len("toolbar_visible="):].rstrip("\n")
653                         toolbar_visible = tbar == "True"
654                     elif line.startswith("statusbar_visible="):
655                         sbar = line[len("statusbar_visible="):].rstrip("\n")
656                         statusbar_visible = sbar == "True"
657
658             self.window.set_default_size(window_width, window_height)
659
660             if not toolbar_visible:
661                 gobject.idle_add(self.toolbar.hide,
662                                  priority=gobject.PRIORITY_HIGH)
663             if not statusbar_visible:
664                 gobject.idle_add(self.statusbar.hide,
665                                  priority=gobject.PRIORITY_HIGH)
666         except:
667             traceback.print_exc()