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, HTTPDebugHandler
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)
266 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
268 res = opener.open(req)
269 except urllib2.HTTPError, e:
271 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
275 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
277 line = res.readline()
278 maybe_incomplete = False
280 if not line.endswith("\n"):
281 maybe_incomplete = True
282 print "does not end with \\n. maybe incomplete"
285 line = res.readline()
293 if "Last-Modified" in headers:
294 lastmod = headers["Last-Modified"]
295 if "ETag" in headers:
296 etag = headers["Etag"]
300 title = datfile.get_title_from_dat(
301 self.bbs, self.board, self.thread)
304 gobject.idle_add(self.window.set_title, title)
306 idx_dic = {"title": self.title, "lineCount": self.num,
307 "lastModified": lastmod, "etag": etag}
308 idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
310 gobject.idle_add(session.thread_idx_updated,
311 self.bbs_type.get_thread_uri(), idx_dic)
313 def update(self, widget=None):
315 self.jump_request_num = 0
320 self.textbuffer.create_mark("1", self.enditer, True)
321 gobject.idle_add(create_mark)
323 line_count = datfile.get_dat_line_count(
324 self.bbs, self.board, self.thread)
325 if line_count > self.num:
326 datfile.load_dat_partly(
327 self.bbs, self.board, self.thread,
328 self.append_rawres_to_buffer, self.num+1)
331 if self.jump_request_num:
332 if self.jump_request_num <= num:
333 # jump if enable, otherwize jump later.
334 num = self.jump_request_num
335 self.jump_request_num = 0
336 mark = self.textbuffer.get_mark(str(num))
338 self.textview.scroll_to_mark(
341 self.jump_to_the_end(num)
343 gobject.idle_add(do_jump, self.num)
346 dat_path = misc.get_thread_dat_path(
347 self.bbs, self.board, self.thread)
348 dat_file = FileWrap(dat_path)
350 def save_line_and_append_to_buffer(line):
351 dat_file.seek(self.size)
353 self.append_rawres_to_buffer(line)
355 self.http_get_dat(save_line_and_append_to_buffer)
359 if self.jump_request_num:
360 num = self.jump_request_num
361 self.jump_request_num = 0
362 mark = self.textbuffer.get_mark(str(num))
364 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
366 gobject.idle_add(do_jump)
372 self.progress = False
375 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
382 self.jump_request_num = 0
387 self.textbuffer.create_mark("1", self.enditer, True)
388 gobject.idle_add(create_mark)
390 datfile.load_dat(self.bbs, self.board, self.thread,
391 self.append_rawres_to_buffer)
395 if self.jump_request_num:
396 num = self.jump_request_num
397 self.jump_request_num = 0
398 mark = self.textbuffer.get_mark(str(num))
400 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
402 self.jump_to_the_end(num)
404 gobject.idle_add(do_jump, self.num)
410 self.progress = False
413 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
416 def append_rawres_to_buffer(self, line):
417 self.size += len(line)
420 if not self.title and self.num == 1:
421 title = datfile.do_get_title_from_dat(line)
424 gobject.idle_add(self.window.set_title, title)
426 h = lambda name,mail,date,msg: self.reselems_to_buffer(
427 self.num, name, mail, date, msg)
430 datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
432 def process_res_queue(res_queue, num):
433 self.process_queue(res_queue)
435 self.textbuffer.create_mark(str(num+1), self.enditer, True)
438 process_res_queue, self.res_queue, self.num)
440 def reselems_to_buffer(self, num, name, mail, date, msg):
441 p = barehtmlparser.BareHTMLParser(
442 lambda d,b,h: self.res_queue.append((d,b,h,False)))
444 p.feed(str(num) + " ")
447 p.feed("<b>" + name + "</b>")
450 p.feed("[" + mail + "]")
457 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
458 p.feed(msg.lstrip(" "))
463 def href_tag(self, href):
464 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
465 tag.set_data("href", href)
468 def process_queue(self, queue):
469 for data, bold, href, margin in queue:
472 taglist.append(self.boldtag)
474 taglist.append(self.href_tag(href))
476 taglist.append(self.leftmargintag)
479 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
481 self.textbuffer.insert(self.enditer, data)
483 def jump_to_the_end(self, num):
484 mark = self.textbuffer.get_mark(str(num+1))
486 self.textview.scroll_to_mark(mark, 0)
490 print "locked, try later."
498 self.lock_obj = False
501 def jump_to_res(self, uri):
502 strict_uri = self.bbs_type.get_thread_uri()
503 if uri != strict_uri and uri.startswith(strict_uri):
504 resnum = uri[len(strict_uri):]
505 match = re.match("\d+", resnum)
507 resnum = match.group()
508 mark = self.textbuffer.get_mark(resnum)
510 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
513 self.jump_request_num = int(resnum)
515 def load(self, update=False):
516 dat_path = misc.get_thread_dat_path(
517 self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
518 dat_exists = os.path.exists(dat_path)
519 if update or not dat_exists:
526 states_path = misc.get_thread_states_path(
527 self.bbs_type.bbs_type, self.bbs_type.board,
528 self.bbs_type.thread)
529 dat_path = misc.get_thread_dat_path(
530 self.bbs_type.bbs_type, self.bbs_type.board,
531 self.bbs_type.thread)
533 # save only if dat file exists.
534 if os.path.exists(dat_path):
535 window_width, window_height = self.window.get_size()
536 toolbar_visible = self.toolbar.parent.get_property("visible")
537 statusbar_visible = self.statusbar.get_property("visible")
539 dirname = os.path.dirname(states_path)
540 if not os.path.isdir(dirname):
543 f = file(states_path, "w")
545 f.write("window_width=" + str(window_width) + "\n")
546 f.write("window_height=" + str(window_height) + "\n")
547 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
548 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
552 traceback.print_exc()
558 toolbar_visible = True
559 statusbar_visible = True
562 key_base = config.gconf_app_key_base() + "/thread_states"
563 gconf_client = gconf.client_get_default()
564 width = gconf_client.get_int(key_base + "/window_width")
565 height = gconf_client.get_int(key_base + "/window_height")
566 toolbar_visible = gconf_client.get_bool(
567 key_base + "/toolbar")
568 statusbar_visible = gconf_client.get_bool(
569 key_base + "/statusbar")
573 window_height = height
575 traceback.print_exc()
577 states_path = misc.get_thread_states_path(
578 self.bbs_type.bbs_type, self.bbs_type.board,
579 self.bbs_type.thread)
580 if os.path.exists(states_path):
581 for line in file(states_path):
582 if line.startswith("window_height="):
583 height = window_height
586 line[len("window_height="):].rstrip("\n"))
590 window_height = height
591 elif line.startswith("window_width="):
595 line[len("window_width="):].rstrip("\n"))
600 elif line.startswith("toolbar_visible="):
601 tbar = line[len("toolbar_visible="):].rstrip("\n")
602 toolbar_visible = tbar == "True"
603 elif line.startswith("statusbar_visible="):
604 sbar = line[len("statusbar_visible="):].rstrip("\n")
605 statusbar_visible = sbar == "True"
607 self.window.set_default_size(window_width, window_height)
609 if not toolbar_visible:
610 gobject.idle_add(self.toolbar.parent.hide)
611 if not statusbar_visible:
612 gobject.idle_add(self.statusbar.hide)
614 traceback.print_exc()