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
56 GLADE_FILENAME = "thread_window.glade"
58 def open_thread(uri, update=False):
60 raise ValueError, "parameter must not be empty"
62 bbs_type = bbs_type_judge_uri.get_type(uri)
63 if not bbs_type.is_thread():
64 raise bbs_type_exception.BbsTypeError, \
65 "the uri does not represent thread: " + uri
66 uri = bbs_type.get_thread_uri() # use strict thread uri
68 winwrap = session.get_window(uri)
71 winwrap.window.present()
75 winwrap = WinWrap(bbs_type.uri) # pass original uri
78 # jump to the res if necessary.
79 strict_uri = bbs_type.get_thread_uri()
80 if (bbs_type.uri != strict_uri and
81 bbs_type.uri.startswith(strict_uri)):
82 resnum = bbs_type.uri[len(strict_uri):]
83 match = re.match("\d+", resnum)
85 resnum = int(match.group())
86 winwrap.jump_to_res(resnum)
89 class HTMLParserToThreadView:
90 def __init__(self, threadview, resnum, left_margin):
91 self.threadview = threadview
93 self.left_margin = left_margin
96 def set_left_margin(self, left_margin):
97 self.left_margin = left_margin
102 def on_new_line(self):
103 self.to_thread_view()
104 self.layout = self.threadview.create_res_layout(
105 self.left_margin, self.resnum)
107 def from_html_parser(self, data, bold, href):
108 if self.layout == None:
109 self.layout = self.threadview.create_res_layout(
110 self.left_margin, self.resnum)
112 gtk.gdk.threads_enter()
113 self.layout.add_text(data, bold, href)
114 gtk.gdk.threads_leave()
116 def to_thread_view(self):
117 if self.layout is not None:
118 # gobject.idle_add(self.threadview.add_layout, self.layout)
119 gtk.gdk.threads_enter()
120 self.threadview.add_layout(self.layout)
121 gtk.gdk.threads_leave()
125 class WinWrap(winwrapbase.WinWrapBase):
126 hovering_over_link = False
127 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
128 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
130 def __init__(self, uri):
131 from BbsType import bbs_type_judge_uri
132 from BbsType import bbs_type_exception
133 self.bbs_type = bbs_type_judge_uri.get_type(uri)
134 if not self.bbs_type.is_thread():
135 raise bbs_type_exception.BbsTypeError, \
136 "the uri does not represent thread: " + uri
140 self.lock_obj = False
141 self.jump_request_num = 0
142 self.progress = False
144 glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
145 self.widget_tree = gtk.glade.XML(glade_path)
147 self.widget_tree.signal_autoconnect(self)
149 self.toolbar.unset_style()
151 self.threadview = thread_view.ThreadView()
152 self.threadpopup = thread_popup.ThreadPopup(self.bbs_type)
153 self.threadpopup.push_thread_view(self.threadview)
154 self.vbox.pack_start(self.threadview)
155 self.vbox.reorder_child(self.threadview, 2)
156 self.window.set_focus(self.threadview.drawingarea)
158 self._get_popupmenu_widgets()
160 self.threadview.connect(
161 "uri-clicked-event", self.on_thread_view_uri_clicked)
162 self.threadpopup.connect(
163 "uri-clicked-event", self.on_thread_popup_uri_clicked)
165 self.statusbar_context_id = self.statusbar.get_context_id(
166 "Thread Window Status")
167 self.statusbar.push(self.statusbar_context_id, "OK.")
169 self.initialize_buffer()
172 self.window.show_all()
176 def _get_widgets(self):
177 self.window = self.widget_tree.get_widget("thread_window")
178 self.toolbar = self.widget_tree.get_widget("toolbar")
179 self.statusbar = self.widget_tree.get_widget("statusbar")
180 self.vbox = self.widget_tree.get_widget("vbox")
182 def _get_popupmenu_widgets(self):
183 self.threadview.popupmenu = self.widget_tree.get_widget(
184 "popup_threadview_menu")
185 self.threadview.menu_openuri = self.widget_tree.get_widget(
186 "popup_threadview_menu_openuri")
187 self.threadview.menu_copylinkaddress = self.widget_tree.get_widget(
188 "popup_threadview_menu_copylinkaddress")
189 self.threadview.menu_separator_link = self.widget_tree.get_widget(
190 "popup_threadview_menu_separator_link")
191 self.threadview.menu_copyselection = self.widget_tree.get_widget(
192 "popup_threadview_menu_copyselection")
193 self.threadview.menu_openasuri = self.widget_tree.get_widget(
194 "popup_threadview_menu_openasuri")
195 self.threadview.menu_separator_selection = self.widget_tree.get_widget(
196 "popup_threadview_menu_separator_selection")
198 def initialize_buffer(self):
199 self.threadview.initialize_buffer()
203 self.window.destroy()
206 return self.bbs_type.get_thread_uri()
209 self.window.deiconify()
212 self.window.iconify()
214 def _show_submit_window(self):
215 submit_window.open(self.bbs_type.get_thread_uri())
217 def _toggle_toolbar(self):
218 if self.toolbar.get_property("visible"):
223 def _toggle_statusbar(self):
224 if self.statusbar.get_property("visible"):
225 self.statusbar.hide()
227 self.statusbar.show()
229 def _close_window(self):
232 def _quit_session(self):
235 def _regist_as_bookmark(self):
236 bookmark_list.bookmark_list.add_bookmark_with_edit(
237 name=self.title, uri=self.bbs_type.uri)
239 def _manage_bookmarks(self):
240 bookmark_window.open()
242 def _show_board(self):
243 board_window.open_board(self.bbs_type.get_uri_base())
245 def _delete_log(self):
247 dat_path = misc.get_thread_dat_path(self.bbs_type)
250 traceback.print_exc()
252 idx_path = misc.get_thread_idx_path(self.bbs_type)
255 traceback.print_exc()
257 states_path = misc.get_thread_states_path(self.bbs_type)
258 os.remove(states_path)
260 traceback.print_exc()
262 def _open_uri(self, uri):
263 if not uri.startswith("http://"):
264 # maybe a relative uri.
265 uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
268 uri_opener.open_uri(uri)
269 except bbs_type_exception.BbsTypeError:
270 # not supported, show with the web browser.
273 def _copy_text_to_clipboard(self, text):
274 if text and len(text) > 0:
275 clip = gtk.Clipboard()
276 text = text.encode("utf8")
277 clip.set_text(text, len(text))
279 def _modify_uri(self, uri):
280 if not uri.startswith("http://"):
281 uri = "http://" + uri
284 def http_get_dat(self, on_get_res):
285 datfile_url = self.bbs_type.get_dat_uri()
287 idx_dic = idxfile.load_idx(self.bbs_type)
288 lastmod = idx_dic["lastModified"]
289 etag = idx_dic["etag"]
291 req = urllib2.Request(datfile_url)
292 req.add_header("User-agent", config.User_Agent)
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 self.statusbar.pop(self.statusbar_context_id)
302 self.statusbar.push(self.statusbar_context_id, "GET...")
303 gobject.idle_add(push)
305 req = self.bbs_type.set_extra_dat_request(req, self)
307 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
309 res = opener.open(req)
310 except urllib2.HTTPError, e:
312 message = "%d %s" % (code, msg)
313 self.statusbar.pop(self.statusbar_context_id)
314 self.statusbar.push(self.statusbar_context_id, message)
315 gobject.idle_add(push, e.code, e.msg)
319 if "Last-Modified" in headers:
320 la = headers["Last-Modified"]
321 def push(code, msg, lastm):
322 message = "%d %s [%s]" % (code, msg, lastm)
323 self.statusbar.pop(self.statusbar_context_id)
324 self.statusbar.push(self.statusbar_context_id, message)
325 gobject.idle_add(push, res.code, res.msg, la)
328 message = "%d %s" % (code, msg)
329 self.statusbar.pop(self.statusbar_context_id)
330 self.statusbar.push(self.statusbar_context_id, message)
331 gobject.idle_add(push, res.code, res.msg)
333 maybe_incomplete = False
335 if not line.endswith("\n"):
336 maybe_incomplete = True
337 print "does not end with \\n. maybe incomplete"
347 if "Last-Modified" in headers:
348 lastmod = headers["Last-Modified"]
349 if "ETag" in headers:
350 etag = headers["Etag"]
354 idx_dic = {"title": self.title, "lineCount": self.num,
355 "lastModified": lastmod, "etag": etag}
356 idxfile.save_idx(self.bbs_type, idx_dic)
358 gobject.idle_add(session.thread_idx_updated,
359 self.bbs_type.get_thread_uri(), idx_dic)
363 self.jump_request_num = 0
366 line_count = datfile.get_dat_line_count(self.bbs_type)
367 if line_count < self.num:
371 gobject.idle_add(self.initialize_buffer)
373 if line_count > self.num:
374 datfile.load_dat_partly(
375 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
378 if self.jump_request_num:
379 if self.jump_request_num <= num:
380 num = self.jump_request_num
381 self.jump_request_num = 0
382 self.jump_to_res(num)
384 self.jump_to_the_end()
386 gobject.idle_add(do_jump)
389 dat_path = misc.get_thread_dat_path(self.bbs_type)
390 dat_file = FileWrap(dat_path)
392 def save_line_and_append_to_buffer(line):
393 dat_file.seek(self.size)
395 self.append_rawres_to_buffer(line)
397 self.http_get_dat(save_line_and_append_to_buffer)
398 gtk.gdk.threads_enter()
399 self.threadview.redraw()
400 gtk.gdk.threads_leave()
404 if self.jump_request_num:
405 num = self.jump_request_num
406 self.jump_request_num = 0
407 self.jump_to_res(num)
409 gobject.idle_add(do_jump)
415 self.progress = False
418 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
425 self.jump_request_num = 0
428 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
433 if self.jump_request_num:
434 num = self.jump_request_num
435 self.jump_request_num = 0
436 self.jump_to_res(num)
438 self.jump_to_the_end()
440 gobject.idle_add(do_jump)
446 self.progress = False
449 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
452 def append_rawres_to_buffer(self, line):
453 self.size += len(line)
456 if not self.title and self.num == 1:
457 title = self.bbs_type.get_title_from_dat(line)
460 gobject.idle_add(self.window.set_title, title)
462 line = line.decode(self.bbs_type.encoding, "replace")
463 m = self.bbs_type.dat_reg.match(line)
465 name = m.group("name")
466 mail = m.group("mail")
467 date = m.group("date")
470 num = int(m.group("num"))
472 # use simple counter num
484 self.reselems_to_buffer(num, name, mail, date, msg)
486 self.reselems_to_buffer(
487 str(self.num), "Invalid Name", "Invalid Mail",
488 "Invalid Date", line)
489 print "maybe syntax error.", self.num, line
491 def reselems_to_buffer(self, num, name, mail, date, msg):
492 pipe = HTMLParserToThreadView(self.threadview, num, 0)
493 p = barehtmlparser.BareHTMLParser(
494 pipe.from_html_parser, pipe.on_new_line)
496 # First, create a pango layout for num,name,mail,date
499 p.feed(str(num) + " ")
502 p.feed("<b>" + name + "</b>")
505 p.feed("[" + mail + "]")
511 pipe.to_thread_view()
514 # Second, create a pango layout for message
515 # 'margin left' is 20
517 pipe.set_left_margin(20)
518 p.feed(msg.lstrip(" "))
523 pipe.to_thread_view()
525 def jump(self, value):
526 gobject.idle_add(self.threadview.jump, value)
528 def jump_to_layout(self, layout):
529 gobject.idle_add(self.threadview.jump_to_layout, layout)
531 def jump_to_the_end(self):
532 gobject.idle_add(self.threadview.jump_to_the_end)
536 print "locked, try later."
544 self.lock_obj = False
547 def jump_to_res(self, resnum):
548 if self.threadview.jump_to_res(resnum):
550 self.jump_request_num = resnum
552 def load(self, update=False):
553 dat_path = misc.get_thread_dat_path(self.bbs_type)
554 dat_exists = os.path.exists(dat_path)
555 if update or not dat_exists:
562 states_path = misc.get_thread_states_path(self.bbs_type)
563 dat_path = misc.get_thread_dat_path(self.bbs_type)
565 # save only if dat file exists.
566 if os.path.exists(dat_path):
567 window_width, window_height = self.window.get_size()
568 toolbar_visible = self.toolbar.get_property("visible")
569 statusbar_visible = self.statusbar.get_property("visible")
571 dirname = os.path.dirname(states_path)
572 if not os.path.isdir(dirname):
575 f = file(states_path, "w")
577 f.write("window_width=" + str(window_width) + "\n")
578 f.write("window_height=" + str(window_height) + "\n")
579 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
580 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
584 traceback.print_exc()
590 toolbar_visible = True
591 statusbar_visible = True
594 key_base = config.gconf_app_key_base() + "/thread_states"
595 gconf_client = gconf.client_get_default()
596 width = gconf_client.get_int(key_base + "/window_width")
597 height = gconf_client.get_int(key_base + "/window_height")
598 toolbar_visible = gconf_client.get_bool(
599 key_base + "/toolbar")
600 statusbar_visible = gconf_client.get_bool(
601 key_base + "/statusbar")
605 window_height = height
607 traceback.print_exc()
609 states_path = misc.get_thread_states_path(self.bbs_type)
610 if os.path.exists(states_path):
611 for line in file(states_path):
612 if line.startswith("window_height="):
613 height = window_height
616 line[len("window_height="):].rstrip("\n"))
620 window_height = height
621 elif line.startswith("window_width="):
625 line[len("window_width="):].rstrip("\n"))
630 elif line.startswith("toolbar_visible="):
631 tbar = line[len("toolbar_visible="):].rstrip("\n")
632 toolbar_visible = tbar == "True"
633 elif line.startswith("statusbar_visible="):
634 sbar = line[len("statusbar_visible="):].rstrip("\n")
635 statusbar_visible = sbar == "True"
637 self.window.set_default_size(window_width, window_height)
639 if not toolbar_visible:
640 gobject.idle_add(self.toolbar.hide,
641 priority=gobject.PRIORITY_HIGH)
642 if not statusbar_visible:
643 gobject.idle_add(self.statusbar.hide,
644 priority=gobject.PRIORITY_HIGH)
646 traceback.print_exc()
651 def on_thread_view_uri_clicked(self, widget, uri):
654 def on_thread_popup_uri_clicked(self, widget, threadview, uri):
657 def on_thread_window_delete_event(self, widget, event):
661 def on_thread_window_destroy(self, widget):
671 def on_menu_file_show_board_activate(self, widget):
674 def on_menu_file_compose_activate(self, widget):
675 self._show_submit_window()
677 def on_menu_file_delete_activate(self, widget):
680 def on_menu_file_close_activate(self, widget):
683 def on_menu_file_quit_activate(self, widget):
688 def on_menu_view_refresh_activate(self, widget):
691 def on_menu_view_toolbar_activate(self, widget):
692 self._toggle_toolbar()
694 def on_menu_view_statusbar_activate(self, widget):
695 self._toggle_statusbar()
699 def on_menu_bookmarks_bookmarkthispage_activate(self, widget):
700 self._regist_as_bookmark()
702 def on_menu_bookmarks_showbookmarks_activate(self, widget):
703 self._manage_bookmarks()
707 def on_toolbutton_refresh_activate(self, widget):
710 def on_toolbutton_showboard_activate(self, widget):
713 def on_toolbutton_compose_activate(self, widget):
714 self._show_submit_window()
716 def on_toolbutton_delete_activate(self, widget):
721 def on_popup_threadview_menu_openuri_activate(self, widget):
722 self._open_uri(widget.uri)
724 def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
725 self._copy_text_to_clipboard(widget.uri)
727 def on_popup_threadview_menu_copyselection_activate(self, widget):
728 text = self.threadview.get_selected_text()
729 self._copy_text_to_clipboard(text)
731 def on_popup_threadview_menu_openasuri_activate(self, widget):
732 text = self.threadview.get_selected_text()
733 uri = self._modify_uri(text)
736 def on_popup_threadview_menu_refresh_activate(self, widget):