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
35 from misc import FileWrap, ThreadInvoker
42 from http_sub import HTTPRedirectHandler302
43 from BbsType import bbs_type_judge_uri
44 from BbsType import bbs_type_exception
48 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
50 GLADE_FILENAME = "thread_window.glade"
52 def open_thread(uri, update=False):
54 raise ValueError, "parameter must not be empty"
56 bbs_type = bbs_type_judge_uri.get_type(uri)
57 if not bbs_type.is_thread():
58 raise bbs_type_exception.BbsTypeError, \
59 "the uri does not represent thread: " + uri
60 uri = bbs_type.get_thread_uri() # use strict thread uri
62 winwrap = session.get_window(uri)
65 winwrap.window.present()
69 winwrap = WinWrap(bbs_type.uri) # pass original uri
72 # jump to the res if necessary.
73 winwrap.jump_to_res(bbs_type.uri)
76 class WinWrap(winwrapbase.WinWrapBase):
77 hovering_over_link = False
78 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
79 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
82 def __init__(self, uri):
83 from BbsType import bbs_type_judge_uri
84 from BbsType import bbs_type_exception
85 self.bbs_type = bbs_type_judge_uri.get_type(uri)
86 if not self.bbs_type.is_thread():
87 raise bbs_type_exception.BbsTypeError, \
88 "the uri does not represent thread: " + uri
89 self.bbs = self.bbs_type.bbs_type
90 self.board = self.bbs_type.board
91 self.thread = self.bbs_type.thread
92 self.host = self.bbs_type.host
93 self.uri = self.bbs_type.uri
98 self.jump_request_num = 0
101 glade_path = os.path.join(GLADE_DIR, GLADE_FILENAME)
102 self.widget_tree = gtk.glade.XML(glade_path)
103 self.window = self.widget_tree.get_widget("thread_window")
104 self.toolbar = self.widget_tree.get_widget("toolbar")
105 self.toolbar.unset_style()
106 self.statusbar = self.widget_tree.get_widget("appbar")
107 self.textview = self.widget_tree.get_widget("textview")
108 self.textview.drag_dest_unset()
109 self.textbuffer = self.textview.get_buffer()
110 self.enditer = self.textbuffer.get_end_iter()
111 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
112 self.leftmargintag = self.textbuffer.create_tag()
113 self.leftmargintag.set_property("left-margin", 20)
115 sigdic = {"on_refresh_activate": self.update,
116 "on_compose_activate": self.on_compose_clicked,
117 "on_toolbar_activate": self.on_toolbar_activate,
118 "on_statusbar_activate": self.on_statusbar_activate,
119 "on_refresh_activate": self.update,
120 "on_close_activate": self.on_close_activate,
121 "on_quit_activate": self.on_quit_activate,
122 "on_show_board_activate": self.on_show_board_activate,
123 "on_thread_window_delete_event":
124 self.on_thread_window_delete_event,
125 "on_thread_window_destroy": self.on_thread_window_destroy}
126 self.widget_tree.signal_autoconnect(sigdic)
128 self.textview.connect("event-after", self.on_event_after)
129 self.textview.connect("motion-notify-event",
130 self.on_motion_notify_event)
131 self.textview.connect("visibility-notify-event",
132 self.on_visibility_notify_event)
140 self.window.destroy()
143 return self.bbs_type.get_thread_uri()
145 def on_compose_clicked(self, widget):
147 submit_window.open(self.bbs_type.get_thread_uri())
149 def on_toolbar_activate(self, widget):
150 if self.toolbar.parent.get_property("visible"):
151 self.toolbar.parent.hide()
153 self.toolbar.parent.show()
155 def on_statusbar_activate(self, widget):
156 if self.statusbar.get_property("visible"):
157 self.statusbar.hide()
159 self.statusbar.show()
161 def on_event_after(self, widget, event):
162 if event.type != gtk.gdk.BUTTON_RELEASE:
164 if event.button != 1:
166 buffer = widget.get_buffer()
169 start, end = buffer.get_selection_bounds()
173 if start.get_offset() != end.get_offset():
176 x, y = widget.window_to_buffer_coords(
177 gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
178 iter = widget.get_iter_at_location(x, y)
179 if not iter.has_tag(self.leftmargintag) or x > 20:
180 tags = iter.get_tags()
182 href = tag.get_data("href")
184 self.on_link_clicked(widget, href)
187 def on_link_clicked(self, widget, href):
189 if not href.startswith("http://"):
190 # maybe a relative uri.
191 href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
194 uri_opener.open_uri(href)
195 except bbs_type_exception.BbsTypeError:
196 # not supported, show with the web browser.
199 def on_motion_notify_event(self, widget, event):
200 x, y = widget.window_to_buffer_coords(
201 gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
202 self.set_cursor_if_appropriate(widget, x, y)
203 widget.window.get_pointer()
206 def on_visibility_notify_event(self, widget, event):
207 wx, wy, mod = widget.window.get_pointer()
208 bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
210 self.set_cursor_if_appropriate(widget, bx, by)
213 def set_cursor_if_appropriate(self, widget, x, y):
216 buffer = widget.get_buffer()
217 iter = widget.get_iter_at_location(x, y)
218 if not iter.has_tag(self.leftmargintag) or x > 20:
219 tags = iter.get_tags()
221 href = tag.get_data("href")
225 if hovering != self.hovering_over_link:
226 self.hovering_over_link = hovering
228 if self.hovering_over_link:
229 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
232 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
235 def on_close_activate(self, widget):
238 def on_thread_window_delete_event(self, widget, event):
242 def on_thread_window_destroy(self, widget):
245 def on_quit_activate(self, widget):
248 def on_show_board_activate(self, widget):
249 board_window.open_board(self.bbs_type.get_uri_base())
251 def http_get_dat(self, on_get_res):
252 datfile_url = self.bbs_type.get_dat_uri()
254 idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
255 lastmod = idx_dic["lastModified"]
256 etag = idx_dic["etag"]
258 req = urllib2.Request(datfile_url)
260 req.add_header("Range", "bytes=" + str(self.size) + "-")
262 req.add_header("If-Modified-Since", lastmod)
264 req.add_header("If-None-Match", etag)
267 opener = urllib2.build_opener(HTTPRedirectHandler302)
269 res = opener.open(req)
270 except urllib2.HTTPError, e:
273 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
278 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
280 line = res.readline()
281 maybe_incomplete = False
283 if not line.endswith("\n"):
284 maybe_incomplete = True
285 print "does not end with \\n. maybe incomplete"
288 line = res.readline()
296 if "Last-Modified" in headers:
297 lastmod = headers["Last-Modified"]
298 if "ETag" in headers:
299 etag = headers["Etag"]
303 title = datfile.get_title_from_dat(
304 self.bbs, self.board, self.thread)
307 gobject.idle_add(self.window.set_title, title)
309 idx_dic = {"title": self.title, "lineCount": self.num,
310 "lastModified": lastmod, "etag": etag}
311 idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
313 gobject.idle_add(session.thread_idx_updated,
314 self.bbs_type.get_thread_uri(), idx_dic)
316 def update(self, widget=None):
318 self.jump_request_num = 0
323 self.textbuffer.create_mark("1", self.enditer, True)
324 gobject.idle_add(create_mark)
326 line_count = datfile.get_dat_line_count(
327 self.bbs, self.board, self.thread)
328 if line_count > self.num:
329 datfile.load_dat_partly(
330 self.bbs, self.board, self.thread,
331 self.append_rawres_to_buffer, self.num+1)
334 if self.jump_request_num:
335 if self.jump_request_num <= num:
336 # jump if enable, otherwize jump later.
337 num = self.jump_request_num
338 self.jump_request_num = 0
339 mark = self.textbuffer.get_mark(str(num))
341 self.textview.scroll_to_mark(
344 self.jump_to_the_end(num)
346 gobject.idle_add(do_jump, self.num)
349 dat_path = misc.get_thread_dat_path(
350 self.bbs, self.board, self.thread)
351 dat_file = FileWrap(dat_path)
353 def save_line_and_append_to_buffer(line):
354 dat_file.seek(self.size)
356 self.append_rawres_to_buffer(line)
358 self.http_get_dat(save_line_and_append_to_buffer)
362 if self.jump_request_num:
363 num = self.jump_request_num
364 self.jump_request_num = 0
365 mark = self.textbuffer.get_mark(str(num))
367 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
369 gobject.idle_add(do_jump)
375 self.progress = False
378 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
385 self.jump_request_num = 0
390 self.textbuffer.create_mark("1", self.enditer, True)
391 gobject.idle_add(create_mark)
393 datfile.load_dat(self.bbs, self.board, self.thread,
394 self.append_rawres_to_buffer)
398 if self.jump_request_num:
399 num = self.jump_request_num
400 self.jump_request_num = 0
401 mark = self.textbuffer.get_mark(str(num))
403 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
405 self.jump_to_the_end(num)
407 gobject.idle_add(do_jump, self.num)
413 self.progress = False
416 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
419 def append_rawres_to_buffer(self, line):
420 self.size += len(line)
423 if not self.title and self.num == 1:
424 title = datfile.do_get_title_from_dat(line)
427 gobject.idle_add(self.window.set_title, title)
429 h = lambda name,mail,date,msg: self.reselems_to_buffer(
430 self.num, name, mail, date, msg)
433 datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
435 def process_res_queue(res_queue, num):
436 self.process_queue(res_queue)
438 self.textbuffer.create_mark(str(num+1), self.enditer, True)
441 process_res_queue, self.res_queue, self.num)
443 def reselems_to_buffer(self, num, name, mail, date, msg):
444 p = barehtmlparser.BareHTMLParser(
445 lambda d,b,h: self.res_queue.append((d,b,h,False)))
447 p.feed(str(num) + " ")
450 p.feed("<b>" + name + "</b>")
453 p.feed("[" + mail + "]")
460 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
461 p.feed(msg.lstrip(" "))
466 def href_tag(self, href):
467 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
468 tag.set_data("href", href)
471 def process_queue(self, queue):
472 for data, bold, href, margin in queue:
475 taglist.append(self.boldtag)
477 taglist.append(self.href_tag(href))
479 taglist.append(self.leftmargintag)
482 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
484 self.textbuffer.insert(self.enditer, data)
486 def jump_to_the_end(self, num):
487 mark = self.textbuffer.get_mark(str(num+1))
489 self.textview.scroll_to_mark(mark, 0)
493 print "locked, try later."
501 self.lock_obj = False
504 def jump_to_res(self, uri):
505 strict_uri = self.bbs_type.get_thread_uri()
506 if uri != strict_uri and uri.startswith(strict_uri):
507 resnum = uri[len(strict_uri):]
508 match = re.match("\d+", resnum)
510 resnum = match.group()
511 mark = self.textbuffer.get_mark(resnum)
513 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
516 self.jump_request_num = int(resnum)
518 def load(self, update=False):
519 dat_path = misc.get_thread_dat_path(
520 self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
521 dat_exists = os.path.exists(dat_path)
522 if update or not dat_exists:
529 states_path = misc.get_thread_states_path(
530 self.bbs_type.bbs_type, self.bbs_type.board,
531 self.bbs_type.thread)
532 dat_path = misc.get_thread_dat_path(
533 self.bbs_type.bbs_type, self.bbs_type.board,
534 self.bbs_type.thread)
536 # save only if dat file exists.
537 if os.path.exists(dat_path):
538 window_width, window_height = self.window.get_size()
539 toolbar_visible = self.toolbar.parent.get_property("visible")
540 statusbar_visible = self.statusbar.get_property("visible")
542 dirname = os.path.dirname(states_path)
543 if not os.path.isdir(dirname):
546 f = file(states_path, "w")
548 f.write("window_width=" + str(window_width) + "\n")
549 f.write("window_height=" + str(window_height) + "\n")
550 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
551 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
555 traceback.print_exc()
561 toolbar_visible = True
562 statusbar_visible = True
565 key_base = config.gconf_app_key_base() + "/thread_states"
566 gconf_client = gconf.client_get_default()
567 width = gconf_client.get_int(key_base + "/window_width")
568 height = gconf_client.get_int(key_base + "/window_height")
569 toolbar_visible = gconf_client.get_bool(
570 key_base + "/toolbar")
571 statusbar_visible = gconf_client.get_bool(
572 key_base + "/statusbar")
576 window_height = height
578 traceback.print_exc()
580 states_path = misc.get_thread_states_path(
581 self.bbs_type.bbs_type, self.bbs_type.board,
582 self.bbs_type.thread)
583 if os.path.exists(states_path):
584 for line in file(states_path):
585 if line.startswith("window_height="):
586 height = window_height
589 line[len("window_height="):].rstrip("\n"))
593 window_height = height
594 elif line.startswith("window_width="):
598 line[len("window_width="):].rstrip("\n"))
603 elif line.startswith("toolbar_visible="):
604 tbar = line[len("toolbar_visible="):].rstrip("\n")
605 toolbar_visible = tbar == "True"
606 elif line.startswith("statusbar_visible="):
607 sbar = line[len("statusbar_visible="):].rstrip("\n")
608 statusbar_visible = sbar == "True"
610 self.window.set_default_size(window_width, window_height)
612 if not toolbar_visible:
613 gobject.idle_add(self.toolbar.parent.hide)
614 if not statusbar_visible:
615 gobject.idle_add(self.statusbar.hide)
617 traceback.print_exc()