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
36 from misc import FileWrap, ThreadInvoker
43 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
44 from BbsType import bbs_type_judge_uri
45 from BbsType import bbs_type_exception
49 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
51 GLADE_FILENAME = "thread_window.glade"
53 def open_thread(uri, update=False):
55 raise ValueError, "parameter must not be empty"
57 bbs_type = bbs_type_judge_uri.get_type(uri)
58 if not bbs_type.is_thread():
59 raise bbs_type_exception.BbsTypeError, \
60 "the uri does not represent thread: " + uri
61 uri = bbs_type.get_thread_uri() # use strict thread uri
63 winwrap = session.get_window(uri)
66 winwrap.window.present()
70 winwrap = WinWrap(bbs_type.uri) # pass original uri
73 # jump to the res if necessary.
74 winwrap.jump_to_res(bbs_type.uri)
77 class WinWrap(winwrapbase.WinWrapBase):
78 hovering_over_link = False
79 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
80 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
83 def __init__(self, uri):
84 from BbsType import bbs_type_judge_uri
85 from BbsType import bbs_type_exception
86 self.bbs_type = bbs_type_judge_uri.get_type(uri)
87 if not self.bbs_type.is_thread():
88 raise bbs_type_exception.BbsTypeError, \
89 "the uri does not represent thread: " + uri
94 self.jump_request_num = 0
97 glade_path = os.path.join(GLADE_DIR, GLADE_FILENAME)
98 self.widget_tree = gtk.glade.XML(glade_path)
99 self.window = self.widget_tree.get_widget("thread_window")
100 self.toolbar = self.widget_tree.get_widget("toolbar")
101 self.toolbar.unset_style()
102 self.statusbar = self.widget_tree.get_widget("appbar")
103 self.textview = self.widget_tree.get_widget("textview")
104 self.textview.drag_dest_unset()
106 self.initialize_buffer()
108 sigdic = {"on_refresh_activate": self.update,
109 "on_compose_activate": self.on_compose_clicked,
110 "on_toolbar_activate": self.on_toolbar_activate,
111 "on_statusbar_activate": self.on_statusbar_activate,
112 "on_refresh_activate": self.update,
113 "on_close_activate": self.on_close_activate,
114 "on_quit_activate": self.on_quit_activate,
115 "on_show_board_activate": self.on_show_board_activate,
116 "on_delete_activate": self.on_delete_activate,
117 "on_thread_window_delete_event":
118 self.on_thread_window_delete_event,
119 "on_thread_window_destroy": self.on_thread_window_destroy}
120 self.widget_tree.signal_autoconnect(sigdic)
122 self.textview.connect("event-after", self.on_event_after)
123 self.textview.connect("motion-notify-event",
124 self.on_motion_notify_event)
125 self.textview.connect("visibility-notify-event",
126 self.on_visibility_notify_event)
132 def initialize_buffer(self):
133 self.textbuffer = gtk.TextBuffer()
134 self.textview.set_buffer(self.textbuffer)
135 self.enditer = self.textbuffer.get_end_iter()
136 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
137 self.leftmargintag = self.textbuffer.create_tag()
138 self.leftmargintag.set_property("left-margin", 20)
142 self.window.destroy()
145 return self.bbs_type.get_thread_uri()
147 def on_compose_clicked(self, widget):
149 submit_window.open(self.bbs_type.get_thread_uri())
151 def on_toolbar_activate(self, widget):
152 if self.toolbar.parent.get_property("visible"):
153 self.toolbar.parent.hide()
155 self.toolbar.parent.show()
157 def on_statusbar_activate(self, widget):
158 if self.statusbar.get_property("visible"):
159 self.statusbar.hide()
161 self.statusbar.show()
163 def on_event_after(self, widget, event):
164 if event.type != gtk.gdk.BUTTON_RELEASE:
166 if event.button != 1:
168 buffer = widget.get_buffer()
171 start, end = buffer.get_selection_bounds()
175 if start.get_offset() != end.get_offset():
178 x, y = widget.window_to_buffer_coords(
179 gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
180 iter = widget.get_iter_at_location(x, y)
181 if not iter.has_tag(self.leftmargintag) or x > 20:
182 tags = iter.get_tags()
184 href = tag.get_data("href")
186 self.on_link_clicked(widget, href)
189 def on_link_clicked(self, widget, href):
191 if not href.startswith("http://"):
192 # maybe a relative uri.
193 href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
196 uri_opener.open_uri(href)
197 except bbs_type_exception.BbsTypeError:
198 # not supported, show with the web browser.
201 def on_motion_notify_event(self, widget, event):
202 x, y = widget.window_to_buffer_coords(
203 gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
204 self.set_cursor_if_appropriate(widget, x, y)
205 widget.window.get_pointer()
208 def on_visibility_notify_event(self, widget, event):
209 wx, wy, mod = widget.window.get_pointer()
210 bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
212 self.set_cursor_if_appropriate(widget, bx, by)
215 def set_cursor_if_appropriate(self, widget, x, y):
218 buffer = widget.get_buffer()
219 iter = widget.get_iter_at_location(x, y)
220 if not iter.has_tag(self.leftmargintag) or x > 20:
221 tags = iter.get_tags()
223 href = tag.get_data("href")
227 if hovering != self.hovering_over_link:
228 self.hovering_over_link = hovering
230 if self.hovering_over_link:
231 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
234 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
237 def on_close_activate(self, widget):
240 def on_thread_window_delete_event(self, widget, event):
244 def on_thread_window_destroy(self, widget):
247 def on_quit_activate(self, widget):
250 def on_show_board_activate(self, widget):
251 board_window.open_board(self.bbs_type.get_uri_base())
253 def on_delete_activate(self, widget):
255 dat_path = misc.get_thread_dat_path(self.bbs_type)
258 traceback.print_exc()
260 idx_path = misc.get_thread_idx_path(self.bbs_type)
263 traceback.print_exc()
265 states_path = misc.get_thread_states_path(self.bbs_type)
266 os.remove(states_path)
268 traceback.print_exc()
270 def http_get_dat(self, on_get_res):
271 datfile_url = self.bbs_type.get_dat_uri()
273 idx_dic = idxfile.load_idx(self.bbs_type)
274 lastmod = idx_dic["lastModified"]
275 etag = idx_dic["etag"]
277 req = urllib2.Request(datfile_url)
279 req.add_header("Range", "bytes=" + str(self.size) + "-")
281 req.add_header("If-Modified-Since", lastmod)
283 req.add_header("If-None-Match", etag)
285 req = self.bbs_type.set_extra_dat_request(req, self)
287 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
289 res = opener.open(req)
290 except urllib2.HTTPError, e:
292 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
296 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
298 maybe_incomplete = False
300 if not line.endswith("\n"):
301 maybe_incomplete = True
302 print "does not end with \\n. maybe incomplete"
312 if "Last-Modified" in headers:
313 lastmod = headers["Last-Modified"]
314 if "ETag" in headers:
315 etag = headers["Etag"]
319 idx_dic = {"title": self.title, "lineCount": self.num,
320 "lastModified": lastmod, "etag": etag}
321 idxfile.save_idx(self.bbs_type, idx_dic)
323 gobject.idle_add(session.thread_idx_updated,
324 self.bbs_type.get_thread_uri(), idx_dic)
326 def update(self, widget=None):
328 self.jump_request_num = 0
333 self.textbuffer.create_mark("1", self.enditer, True)
334 gobject.idle_add(create_mark)
336 line_count = datfile.get_dat_line_count(self.bbs_type)
337 if line_count < self.num:
341 gobject.idle_add(self.initialize_buffer)
343 if line_count > self.num:
344 datfile.load_dat_partly(
345 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
348 if self.jump_request_num:
349 if self.jump_request_num <= num:
350 # jump if enable, otherwize jump later.
351 num = self.jump_request_num
352 self.jump_request_num = 0
353 mark = self.textbuffer.get_mark(str(num))
355 self.textview.scroll_to_mark(
358 self.jump_to_the_end(num)
360 gobject.idle_add(do_jump, self.num)
363 dat_path = misc.get_thread_dat_path(self.bbs_type)
364 dat_file = FileWrap(dat_path)
366 def save_line_and_append_to_buffer(line):
367 dat_file.seek(self.size)
369 self.append_rawres_to_buffer(line)
371 self.http_get_dat(save_line_and_append_to_buffer)
375 if self.jump_request_num:
376 num = self.jump_request_num
377 self.jump_request_num = 0
378 mark = self.textbuffer.get_mark(str(num))
380 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
382 gobject.idle_add(do_jump)
388 self.progress = False
391 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
398 self.jump_request_num = 0
403 self.textbuffer.create_mark("1", self.enditer, True)
404 gobject.idle_add(create_mark)
406 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
410 if self.jump_request_num:
411 num = self.jump_request_num
412 self.jump_request_num = 0
413 mark = self.textbuffer.get_mark(str(num))
415 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
417 self.jump_to_the_end(num)
419 gobject.idle_add(do_jump, self.num)
425 self.progress = False
428 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
431 def append_rawres_to_buffer(self, line):
432 self.size += len(line)
435 if not self.title and self.num == 1:
436 title = self.bbs_type.get_title_from_dat(line)
439 gobject.idle_add(self.window.set_title, title)
443 line = line.decode(self.bbs_type.encoding, "replace")
444 m = self.bbs_type.dat_reg.match(line)
446 name = m.group("name")
447 mail = m.group("mail")
448 date = m.group("date")
451 num = int(m.group("num"))
453 # use simple counter num
465 self.reselems_to_buffer(num, name, mail, date, msg)
467 self.res_queue.append((str(self.num)+"\n", False, None, False))
468 self.res_queue.append((line, False, None, True))
469 print "maybe syntax error.", self.num, line
471 def process_res_queue(res_queue, num):
472 self.process_queue(res_queue)
474 self.textbuffer.create_mark(str(num+1), self.enditer, True)
477 process_res_queue, self.res_queue, self.num)
479 def reselems_to_buffer(self, num, name, mail, date, msg):
480 p = barehtmlparser.BareHTMLParser(
481 lambda d,b,h: self.res_queue.append((d,b,h,False)))
483 p.feed(str(num) + " ")
486 p.feed("<b>" + name + "</b>")
489 p.feed("[" + mail + "]")
496 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
497 p.feed(msg.lstrip(" "))
502 def href_tag(self, href):
503 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
504 tag.set_data("href", href)
507 def process_queue(self, queue):
508 for data, bold, href, margin in queue:
511 taglist.append(self.boldtag)
513 taglist.append(self.href_tag(href))
515 taglist.append(self.leftmargintag)
518 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
520 self.textbuffer.insert(self.enditer, data)
522 def jump_to_the_end(self, num):
523 mark = self.textbuffer.get_mark(str(num+1))
525 self.textview.scroll_to_mark(mark, 0)
529 print "locked, try later."
537 self.lock_obj = False
540 def jump_to_res(self, uri):
541 strict_uri = self.bbs_type.get_thread_uri()
542 if uri != strict_uri and uri.startswith(strict_uri):
543 resnum = uri[len(strict_uri):]
544 match = re.match("\d+", resnum)
546 resnum = match.group()
547 mark = self.textbuffer.get_mark(resnum)
549 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
552 self.jump_request_num = int(resnum)
554 def load(self, update=False):
555 dat_path = misc.get_thread_dat_path(self.bbs_type)
556 dat_exists = os.path.exists(dat_path)
557 if update or not dat_exists:
564 states_path = misc.get_thread_states_path(self.bbs_type)
565 dat_path = misc.get_thread_dat_path(self.bbs_type)
567 # save only if dat file exists.
568 if os.path.exists(dat_path):
569 window_width, window_height = self.window.get_size()
570 toolbar_visible = self.toolbar.parent.get_property("visible")
571 statusbar_visible = self.statusbar.get_property("visible")
573 dirname = os.path.dirname(states_path)
574 if not os.path.isdir(dirname):
577 f = file(states_path, "w")
579 f.write("window_width=" + str(window_width) + "\n")
580 f.write("window_height=" + str(window_height) + "\n")
581 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
582 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
586 traceback.print_exc()
592 toolbar_visible = True
593 statusbar_visible = True
596 key_base = config.gconf_app_key_base() + "/thread_states"
597 gconf_client = gconf.client_get_default()
598 width = gconf_client.get_int(key_base + "/window_width")
599 height = gconf_client.get_int(key_base + "/window_height")
600 toolbar_visible = gconf_client.get_bool(
601 key_base + "/toolbar")
602 statusbar_visible = gconf_client.get_bool(
603 key_base + "/statusbar")
607 window_height = height
609 traceback.print_exc()
611 states_path = misc.get_thread_states_path(self.bbs_type)
612 if os.path.exists(states_path):
613 for line in file(states_path):
614 if line.startswith("window_height="):
615 height = window_height
618 line[len("window_height="):].rstrip("\n"))
622 window_height = height
623 elif line.startswith("window_width="):
627 line[len("window_width="):].rstrip("\n"))
632 elif line.startswith("toolbar_visible="):
633 tbar = line[len("toolbar_visible="):].rstrip("\n")
634 toolbar_visible = tbar == "True"
635 elif line.startswith("statusbar_visible="):
636 sbar = line[len("statusbar_visible="):].rstrip("\n")
637 statusbar_visible = sbar == "True"
639 self.window.set_default_size(window_width, window_height)
641 if not toolbar_visible:
642 gobject.idle_add(self.toolbar.parent.hide,
643 priority=gobject.PRIORITY_HIGH)
644 if not statusbar_visible:
645 gobject.idle_add(self.statusbar.hide,
646 priority=gobject.PRIORITY_HIGH)
648 traceback.print_exc()