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
38 from misc import FileWrap, ThreadInvoker
45 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
46 from BbsType import bbs_type_judge_uri
47 from BbsType import bbs_type_exception
51 import bookmark_window
53 GLADE_FILENAME = "thread_window.glade"
55 def open_thread(uri, update=False):
57 raise ValueError, "parameter must not be empty"
59 bbs_type = bbs_type_judge_uri.get_type(uri)
60 if not bbs_type.is_thread():
61 raise bbs_type_exception.BbsTypeError, \
62 "the uri does not represent thread: " + uri
63 uri = bbs_type.get_thread_uri() # use strict thread uri
65 winwrap = session.get_window(uri)
68 winwrap.window.present()
72 winwrap = WinWrap(bbs_type.uri) # pass original uri
75 # jump to the res if necessary.
76 winwrap.jump_to_res(bbs_type.uri)
79 class WinWrap(winwrapbase.WinWrapBase):
80 hovering_over_link = False
81 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
82 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
85 def __init__(self, uri):
86 from BbsType import bbs_type_judge_uri
87 from BbsType import bbs_type_exception
88 self.bbs_type = bbs_type_judge_uri.get_type(uri)
89 if not self.bbs_type.is_thread():
90 raise bbs_type_exception.BbsTypeError, \
91 "the uri does not represent thread: " + uri
96 self.jump_request_num = 0
99 glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
100 self.widget_tree = gtk.glade.XML(glade_path)
101 self.window = self.widget_tree.get_widget("thread_window")
102 self.toolbar = self.widget_tree.get_widget("toolbar")
103 self.toolbar.unset_style()
104 self.statusbar = self.widget_tree.get_widget("appbar")
105 self.textview = self.widget_tree.get_widget("textview")
106 self.textview.drag_dest_unset()
108 self.initialize_buffer()
110 self.hint = HintWrap()
112 sigdic = {"on_refresh_activate": self.update,
113 "on_compose_activate": self.on_compose_clicked,
114 "on_toolbar_activate": self.on_toolbar_activate,
115 "on_statusbar_activate": self.on_statusbar_activate,
116 "on_refresh_activate": self.update,
117 "on_close_activate": self.on_close_activate,
118 "on_quit_activate": self.on_quit_activate,
119 "on_show_board_activate": self.on_show_board_activate,
120 "on_delete_activate": self.on_delete_activate,
121 "on_thread_window_delete_event":
122 self.on_thread_window_delete_event,
123 "on_add_bookmark_activate": self.on_add_bookmark_activate,
124 "on_manage_bookmarks_activate": \
125 self.on_manage_bookmarks_activate,
126 "on_thread_window_destroy": self.on_thread_window_destroy}
127 self.widget_tree.signal_autoconnect(sigdic)
129 self.textview.connect("event-after", self.on_event_after)
130 self.textview.connect("motion-notify-event",
131 self.on_motion_notify_event)
132 self.textview.connect("visibility-notify-event",
133 self.on_visibility_notify_event)
139 def initialize_buffer(self):
140 self.textbuffer = gtk.TextBuffer()
141 self.textview.set_buffer(self.textbuffer)
142 self.enditer = self.textbuffer.get_end_iter()
143 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
144 self.leftmargintag = self.textbuffer.create_tag()
145 self.leftmargintag.set_property("left-margin", 20)
149 self.window.destroy()
152 return self.bbs_type.get_thread_uri()
154 def on_compose_clicked(self, widget):
156 submit_window.open(self.bbs_type.get_thread_uri())
158 def on_toolbar_activate(self, widget):
159 if self.toolbar.parent.get_property("visible"):
160 self.toolbar.parent.hide()
162 self.toolbar.parent.show()
164 def on_statusbar_activate(self, widget):
165 if self.statusbar.get_property("visible"):
166 self.statusbar.hide()
168 self.statusbar.show()
170 def on_event_after(self, widget, event):
171 if event.type != gtk.gdk.BUTTON_RELEASE:
173 if event.button != 1:
175 buffer = widget.get_buffer()
178 start, end = buffer.get_selection_bounds()
182 if start.get_offset() != end.get_offset():
185 x, y = widget.window_to_buffer_coords(
186 gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
187 iter = widget.get_iter_at_location(x, y)
188 if not iter.has_tag(self.leftmargintag) or x > 20:
189 tags = iter.get_tags()
191 href = tag.get_data("href")
193 self.on_link_clicked(widget, href)
196 def on_link_clicked(self, widget, href):
198 if not href.startswith("http://"):
199 # maybe a relative uri.
200 href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
203 uri_opener.open_uri(href)
204 except bbs_type_exception.BbsTypeError:
205 # not supported, show with the web browser.
208 def on_motion_notify_event(self, widget, event):
209 x, y = widget.window_to_buffer_coords(
210 gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
211 self.set_cursor_if_appropriate(widget, x, y)
212 widget.window.get_pointer()
215 def on_visibility_notify_event(self, widget, event):
216 wx, wy, mod = widget.window.get_pointer()
217 bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
219 self.set_cursor_if_appropriate(widget, bx, by)
222 def set_cursor_if_appropriate(self, widget, x, y):
226 buffer = widget.get_buffer()
227 iter = widget.get_iter_at_location(x, y)
228 if not iter.has_tag(self.leftmargintag) or x > 20:
229 tags = iter.get_tags()
231 href = tag.get_data("href")
235 if hovering != self.hovering_over_link:
236 self.hovering_over_link = hovering
238 if self.hovering_over_link:
239 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
242 if not href.startswith("http://"):
243 href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
245 strict_uri = self.bbs_type.get_thread_uri()
246 if href != strict_uri and href.startswith(strict_uri):
247 resnum = href[len(strict_uri):]
248 match = re.match("\d+", resnum)
250 resnum = int(match.group())
251 if not self.hint.visible(resnum):
252 mark = self.textbuffer.get_mark(str(resnum))
253 n_mark = self.textbuffer.get_mark(str(resnum+1))
255 iter = self.textbuffer.get_iter_at_mark(mark)
256 n_iter = self.textbuffer.get_iter_at_mark(
258 text = self.textbuffer.get_text(
261 iter = self.textview.get_iter_at_location(
263 rect = self.textview.get_iter_location(
266 self.textview.buffer_to_window_coords(
267 gtk.TEXT_WINDOW_WIDGET, rect.x, rect.y)
268 x, y = self.textview.translate_coordinates(
270 wx, wy = self.window.get_position()
274 self.hint.show(x, y, text, resnum)
276 widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
280 def on_close_activate(self, widget):
283 def on_thread_window_delete_event(self, widget, event):
287 def on_thread_window_destroy(self, widget):
290 def on_quit_activate(self, widget):
293 def on_add_bookmark_activate(self, widget):
294 bookmark_list.bookmark_list.add_bookmark_with_edit(
295 name=self.title, uri=self.bbs_type.uri)
297 def on_manage_bookmarks_activate(self, widget):
298 bookmark_window.open()
300 def on_show_board_activate(self, widget):
301 board_window.open_board(self.bbs_type.get_uri_base())
303 def on_delete_activate(self, widget):
305 dat_path = misc.get_thread_dat_path(self.bbs_type)
308 traceback.print_exc()
310 idx_path = misc.get_thread_idx_path(self.bbs_type)
313 traceback.print_exc()
315 states_path = misc.get_thread_states_path(self.bbs_type)
316 os.remove(states_path)
318 traceback.print_exc()
320 def http_get_dat(self, on_get_res):
321 datfile_url = self.bbs_type.get_dat_uri()
323 idx_dic = idxfile.load_idx(self.bbs_type)
324 lastmod = idx_dic["lastModified"]
325 etag = idx_dic["etag"]
327 req = urllib2.Request(datfile_url)
328 req.add_header("User-agent", config.User_Agent)
330 req.add_header("Range", "bytes=" + str(self.size) + "-")
332 req.add_header("If-Modified-Since", lastmod)
334 req.add_header("If-None-Match", etag)
336 req = self.bbs_type.set_extra_dat_request(req, self)
338 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
340 res = opener.open(req)
341 except urllib2.HTTPError, e:
343 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
347 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
349 maybe_incomplete = False
351 if not line.endswith("\n"):
352 maybe_incomplete = True
353 print "does not end with \\n. maybe incomplete"
363 if "Last-Modified" in headers:
364 lastmod = headers["Last-Modified"]
365 if "ETag" in headers:
366 etag = headers["Etag"]
370 idx_dic = {"title": self.title, "lineCount": self.num,
371 "lastModified": lastmod, "etag": etag}
372 idxfile.save_idx(self.bbs_type, idx_dic)
374 gobject.idle_add(session.thread_idx_updated,
375 self.bbs_type.get_thread_uri(), idx_dic)
377 def update(self, widget=None):
379 self.jump_request_num = 0
384 self.textbuffer.create_mark("1", self.enditer, True)
385 gobject.idle_add(create_mark)
387 line_count = datfile.get_dat_line_count(self.bbs_type)
388 if line_count < self.num:
392 gobject.idle_add(self.initialize_buffer)
394 if line_count > self.num:
395 datfile.load_dat_partly(
396 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
399 if self.jump_request_num:
400 if self.jump_request_num <= num:
401 # jump if enable, otherwize jump later.
402 num = self.jump_request_num
403 self.jump_request_num = 0
404 mark = self.textbuffer.get_mark(str(num))
406 self.textview.scroll_to_mark(
409 self.jump_to_the_end(num)
411 gobject.idle_add(do_jump, self.num)
414 dat_path = misc.get_thread_dat_path(self.bbs_type)
415 dat_file = FileWrap(dat_path)
417 def save_line_and_append_to_buffer(line):
418 dat_file.seek(self.size)
420 self.append_rawres_to_buffer(line)
422 self.http_get_dat(save_line_and_append_to_buffer)
426 if self.jump_request_num:
427 num = self.jump_request_num
428 self.jump_request_num = 0
429 mark = self.textbuffer.get_mark(str(num))
431 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
433 gobject.idle_add(do_jump)
439 self.progress = False
442 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
449 self.jump_request_num = 0
454 self.textbuffer.create_mark("1", self.enditer, True)
455 gobject.idle_add(create_mark)
457 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
461 if self.jump_request_num:
462 num = self.jump_request_num
463 self.jump_request_num = 0
464 mark = self.textbuffer.get_mark(str(num))
466 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
468 self.jump_to_the_end(num)
470 gobject.idle_add(do_jump, self.num)
476 self.progress = False
479 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
482 def append_rawres_to_buffer(self, line):
483 self.size += len(line)
486 if not self.title and self.num == 1:
487 title = self.bbs_type.get_title_from_dat(line)
490 gobject.idle_add(self.window.set_title, title)
494 line = line.decode(self.bbs_type.encoding, "replace")
495 m = self.bbs_type.dat_reg.match(line)
497 name = m.group("name")
498 mail = m.group("mail")
499 date = m.group("date")
502 num = int(m.group("num"))
504 # use simple counter num
516 self.reselems_to_buffer(num, name, mail, date, msg)
518 self.res_queue.append((str(self.num)+"\n", False, None, False))
519 self.res_queue.append((line, False, None, True))
520 print "maybe syntax error.", self.num, line
522 def process_res_queue(res_queue, num):
523 self.process_queue(res_queue)
525 self.textbuffer.create_mark(str(num+1), self.enditer, True)
528 process_res_queue, self.res_queue, self.num)
530 def reselems_to_buffer(self, num, name, mail, date, msg):
531 p = barehtmlparser.BareHTMLParser(
532 lambda d,b,h: self.res_queue.append((d,b,h,False)))
534 p.feed(str(num) + " ")
537 p.feed("<b>" + name + "</b>")
540 p.feed("[" + mail + "]")
547 p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
548 p.feed(msg.lstrip(" "))
553 def href_tag(self, href):
554 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
555 tag.set_data("href", href)
558 def process_queue(self, queue):
559 for data, bold, href, margin in queue:
562 taglist.append(self.boldtag)
564 taglist.append(self.href_tag(href))
566 taglist.append(self.leftmargintag)
569 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
571 self.textbuffer.insert(self.enditer, data)
573 def jump_to_the_end(self, num):
574 mark = self.textbuffer.get_mark(str(num+1))
576 self.textview.scroll_to_mark(mark, 0)
580 print "locked, try later."
588 self.lock_obj = False
591 def jump_to_res(self, uri):
592 strict_uri = self.bbs_type.get_thread_uri()
593 if uri != strict_uri and uri.startswith(strict_uri):
594 resnum = uri[len(strict_uri):]
595 match = re.match("\d+", resnum)
597 resnum = match.group()
598 mark = self.textbuffer.get_mark(resnum)
600 self.textview.scroll_to_mark(mark, 0, True, 0, 0)
603 self.jump_request_num = int(resnum)
605 def load(self, update=False):
606 dat_path = misc.get_thread_dat_path(self.bbs_type)
607 dat_exists = os.path.exists(dat_path)
608 if update or not dat_exists:
615 states_path = misc.get_thread_states_path(self.bbs_type)
616 dat_path = misc.get_thread_dat_path(self.bbs_type)
618 # save only if dat file exists.
619 if os.path.exists(dat_path):
620 window_width, window_height = self.window.get_size()
621 toolbar_visible = self.toolbar.parent.get_property("visible")
622 statusbar_visible = self.statusbar.get_property("visible")
624 dirname = os.path.dirname(states_path)
625 if not os.path.isdir(dirname):
628 f = file(states_path, "w")
630 f.write("window_width=" + str(window_width) + "\n")
631 f.write("window_height=" + str(window_height) + "\n")
632 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
633 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
637 traceback.print_exc()
643 toolbar_visible = True
644 statusbar_visible = True
647 key_base = config.gconf_app_key_base() + "/thread_states"
648 gconf_client = gconf.client_get_default()
649 width = gconf_client.get_int(key_base + "/window_width")
650 height = gconf_client.get_int(key_base + "/window_height")
651 toolbar_visible = gconf_client.get_bool(
652 key_base + "/toolbar")
653 statusbar_visible = gconf_client.get_bool(
654 key_base + "/statusbar")
658 window_height = height
660 traceback.print_exc()
662 states_path = misc.get_thread_states_path(self.bbs_type)
663 if os.path.exists(states_path):
664 for line in file(states_path):
665 if line.startswith("window_height="):
666 height = window_height
669 line[len("window_height="):].rstrip("\n"))
673 window_height = height
674 elif line.startswith("window_width="):
678 line[len("window_width="):].rstrip("\n"))
683 elif line.startswith("toolbar_visible="):
684 tbar = line[len("toolbar_visible="):].rstrip("\n")
685 toolbar_visible = tbar == "True"
686 elif line.startswith("statusbar_visible="):
687 sbar = line[len("statusbar_visible="):].rstrip("\n")
688 statusbar_visible = sbar == "True"
690 self.window.set_default_size(window_width, window_height)
692 if not toolbar_visible:
693 gobject.idle_add(self.toolbar.parent.hide,
694 priority=gobject.PRIORITY_HIGH)
695 if not statusbar_visible:
696 gobject.idle_add(self.statusbar.hide,
697 priority=gobject.PRIORITY_HIGH)
699 traceback.print_exc()
715 self.window.destroy()
720 def show(self, x, y, text, *nums):
723 self.window = gtk.Window(gtk.WINDOW_POPUP)
724 self.window.set_default_size(400, 10)
725 self.window.move(x, y)
727 self.textview = gtk.TextView()
728 self.window.add(self.textview)
730 self.textview.set_wrap_mode(gtk.WRAP_CHAR)
731 self.textview.set_editable(False)
733 buffer = self.textview.get_buffer()
734 buffer.set_text(text.rstrip())
737 self.window.show_all()
739 def visible(self, *nums):
740 if not self.nums or len(self.nums) != len(nums):
743 for num, mun in itertools.izip(self.nums, nums):