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_type)
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(self.bbs_type)
303 gobject.idle_add(self.window.set_title, title)
305 idx_dic = {"title": self.title, "lineCount": self.num,
306 "lastModified": lastmod, "etag": etag}
307 idxfile.save_idx(self.bbs_type, idx_dic)
309 gobject.idle_add(session.thread_idx_updated,
310 self.bbs_type.get_thread_uri(), idx_dic)
312 def update(self, widget=None):
314 self.jump_request_num = 0
319 self.textbuffer.create_mark("1", self.enditer, True)
320 gobject.idle_add(create_mark)
322 line_count = datfile.get_dat_line_count(self.bbs_type)
323 if line_count > self.num:
324 datfile.load_dat_partly(
325 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
328 if self.jump_request_num:
329 if self.jump_request_num <= num:
330 # jump if enable, otherwize jump later.
331 num = self.jump_request_num
332 self.jump_request_num = 0
333 mark = self.textbuffer.get_mark(str(num))
335 self.textview.scroll_to_mark(
338 self.jump_to_the_end(num)
340 gobject.idle_add(do_jump, self.num)
343 dat_path = misc.get_thread_dat_path(self.bbs_type)
344 dat_file = FileWrap(dat_path)
346 def save_line_and_append_to_buffer(line):
347 dat_file.seek(self.size)
349 self.append_rawres_to_buffer(line)
351 self.http_get_dat(save_line_and_append_to_buffer)
355 if self.jump_request_num:
356 num = self.jump_request_num
357 self.jump_request_num = 0
358 mark = self.textbuffer.get_mark(str(num))
360 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
362 gobject.idle_add(do_jump)
368 self.progress = False
371 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
378 self.jump_request_num = 0
383 self.textbuffer.create_mark("1", self.enditer, True)
384 gobject.idle_add(create_mark)
386 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
390 if self.jump_request_num:
391 num = self.jump_request_num
392 self.jump_request_num = 0
393 mark = self.textbuffer.get_mark(str(num))
395 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
397 self.jump_to_the_end(num)
399 gobject.idle_add(do_jump, self.num)
405 self.progress = False
408 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
411 def append_rawres_to_buffer(self, line):
412 self.size += len(line)
415 if not self.title and self.num == 1:
416 title = datfile.do_get_title_from_dat(line)
419 gobject.idle_add(self.window.set_title, title)
421 h = lambda name,mail,date,msg: self.reselems_to_buffer(
422 self.num, name, mail, date, msg)
425 datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
427 def process_res_queue(res_queue, num):
428 self.process_queue(res_queue)
430 self.textbuffer.create_mark(str(num+1), self.enditer, True)
433 process_res_queue, self.res_queue, self.num)
435 def reselems_to_buffer(self, num, name, mail, date, msg):
436 p = barehtmlparser.BareHTMLParser(
437 lambda d,b,h: self.res_queue.append((d,b,h,False)))
439 p.feed(str(num) + " ")
442 p.feed("<b>" + name + "</b>")
445 p.feed("[" + mail + "]")
452 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
453 p.feed(msg.lstrip(" "))
458 def href_tag(self, href):
459 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
460 tag.set_data("href", href)
463 def process_queue(self, queue):
464 for data, bold, href, margin in queue:
467 taglist.append(self.boldtag)
469 taglist.append(self.href_tag(href))
471 taglist.append(self.leftmargintag)
474 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
476 self.textbuffer.insert(self.enditer, data)
478 def jump_to_the_end(self, num):
479 mark = self.textbuffer.get_mark(str(num+1))
481 self.textview.scroll_to_mark(mark, 0)
485 print "locked, try later."
493 self.lock_obj = False
496 def jump_to_res(self, uri):
497 strict_uri = self.bbs_type.get_thread_uri()
498 if uri != strict_uri and uri.startswith(strict_uri):
499 resnum = uri[len(strict_uri):]
500 match = re.match("\d+", resnum)
502 resnum = match.group()
503 mark = self.textbuffer.get_mark(resnum)
505 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
508 self.jump_request_num = int(resnum)
510 def load(self, update=False):
511 dat_path = misc.get_thread_dat_path(self.bbs_type)
512 dat_exists = os.path.exists(dat_path)
513 if update or not dat_exists:
520 states_path = misc.get_thread_states_path(self.bbs_type)
521 dat_path = misc.get_thread_dat_path(self.bbs_type)
523 # save only if dat file exists.
524 if os.path.exists(dat_path):
525 window_width, window_height = self.window.get_size()
526 toolbar_visible = self.toolbar.parent.get_property("visible")
527 statusbar_visible = self.statusbar.get_property("visible")
529 dirname = os.path.dirname(states_path)
530 if not os.path.isdir(dirname):
533 f = file(states_path, "w")
535 f.write("window_width=" + str(window_width) + "\n")
536 f.write("window_height=" + str(window_height) + "\n")
537 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
538 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
542 traceback.print_exc()
548 toolbar_visible = True
549 statusbar_visible = True
552 key_base = config.gconf_app_key_base() + "/thread_states"
553 gconf_client = gconf.client_get_default()
554 width = gconf_client.get_int(key_base + "/window_width")
555 height = gconf_client.get_int(key_base + "/window_height")
556 toolbar_visible = gconf_client.get_bool(
557 key_base + "/toolbar")
558 statusbar_visible = gconf_client.get_bool(
559 key_base + "/statusbar")
563 window_height = height
565 traceback.print_exc()
567 states_path = misc.get_thread_states_path(self.bbs_type)
568 if os.path.exists(states_path):
569 for line in file(states_path):
570 if line.startswith("window_height="):
571 height = window_height
574 line[len("window_height="):].rstrip("\n"))
578 window_height = height
579 elif line.startswith("window_width="):
583 line[len("window_width="):].rstrip("\n"))
588 elif line.startswith("toolbar_visible="):
589 tbar = line[len("toolbar_visible="):].rstrip("\n")
590 toolbar_visible = tbar == "True"
591 elif line.startswith("statusbar_visible="):
592 sbar = line[len("statusbar_visible="):].rstrip("\n")
593 statusbar_visible = sbar == "True"
595 self.window.set_default_size(window_width, window_height)
597 if not toolbar_visible:
598 gobject.idle_add(self.toolbar.parent.hide,
599 priority=gobject.PRIORITY_HIGH)
600 if not statusbar_visible:
601 gobject.idle_add(self.statusbar.hide,
602 priority=gobject.PRIORITY_HIGH)
604 traceback.print_exc()