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
41 from http_sub import HTTPRedirectHandler302
42 from BbsType import bbs_type_judge_uri
43 from BbsType import bbs_type_exception
47 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
49 GLADE_FILENAME = "thread_window.glade"
51 def open_thread(uri, update=False):
53 raise ValueError, "parameter must not be empty"
55 bbs_type = bbs_type_judge_uri.get_type(uri)
56 if not bbs_type.is_thread():
57 raise bbs_type_exception.BbsTypeError, \
58 "the uri does not represent thread: " + uri
59 uri = bbs_type.get_thread_uri() # use strict thread uri
61 winwrap = session.get_window(uri)
64 winwrap.window.present()
68 winwrap = WinWrap(bbs_type.uri) # pass original uri
71 # jump to the res if necessary.
72 winwrap.jump_to_res(bbs_type.uri)
75 class ThreadInvoker(threading.Thread):
76 def __init__(self, on_end, *methods):
77 super(ThreadInvoker, self).__init__()
79 self.methods = methods
82 for m in self.methods:
89 def __init__(self, path):
95 self.file().seek(size)
96 def write(self, data):
97 self.file().write(data)
104 basedir = os.path.dirname(self._path)
105 if not os.path.isdir(basedir):
107 self._file = file(self._path, "a+")
111 class WinWrap(winwrapbase.WinWrapBase):
112 hovering_over_link = False
113 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
114 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
117 def __init__(self, uri):
118 from BbsType import bbs_type_judge_uri
119 from BbsType import bbs_type_exception
120 self.bbs_type = bbs_type_judge_uri.get_type(uri)
121 if not self.bbs_type.is_thread():
122 raise bbs_type_exception.BbsTypeError, \
123 "the uri does not represent thread: " + uri
124 self.bbs = self.bbs_type.bbs_type
125 self.board = self.bbs_type.board
126 self.thread = self.bbs_type.thread
127 self.host = self.bbs_type.host
128 self.uri = self.bbs_type.uri
132 self.lock_obj = False
133 self.jump_request_num = 0
134 self.progress = False
136 glade_path = os.path.join(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("appbar")
142 self.textview = self.widget_tree.get_widget("textview")
143 self.textbuffer = self.textview.get_buffer()
144 self.enditer = self.textbuffer.get_end_iter()
145 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
146 self.leftmargintag = self.textbuffer.create_tag()
147 self.leftmargintag.set_property("left-margin", 20)
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_thread_window_delete_event":
158 self.on_thread_window_delete_event,
159 "on_thread_window_destroy": self.on_thread_window_destroy}
160 self.widget_tree.signal_autoconnect(sigdic)
162 self.textview.connect("event-after", self.on_event_after)
163 self.textview.connect("motion-notify-event",
164 self.on_motion_notify_event)
165 self.textview.connect("visibility-notify-event",
166 self.on_visibility_notify_event)
174 self.window.destroy()
177 return self.bbs_type.get_thread_uri()
179 def on_compose_clicked(self, widget):
181 submit_window.open(self.bbs_type.get_thread_uri())
183 def on_toolbar_activate(self, widget):
184 if self.toolbar.parent.get_property("visible"):
185 self.toolbar.parent.hide()
187 self.toolbar.parent.show()
189 def on_statusbar_activate(self, widget):
190 if self.statusbar.get_property("visible"):
191 self.statusbar.hide()
193 self.statusbar.show()
195 def on_event_after(self, widget, event):
196 if event.type != gtk.gdk.BUTTON_RELEASE:
198 if event.button != 1:
200 buffer = widget.get_buffer()
203 start, end = buffer.get_selection_bounds()
207 if start.get_offset() != end.get_offset():
210 x, y = widget.window_to_buffer_coords(
211 gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
212 iter = widget.get_iter_at_location(x, y)
213 if not iter.has_tag(self.leftmargintag) or x > 20:
214 tags = iter.get_tags()
216 href = tag.get_data("href")
218 self.on_link_clicked(widget, href)
221 def on_link_clicked(self, widget, href):
223 if not href.startswith("http://"):
224 # maybe a relative uri.
225 href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
228 uri_opener.open_uri(href)
229 except bbs_type_exception.BbsTypeError:
230 # not supported, show with the web browser.
233 def on_motion_notify_event(self, widget, event):
234 x, y = widget.window_to_buffer_coords(
235 gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
236 self.set_cursor_if_appropriate(widget, x, y)
237 widget.window.get_pointer()
240 def on_visibility_notify_event(self, widget, event):
241 wx, wy, mod = widget.window.get_pointer()
242 bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
244 self.set_cursor_if_appropriate(widget, bx, by)
247 def set_cursor_if_appropriate(self, widget, x, y):
250 buffer = widget.get_buffer()
251 iter = widget.get_iter_at_location(x, y)
252 if not iter.has_tag(self.leftmargintag) or x > 20:
253 tags = iter.get_tags()
255 href = tag.get_data("href")
259 if hovering != self.hovering_over_link:
260 self.hovering_over_link = hovering
262 if self.hovering_over_link:
263 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
266 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
269 def on_close_activate(self, widget):
272 def on_thread_window_delete_event(self, widget, event):
276 def on_thread_window_destroy(self, widget):
279 def on_quit_activate(self, widget):
282 def on_show_board_activate(self, widget):
283 board_window.open_board(self.bbs_type.get_uri_base())
285 def http_get_dat(self, on_get_res):
286 datfile_url = self.bbs_type.get_dat_uri()
288 idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
289 lastmod = idx_dic["lastModified"]
290 etag = idx_dic["etag"]
292 req = urllib2.Request(datfile_url)
294 req.add_header("Range", "bytes=" + str(self.size) + "-")
296 req.add_header("If-Modified-Since", lastmod)
298 req.add_header("If-None-Match", etag)
301 opener = urllib2.build_opener(HTTPRedirectHandler302)
302 res = opener.open(req)
306 line = res.readline()
307 maybe_incomplete = False
309 if not line.endswith("\n"):
310 maybe_incomplete = True
311 print "does not end with \\n. maybe incomplete"
314 line = res.readline()
322 if "Last-Modified" in headers:
323 lastmod = headers["Last-Modified"]
324 if "ETag" in headers:
325 etag = headers["Etag"]
329 title = datfile.get_title_from_dat(
330 self.bbs, self.board, self.thread)
333 gobject.idle_add(self.window.set_title, title)
335 idx_dic = {"title": self.title, "lineCount": self.num,
336 "lastModified": lastmod, "etag": etag}
337 idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
339 gobject.idle_add(session.thread_idx_updated,
340 self.bbs_type.get_thread_uri(), idx_dic)
342 def update(self, widget=None):
344 self.jump_request_num = 0
349 self.textbuffer.create_mark("1", self.enditer, True)
350 gobject.idle_add(create_mark)
352 line_count = datfile.get_dat_line_count(
353 self.bbs, self.board, self.thread)
354 if line_count > self.num:
355 datfile.load_dat_partly(
356 self.bbs, self.board, self.thread,
357 self.append_rawres_to_buffer, self.num+1)
360 if self.jump_request_num:
361 if self.jump_request_num <= num:
362 # jump if enable, otherwize jump later.
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(
370 self.jump_to_the_end(num)
372 gobject.idle_add(do_jump, self.num)
375 dat_path = misc.get_thread_dat_path(
376 self.bbs, self.board, self.thread)
377 dat_file = FileWrap(dat_path)
379 def save_line_and_append_to_buffer(line):
380 dat_file.seek(self.size)
382 self.append_rawres_to_buffer(line)
384 self.http_get_dat(save_line_and_append_to_buffer)
388 if self.jump_request_num:
389 num = self.jump_request_num
390 self.jump_request_num = 0
391 mark = self.textbuffer.get_mark(str(num))
393 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
395 gobject.idle_add(do_jump)
401 self.progress = False
404 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
411 self.jump_request_num = 0
416 self.textbuffer.create_mark("1", self.enditer, True)
417 gobject.idle_add(create_mark)
419 datfile.load_dat(self.bbs, self.board, self.thread,
420 self.append_rawres_to_buffer)
424 if self.jump_request_num:
425 num = self.jump_request_num
426 self.jump_request_num = 0
427 mark = self.textbuffer.get_mark(str(num))
429 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
431 self.jump_to_the_end(num)
433 gobject.idle_add(do_jump, self.num)
439 self.progress = False
442 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
445 def append_rawres_to_buffer(self, line):
446 self.size += len(line)
449 if not self.title and self.num == 1:
450 title = datfile.do_get_title_from_dat(line)
453 gobject.idle_add(self.window.set_title, title)
455 h = lambda name,mail,date,msg: self.reselems_to_buffer(
456 self.num, name, mail, date, msg)
459 datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
461 def process_res_queue(res_queue, num):
462 self.process_queue(res_queue)
464 self.textbuffer.create_mark(str(num+1), self.enditer, True)
467 process_res_queue, self.res_queue, self.num)
469 def reselems_to_buffer(self, num, name, mail, date, msg):
470 p = barehtmlparser.BareHTMLParser(
471 lambda d,b,h: self.res_queue.append((d,b,h,False)))
473 p.feed(str(num) + " ")
476 p.feed("<b>" + name + "</b>")
479 p.feed("[" + mail + "]")
486 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
487 p.feed(msg.lstrip(" "))
492 def href_tag(self, href):
493 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
494 tag.set_data("href", href)
497 def process_queue(self, queue):
498 for data, bold, href, margin in queue:
501 taglist.append(self.boldtag)
503 taglist.append(self.href_tag(href))
505 taglist.append(self.leftmargintag)
508 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
510 self.textbuffer.insert(self.enditer, data)
512 def jump_to_the_end(self, num):
513 mark = self.textbuffer.get_mark(str(num+1))
515 self.textview.scroll_to_mark(mark, 0)
519 print "locked, try later."
527 self.lock_obj = False
530 def jump_to_res(self, uri):
531 strict_uri = self.bbs_type.get_thread_uri()
532 if uri != strict_uri and uri.startswith(strict_uri):
533 resnum = uri[len(strict_uri):]
534 match = re.match("\d+", resnum)
536 resnum = match.group()
537 mark = self.textbuffer.get_mark(resnum)
539 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
542 self.jump_request_num = int(resnum)
544 def load(self, update=False):
545 dat_path = misc.get_thread_dat_path(
546 self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
547 dat_exists = os.path.exists(dat_path)
548 if update or not dat_exists:
555 states_path = misc.get_thread_states_path(
556 self.bbs_type.bbs_type, self.bbs_type.board,
557 self.bbs_type.thread)
558 dat_path = misc.get_thread_dat_path(
559 self.bbs_type.bbs_type, self.bbs_type.board,
560 self.bbs_type.thread)
562 # save only if dat file exists.
563 if os.path.exists(dat_path):
564 window_width, window_height = self.window.get_size()
565 toolbar_visible = self.toolbar.parent.get_property("visible")
566 statusbar_visible = self.statusbar.get_property("visible")
568 dirname = os.path.dirname(states_path)
569 if not os.path.isdir(dirname):
572 f = file(states_path, "w")
574 f.write("window_width=" + str(window_width) + "\n")
575 f.write("window_height=" + str(window_height) + "\n")
576 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
577 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
581 traceback.print_exc()
587 toolbar_visible = True
588 statusbar_visible = True
591 key_base = config.gconf_app_key_base() + "/thread_states"
592 gconf_client = gconf.client_get_default()
593 width = gconf_client.get_int(key_base + "/window_width")
594 height = gconf_client.get_int(key_base + "/window_height")
595 toolbar_visible = gconf_client.get_bool(
596 key_base + "/toolbar")
597 statusbar_visible = gconf_client.get_bool(
598 key_base + "/statusbar")
602 window_height = height
604 traceback.print_exc()
606 states_path = misc.get_thread_states_path(
607 self.bbs_type.bbs_type, self.bbs_type.board,
608 self.bbs_type.thread)
609 if os.path.exists(states_path):
610 for line in file(states_path):
611 if line.startswith("window_height="):
612 height = window_height
615 line[len("window_height="):].rstrip("\n"))
619 window_height = height
620 elif line.startswith("window_width="):
624 line[len("window_width="):].rstrip("\n"))
629 elif line.startswith("toolbar_visible="):
630 tbar = line[len("toolbar_visible="):].rstrip("\n")
631 toolbar_visible = tbar == "True"
632 elif line.startswith("statusbar_visible="):
633 sbar = line[len("statusbar_visible="):].rstrip("\n")
634 statusbar_visible = sbar == "True"
636 self.window.set_default_size(window_width, window_height)
638 if not toolbar_visible:
639 gobject.idle_add(self.toolbar.parent.hide)
640 if not statusbar_visible:
641 gobject.idle_add(self.statusbar.hide)
643 traceback.print_exc()