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 winwrap.jump_to_res(bbs_type.uri)
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)
86 width = self.drawingarea.allocation.width
88 for layout in self.pangolayout:
89 layout.set_width(width * pango.SCALE)
90 layout.posY = sum_height
91 x, y = layout.get_pixel_size()
93 self.drawingarea.set_size_request(-1, sum_height)
95 def draw_viewport(self, area):
96 gc = self.drawingarea.window.new_gc()
97 self.drawingarea.window.draw_rectangle(
98 self.drawingarea.style.base_gc[0],
99 True, area.x, area.y, area.width, area.height)
101 for layout in self.pangolayout:
102 w, h = layout.get_pixel_size()
103 if ((layout.posY >= area.y and
104 layout.posY < area.y + area.height) or
105 (layout.posY + h >= area.y and
106 layout.posY + h < area.y + area.height) or
107 (layout.posY <= area.y and layout.posY + h >= area.y)):
108 self.drawingarea.window.draw_layout(gc, 0, layout.posY, layout)
110 def on_drawingarea_expose_event(self, widget, event, data=None):
111 self.draw_viewport(event.area)
114 def on_drawingarea_size_allocate(self, widget, allocation, data=None):
115 if allocation.width != self.drawingarea.prev_width:
117 self.drawingarea.prev_width = allocation.width
120 def __init__(self, uri):
121 self.pangolayout = []
123 from BbsType import bbs_type_judge_uri
124 from BbsType import bbs_type_exception
125 self.bbs_type = bbs_type_judge_uri.get_type(uri)
126 if not self.bbs_type.is_thread():
127 raise bbs_type_exception.BbsTypeError, \
128 "the uri does not represent thread: " + uri
132 self.lock_obj = False
133 self.jump_request_num = 0
134 self.progress = False
136 glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
137 self.widget_tree = gtk.glade.XML(glade_path)
138 self.window = self.widget_tree.get_widget("thread_window")
139 self.toolbar = self.widget_tree.get_widget("toolbar")
140 self.toolbar.unset_style()
141 self.statusbar = self.widget_tree.get_widget("statusbar")
142 self.drawingarea = self.widget_tree.get_widget("drawingarea")
143 self.viewport = self.drawingarea.parent
145 self.drawingarea.prev_width = 0
147 self.initialize_buffer()
149 sigdic = {"on_refresh_activate": self.update,
150 "on_compose_activate": self.on_compose_clicked,
151 "on_toolbar_activate": self.on_toolbar_activate,
152 "on_statusbar_activate": self.on_statusbar_activate,
153 "on_refresh_activate": self.update,
154 "on_close_activate": self.on_close_activate,
155 "on_quit_activate": self.on_quit_activate,
156 "on_show_board_activate": self.on_show_board_activate,
157 "on_delete_activate": self.on_delete_activate,
158 "on_drawingarea_expose_event": self.on_drawingarea_expose_event,
159 "on_drawingarea_size_allocate":
160 self.on_drawingarea_size_allocate,
161 "on_thread_window_delete_event":
162 self.on_thread_window_delete_event,
163 "on_add_bookmark_activate": self.on_add_bookmark_activate,
164 "on_manage_bookmarks_activate": \
165 self.on_manage_bookmarks_activate,
166 "on_thread_window_destroy": self.on_thread_window_destroy}
167 self.widget_tree.signal_autoconnect(sigdic)
170 self.window.show_all()
174 def initialize_buffer(self):
175 self.textbuffer = gtk.TextBuffer()
177 self.enditer = self.textbuffer.get_end_iter()
178 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
179 self.leftmargintag = self.textbuffer.create_tag()
180 self.leftmargintag.set_property("left-margin", 20)
186 self.window.destroy()
189 return self.bbs_type.get_thread_uri()
191 def on_compose_clicked(self, widget):
193 submit_window.open(self.bbs_type.get_thread_uri())
195 def on_toolbar_activate(self, widget):
196 if self.toolbar.get_property("visible"):
201 def on_statusbar_activate(self, widget):
202 if self.statusbar.get_property("visible"):
203 self.statusbar.hide()
205 self.statusbar.show()
207 def on_close_activate(self, widget):
210 def on_thread_window_delete_event(self, widget, event):
214 def on_thread_window_destroy(self, widget):
217 def on_quit_activate(self, widget):
220 def on_add_bookmark_activate(self, widget):
221 bookmark_list.bookmark_list.add_bookmark_with_edit(
222 name=self.title, uri=self.bbs_type.uri)
224 def on_manage_bookmarks_activate(self, widget):
225 bookmark_window.open()
227 def on_show_board_activate(self, widget):
228 board_window.open_board(self.bbs_type.get_uri_base())
230 def on_delete_activate(self, widget):
232 dat_path = misc.get_thread_dat_path(self.bbs_type)
235 traceback.print_exc()
237 idx_path = misc.get_thread_idx_path(self.bbs_type)
240 traceback.print_exc()
242 states_path = misc.get_thread_states_path(self.bbs_type)
243 os.remove(states_path)
245 traceback.print_exc()
247 def http_get_dat(self, on_get_res):
248 datfile_url = self.bbs_type.get_dat_uri()
250 idx_dic = idxfile.load_idx(self.bbs_type)
251 lastmod = idx_dic["lastModified"]
252 etag = idx_dic["etag"]
254 req = urllib2.Request(datfile_url)
255 req.add_header("User-agent", config.User_Agent)
257 req.add_header("Range", "bytes=" + str(self.size) + "-")
259 req.add_header("If-Modified-Since", lastmod)
261 req.add_header("If-None-Match", etag)
263 req = self.bbs_type.set_extra_dat_request(req, self)
265 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
267 res = opener.open(req)
268 except urllib2.HTTPError, e:
271 # lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
275 # lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
277 maybe_incomplete = False
279 if not line.endswith("\n"):
280 maybe_incomplete = True
281 print "does not end with \\n. maybe incomplete"
291 if "Last-Modified" in headers:
292 lastmod = headers["Last-Modified"]
293 if "ETag" in headers:
294 etag = headers["Etag"]
298 idx_dic = {"title": self.title, "lineCount": self.num,
299 "lastModified": lastmod, "etag": etag}
300 idxfile.save_idx(self.bbs_type, idx_dic)
302 gobject.idle_add(session.thread_idx_updated,
303 self.bbs_type.get_thread_uri(), idx_dic)
305 def update(self, widget=None):
307 self.jump_request_num = 0
312 self.textbuffer.create_mark("1", self.enditer, True)
313 gobject.idle_add(create_mark)
315 line_count = datfile.get_dat_line_count(self.bbs_type)
316 if line_count < self.num:
320 gobject.idle_add(self.initialize_buffer)
322 if line_count > self.num:
323 datfile.load_dat_partly(
324 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
327 if self.jump_request_num:
328 if self.jump_request_num <= num:
329 # jump if enable, otherwize jump later.
330 num = self.jump_request_num
331 self.jump_request_num = 0
332 mark = self.textbuffer.get_mark(str(num))
334 # self.textview.scroll_to_mark(
335 # mark, 0, True, 0, 0)
337 self.jump_to_the_end(num)
339 gobject.idle_add(do_jump, self.num)
342 dat_path = misc.get_thread_dat_path(self.bbs_type)
343 dat_file = FileWrap(dat_path)
345 def save_line_and_append_to_buffer(line):
346 dat_file.seek(self.size)
348 self.append_rawres_to_buffer(line)
350 self.http_get_dat(save_line_and_append_to_buffer)
354 if self.jump_request_num:
355 num = self.jump_request_num
356 self.jump_request_num = 0
357 mark = self.textbuffer.get_mark(str(num))
359 # self.textview.scroll_to_mark(mark, 0, True, 0, 0)
361 gobject.idle_add(do_jump)
367 self.progress = False
370 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
377 self.jump_request_num = 0
382 self.textbuffer.create_mark("1", self.enditer, True)
383 gobject.idle_add(create_mark)
385 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
389 if self.jump_request_num:
390 num = self.jump_request_num
391 self.jump_request_num = 0
392 mark = self.textbuffer.get_mark(str(num))
394 # self.textview.scroll_to_mark(mark, 0, True, 0, 0)
396 self.jump_to_the_end(num)
398 gobject.idle_add(do_jump, self.num)
404 self.progress = False
407 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
410 def append_rawres_to_buffer(self, line):
411 self.size += len(line)
414 if not self.title and self.num == 1:
415 title = self.bbs_type.get_title_from_dat(line)
418 gobject.idle_add(self.window.set_title, title)
422 line = line.decode(self.bbs_type.encoding, "replace")
423 m = self.bbs_type.dat_reg.match(line)
425 name = m.group("name")
426 mail = m.group("mail")
427 date = m.group("date")
430 num = int(m.group("num"))
432 # use simple counter num
444 self.reselems_to_buffer(num, name, mail, date, msg)
446 self.res_queue.append((str(self.num)+"\n", False, None, False))
447 self.res_queue.append((line, False, None, True))
448 print "maybe syntax error.", self.num, line
450 def process_res_queue(res_queue, num):
451 self.process_queue(res_queue)
453 #self.textbuffer.create_mark(str(num+1), self.enditer, True)
456 process_res_queue, self.res_queue, self.num)
458 def reselems_to_buffer(self, num, name, mail, date, msg):
459 p = barehtmlparser.BareHTMLParser(
460 lambda d,b,h: self.res_queue.append((d,b,h,False)))
462 p.feed(str(num) + " ")
465 p.feed("<b>" + name + "</b>")
468 p.feed("[" + mail + "]")
475 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
476 p.feed(msg.lstrip(" "))
481 def href_tag(self, href):
482 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
483 tag.set_data("href", href)
486 def process_queue(self, queue):
488 for data, bold, href, margin in queue:
490 layout = self.drawingarea.create_pango_layout(text)
491 layout.set_wrap(pango.WRAP_CHAR)
493 self.pangolayout.append(layout)
497 # taglist.append(self.boldtag)
499 # taglist.append(self.href_tag(href))
501 # taglist.append(self.leftmargintag)
504 # self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
506 # self.textbuffer.insert(self.enditer, data)
508 def jump_to_the_end(self, num):
509 mark = self.textbuffer.get_mark(str(num+1))
511 # self.textview.scroll_to_mark(mark, 0)
515 print "locked, try later."
523 self.lock_obj = False
526 def jump_to_res(self, uri):
527 strict_uri = self.bbs_type.get_thread_uri()
528 if uri != strict_uri and uri.startswith(strict_uri):
529 resnum = uri[len(strict_uri):]
530 match = re.match("\d+", resnum)
532 resnum = match.group()
533 mark = self.textbuffer.get_mark(resnum)
535 # self.textview.scroll_to_mark(mark, 0, True, 0, 0)
536 # elif self.progress:
538 # self.jump_request_num = int(resnum)
540 def load(self, update=False):
541 dat_path = misc.get_thread_dat_path(self.bbs_type)
542 dat_exists = os.path.exists(dat_path)
543 if update or not dat_exists:
550 states_path = misc.get_thread_states_path(self.bbs_type)
551 dat_path = misc.get_thread_dat_path(self.bbs_type)
553 # save only if dat file exists.
554 if os.path.exists(dat_path):
555 window_width, window_height = self.window.get_size()
556 toolbar_visible = self.toolbar.get_property("visible")
557 statusbar_visible = self.statusbar.get_property("visible")
559 dirname = os.path.dirname(states_path)
560 if not os.path.isdir(dirname):
563 f = file(states_path, "w")
565 f.write("window_width=" + str(window_width) + "\n")
566 f.write("window_height=" + str(window_height) + "\n")
567 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
568 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
572 traceback.print_exc()
578 toolbar_visible = True
579 statusbar_visible = True
582 key_base = config.gconf_app_key_base() + "/thread_states"
583 gconf_client = gconf.client_get_default()
584 width = gconf_client.get_int(key_base + "/window_width")
585 height = gconf_client.get_int(key_base + "/window_height")
586 toolbar_visible = gconf_client.get_bool(
587 key_base + "/toolbar")
588 statusbar_visible = gconf_client.get_bool(
589 key_base + "/statusbar")
593 window_height = height
595 traceback.print_exc()
597 states_path = misc.get_thread_states_path(self.bbs_type)
598 if os.path.exists(states_path):
599 for line in file(states_path):
600 if line.startswith("window_height="):
601 height = window_height
604 line[len("window_height="):].rstrip("\n"))
608 window_height = height
609 elif line.startswith("window_width="):
613 line[len("window_width="):].rstrip("\n"))
618 elif line.startswith("toolbar_visible="):
619 tbar = line[len("toolbar_visible="):].rstrip("\n")
620 toolbar_visible = tbar == "True"
621 elif line.startswith("statusbar_visible="):
622 sbar = line[len("statusbar_visible="):].rstrip("\n")
623 statusbar_visible = sbar == "True"
625 self.window.set_default_size(window_width, window_height)
627 if not toolbar_visible:
628 gobject.idle_add(self.toolbar.hide,
629 priority=gobject.PRIORITY_HIGH)
630 if not statusbar_visible:
631 gobject.idle_add(self.statusbar.hide,
632 priority=gobject.PRIORITY_HIGH)
634 traceback.print_exc()