1 # Copyright (C) 2006 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
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.
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.
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
38 from misc import FileWrap, ThreadInvoker
45 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
46 from BbsType import bbs_type_judge_uri
47 from BbsType import bbs_type_exception
51 import bookmark_window
53 GLADE_FILENAME = "thread_window.glade"
55 def open_thread(uri, update=False):
57 raise ValueError, "parameter must not be empty"
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
65 winwrap = session.get_window(uri)
68 winwrap.window.present()
72 winwrap = WinWrap(bbs_type.uri) # pass original uri
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)
82 resnum = int(match.group())
83 winwrap.jump_to_res(resnum)
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)
92 width = self.drawingarea.allocation.width
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()
99 self.adjustment.upper = sum_height
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],
106 self.drawingarea.allocation.width,
107 self.drawingarea.allocation.height)
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
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)
120 def on_drawingarea_expose_event(self, widget, event, data=None):
123 def on_drawingarea_size_allocate(self, widget, allocation, data=None):
124 if allocation.width != self.drawingarea.prev_width:
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
130 def on_drawingarea_button_press_event(self, widget, event, data=None):
131 self.drawingarea.queue_draw()
133 def on_vscrollbar_value_changed(self, widget, data=None):
134 self.drawingarea.queue_draw()
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
147 def __init__(self, uri):
148 self.pangolayout = []
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
159 self.lock_obj = False
160 self.jump_request_num = 0
161 self.progress = False
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
174 self.drawingarea.prev_width = 0
176 self.initialize_buffer()
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)
205 self.window.show_all()
209 def initialize_buffer(self):
210 self.textbuffer = gtk.TextBuffer()
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)
217 self.pangolayout = []
221 self.window.destroy()
224 return self.bbs_type.get_thread_uri()
226 def on_compose_clicked(self, widget):
228 submit_window.open(self.bbs_type.get_thread_uri())
230 def on_toolbar_activate(self, widget):
231 if self.toolbar.get_property("visible"):
236 def on_statusbar_activate(self, widget):
237 if self.statusbar.get_property("visible"):
238 self.statusbar.hide()
240 self.statusbar.show()
242 def on_close_activate(self, widget):
245 def on_thread_window_delete_event(self, widget, event):
249 def on_thread_window_destroy(self, widget):
252 def on_quit_activate(self, widget):
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)
259 def on_manage_bookmarks_activate(self, widget):
260 bookmark_window.open()
262 def on_show_board_activate(self, widget):
263 board_window.open_board(self.bbs_type.get_uri_base())
265 def on_delete_activate(self, widget):
267 dat_path = misc.get_thread_dat_path(self.bbs_type)
270 traceback.print_exc()
272 idx_path = misc.get_thread_idx_path(self.bbs_type)
275 traceback.print_exc()
277 states_path = misc.get_thread_states_path(self.bbs_type)
278 os.remove(states_path)
280 traceback.print_exc()
282 def http_get_dat(self, on_get_res):
283 datfile_url = self.bbs_type.get_dat_uri()
285 idx_dic = idxfile.load_idx(self.bbs_type)
286 lastmod = idx_dic["lastModified"]
287 etag = idx_dic["etag"]
289 req = urllib2.Request(datfile_url)
290 req.add_header("User-agent", config.User_Agent)
292 req.add_header("Range", "bytes=" + str(self.size) + "-")
294 req.add_header("If-Modified-Since", lastmod)
296 req.add_header("If-None-Match", etag)
298 req = self.bbs_type.set_extra_dat_request(req, self)
300 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
302 res = opener.open(req)
303 except urllib2.HTTPError, e:
306 # lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
310 # lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
312 maybe_incomplete = False
314 if not line.endswith("\n"):
315 maybe_incomplete = True
316 print "does not end with \\n. maybe incomplete"
326 if "Last-Modified" in headers:
327 lastmod = headers["Last-Modified"]
328 if "ETag" in headers:
329 etag = headers["Etag"]
333 idx_dic = {"title": self.title, "lineCount": self.num,
334 "lastModified": lastmod, "etag": etag}
335 idxfile.save_idx(self.bbs_type, idx_dic)
337 gobject.idle_add(session.thread_idx_updated,
338 self.bbs_type.get_thread_uri(), idx_dic)
340 def update(self, widget=None):
342 self.jump_request_num = 0
347 self.textbuffer.create_mark("1", self.enditer, True)
348 gobject.idle_add(create_mark)
350 line_count = datfile.get_dat_line_count(self.bbs_type)
351 if line_count < self.num:
355 gobject.idle_add(self.initialize_buffer)
357 if line_count > self.num:
358 datfile.load_dat_partly(
359 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
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)
368 self.jump_to_the_end()
370 gobject.idle_add(do_jump)
373 dat_path = misc.get_thread_dat_path(self.bbs_type)
374 dat_file = FileWrap(dat_path)
376 def save_line_and_append_to_buffer(line):
377 dat_file.seek(self.size)
379 self.append_rawres_to_buffer(line)
381 self.http_get_dat(save_line_and_append_to_buffer)
385 if self.jump_request_num:
386 num = self.jump_request_num
387 self.jump_request_num = 0
388 self.jump_to_res(num)
390 gobject.idle_add(do_jump)
396 self.progress = False
399 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
406 self.jump_request_num = 0
409 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
414 if self.jump_request_num:
415 num = self.jump_request_num
416 self.jump_request_num = 0
417 self.jump_to_res(num)
419 self.jump_to_the_end()
421 gobject.idle_add(do_jump)
427 self.progress = False
430 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
433 def append_rawres_to_buffer(self, line):
434 self.size += len(line)
437 if not self.title and self.num == 1:
438 title = self.bbs_type.get_title_from_dat(line)
441 gobject.idle_add(self.window.set_title, title)
445 line = line.decode(self.bbs_type.encoding, "replace")
446 m = self.bbs_type.dat_reg.match(line)
448 name = m.group("name")
449 mail = m.group("mail")
450 date = m.group("date")
453 num = int(m.group("num"))
455 # use simple counter num
467 self.reselems_to_buffer(num, name, mail, date, msg)
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
473 def process_res_queue(res_queue, num):
474 self.process_queue(res_queue, num)
476 #self.textbuffer.create_mark(str(num+1), self.enditer, True)
479 process_res_queue, self.res_queue, self.num)
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)))
485 p.feed(str(num) + " ")
488 p.feed("<b>" + name + "</b>")
491 p.feed("[" + mail + "]")
498 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
499 p.feed(msg.lstrip(" "))
504 def href_tag(self, href):
505 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
506 tag.set_data("href", href)
509 def process_queue(self, queue, num):
511 for data, bold, href, margin in queue:
513 layout = self.drawingarea.create_pango_layout(text)
514 layout.set_wrap(pango.WRAP_CHAR)
517 self.pangolayout.append(layout)
519 self.drawingarea.queue_draw()
522 # taglist.append(self.boldtag)
524 # taglist.append(self.href_tag(href))
526 # taglist.append(self.leftmargintag)
529 # self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
531 # self.textbuffer.insert(self.enditer, data)
533 def jump(self, value):
535 if value > self.adjustment.upper - self.adjustment.page_size:
536 self.jump_to_the_end()
538 self.adjustment.set_value(value)
541 def jump_to_layout(self, layout):
542 gobject.idle_add(lambda : self.jump(layout.posY))
544 def jump_to_the_end(self):
546 self.adjustment.set_value(
547 self.adjustment.upper - self.adjustment.page_size)
549 # mark = self.textbuffer.get_mark(str(num+1))
551 # self.textview.scroll_to_mark(mark, 0)
555 print "locked, try later."
563 self.lock_obj = False
566 def jump_to_res(self, resnum):
567 for layout in self.pangolayout:
568 if layout.resnum == resnum:
569 self.jump_to_layout(layout)
571 self.jump_request_num = resnum
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:
583 states_path = misc.get_thread_states_path(self.bbs_type)
584 dat_path = misc.get_thread_dat_path(self.bbs_type)
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")
592 dirname = os.path.dirname(states_path)
593 if not os.path.isdir(dirname):
596 f = file(states_path, "w")
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")
605 traceback.print_exc()
611 toolbar_visible = True
612 statusbar_visible = True
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")
626 window_height = height
628 traceback.print_exc()
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
637 line[len("window_height="):].rstrip("\n"))
641 window_height = height
642 elif line.startswith("window_width="):
646 line[len("window_width="):].rstrip("\n"))
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"
658 self.window.set_default_size(window_width, window_height)
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)
667 traceback.print_exc()