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
54 GLADE_FILENAME = "thread_window.glade"
56 def open_thread(uri, update=False):
58 raise ValueError, "parameter must not be empty"
60 bbs_type = bbs_type_judge_uri.get_type(uri)
61 if not bbs_type.is_thread():
62 raise bbs_type_exception.BbsTypeError, \
63 "the uri does not represent thread: " + uri
64 uri = bbs_type.get_thread_uri() # use strict thread uri
66 winwrap = session.get_window(uri)
69 winwrap.window.present()
73 winwrap = WinWrap(bbs_type.uri) # pass original uri
76 # jump to the res if necessary.
77 strict_uri = bbs_type.get_thread_uri()
78 if (bbs_type.uri != strict_uri and
79 bbs_type.uri.startswith(strict_uri)):
80 resnum = bbs_type.uri[len(strict_uri):]
81 match = re.match("\d+", resnum)
83 resnum = int(match.group())
84 winwrap.jump_to_res(resnum)
87 class HTMLParserToThreadView:
88 def __init__(self, threadview, resnum, left_margin):
89 self.threadview = threadview
91 self.left_margin = left_margin
94 def set_left_margin(self, left_margin):
95 self.left_margin = left_margin
100 def on_new_line(self):
101 self.to_thread_view()
102 self.layout = self.threadview.create_res_layout(
103 self.left_margin, self.resnum)
105 def from_html_parser(self, data, bold, href):
106 if self.layout == None:
107 self.layout = self.threadview.create_res_layout(
108 self.left_margin, self.resnum)
110 self.layout.add_text(data, bold, href)
112 def to_thread_view(self):
113 if self.layout is not None:
114 gobject.idle_add(self.threadview.add_layout, self.layout)
118 class WinWrap(winwrapbase.WinWrapBase):
119 hovering_over_link = False
120 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
121 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
123 def __init__(self, uri):
124 from BbsType import bbs_type_judge_uri
125 from BbsType import bbs_type_exception
126 self.bbs_type = bbs_type_judge_uri.get_type(uri)
127 if not self.bbs_type.is_thread():
128 raise bbs_type_exception.BbsTypeError, \
129 "the uri does not represent thread: " + uri
133 self.lock_obj = False
134 self.jump_request_num = 0
135 self.progress = False
137 glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
138 self.widget_tree = gtk.glade.XML(glade_path)
139 self.window = self.widget_tree.get_widget("thread_window")
140 self.toolbar = self.widget_tree.get_widget("toolbar")
141 self.toolbar.unset_style()
142 self.statusbar = self.widget_tree.get_widget("statusbar")
143 self.vbox = self.widget_tree.get_widget("vbox")
145 self.threadview = thread_view.ThreadView()
146 self.vbox.pack_start(self.threadview)
147 self.vbox.reorder_child(self.threadview, 2)
149 self.threadview.on_uri_clicked = self.on_threadview_uri_clicked
151 self.statusbar_context_id = self.statusbar.get_context_id(
152 "Thread Window Status")
153 self.statusbar.push(self.statusbar_context_id, "OK.")
155 self.threadview.popupmenu = self.widget_tree.get_widget(
156 "popup_threadview_menu")
157 self.threadview.menu_openuri = self.widget_tree.get_widget(
158 "popup_threadview_menu_openuri")
159 self.threadview.menu_copylinkaddress = self.widget_tree.get_widget(
160 "popup_threadview_menu_copylinkaddress")
161 self.threadview.menu_separator_link = self.widget_tree.get_widget(
162 "popup_threadview_menu_separator_link")
163 self.threadview.menu_copyselection = self.widget_tree.get_widget(
164 "popup_threadview_menu_copyselection")
165 self.threadview.menu_openasuri = self.widget_tree.get_widget(
166 "popup_threadview_menu_openasuri")
167 self.threadview.menu_separator_selection = self.widget_tree.get_widget(
168 "popup_threadview_menu_separator_selection")
170 self.initialize_buffer()
172 sigdic = {"on_refresh_activate": self.update,
173 "on_compose_activate": self.on_compose_clicked,
174 "on_toolbar_activate": self.on_toolbar_activate,
175 "on_statusbar_activate": self.on_statusbar_activate,
176 "on_refresh_activate": self.update,
177 "on_close_activate": self.on_close_activate,
178 "on_quit_activate": self.on_quit_activate,
179 "on_show_board_activate": self.on_show_board_activate,
180 "on_delete_activate": self.on_delete_activate,
181 "on_thread_window_delete_event":
182 self.on_thread_window_delete_event,
183 "on_add_bookmark_activate": self.on_add_bookmark_activate,
184 "on_manage_bookmarks_activate": \
185 self.on_manage_bookmarks_activate,
186 "on_popup_threadview_menu_openuri_activate":
187 self.on_popup_threadview_menu_openuri_activate,
188 "on_popup_threadview_menu_copylinkaddress_activate":
189 self.on_popup_threadview_menu_copylinkaddress_activate,
190 "on_popup_threadview_menu_copyselection_activate":
191 self.on_popup_threadview_menu_copyselection_activate,
192 "on_popup_threadview_menu_openasuri_activate":
193 self.on_popup_threadview_menu_openasuri_activate,
194 "on_popup_threadview_menu_refresh_activate":
195 self.on_popup_threadview_menu_refresh_activate,
196 "on_thread_window_destroy": self.on_thread_window_destroy}
197 self.widget_tree.signal_autoconnect(sigdic)
200 self.window.show_all()
204 def initialize_buffer(self):
205 self.textbuffer = gtk.TextBuffer()
207 self.enditer = self.textbuffer.get_end_iter()
208 self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
209 self.leftmargintag = self.textbuffer.create_tag()
210 self.leftmargintag.set_property("left-margin", 20)
212 self.threadview.initialize_buffer()
216 self.window.destroy()
219 return self.bbs_type.get_thread_uri()
221 def on_compose_clicked(self, widget):
223 submit_window.open(self.bbs_type.get_thread_uri())
225 def on_toolbar_activate(self, widget):
226 if self.toolbar.get_property("visible"):
231 def on_statusbar_activate(self, widget):
232 if self.statusbar.get_property("visible"):
233 self.statusbar.hide()
235 self.statusbar.show()
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_add_bookmark_activate(self, widget):
251 bookmark_list.bookmark_list.add_bookmark_with_edit(
252 name=self.title, uri=self.bbs_type.uri)
254 def on_manage_bookmarks_activate(self, widget):
255 bookmark_window.open()
257 def on_show_board_activate(self, widget):
258 board_window.open_board(self.bbs_type.get_uri_base())
260 def on_delete_activate(self, widget):
262 dat_path = misc.get_thread_dat_path(self.bbs_type)
265 traceback.print_exc()
267 idx_path = misc.get_thread_idx_path(self.bbs_type)
270 traceback.print_exc()
272 states_path = misc.get_thread_states_path(self.bbs_type)
273 os.remove(states_path)
275 traceback.print_exc()
277 def on_threadview_uri_clicked(self, uri):
279 if not uri.startswith("http://"):
280 # maybe a relative uri.
281 uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
284 uri_opener.open_uri(uri)
285 except bbs_type_exception.BbsTypeError:
286 # not supported, show with the web browser.
289 def on_popup_threadview_menu_openuri_activate(self, widget):
290 self.on_threadview_uri_clicked(widget.uri)
292 def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
293 clip = gtk.Clipboard()
294 clip.set_text(widget.uri, len(widget.uri))
296 def on_popup_threadview_menu_copyselection_activate(self, widget):
297 text = self.threadview.get_selected_text()
298 if text and len(text) > 0:
299 clip = gtk.Clipboard()
300 text = text.encode("utf8")
301 clip.set_text(text, len(text))
303 def on_popup_threadview_menu_openasuri_activate(self, widget):
304 self.on_threadview_uri_clicked(self.threadview.get_selected_text())
306 def on_popup_threadview_menu_refresh_activate(self, widget):
309 def http_get_dat(self, on_get_res):
310 datfile_url = self.bbs_type.get_dat_uri()
312 idx_dic = idxfile.load_idx(self.bbs_type)
313 lastmod = idx_dic["lastModified"]
314 etag = idx_dic["etag"]
316 req = urllib2.Request(datfile_url)
317 req.add_header("User-agent", config.User_Agent)
319 req.add_header("Range", "bytes=" + str(self.size) + "-")
321 req.add_header("If-Modified-Since", lastmod)
323 req.add_header("If-None-Match", etag)
326 self.statusbar.pop(self.statusbar_context_id)
327 self.statusbar.push(self.statusbar_context_id, "GET...")
328 gobject.idle_add(push)
330 req = self.bbs_type.set_extra_dat_request(req, self)
332 opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
334 res = opener.open(req)
335 except urllib2.HTTPError, e:
337 message = "%d %s" % (code, msg)
338 self.statusbar.pop(self.statusbar_context_id)
339 self.statusbar.push(self.statusbar_context_id, message)
340 gobject.idle_add(push, e.code, e.msg)
344 if "Last-Modified" in headers:
345 la = headers["Last-Modified"]
346 def push(code, msg, lastm):
347 message = "%d %s [%s]" % (code, msg, lastm)
348 self.statusbar.pop(self.statusbar_context_id)
349 self.statusbar.push(self.statusbar_context_id, message)
350 gobject.idle_add(push, res.code, res.msg, la)
353 message = "%d %s" % (code, msg)
354 self.statusbar.pop(self.statusbar_context_id)
355 self.statusbar.push(self.statusbar_context_id, message)
356 gobject.idle_add(push, res.code, res.msg)
358 maybe_incomplete = False
360 if not line.endswith("\n"):
361 maybe_incomplete = True
362 print "does not end with \\n. maybe incomplete"
372 if "Last-Modified" in headers:
373 lastmod = headers["Last-Modified"]
374 if "ETag" in headers:
375 etag = headers["Etag"]
379 idx_dic = {"title": self.title, "lineCount": self.num,
380 "lastModified": lastmod, "etag": etag}
381 idxfile.save_idx(self.bbs_type, idx_dic)
383 gobject.idle_add(session.thread_idx_updated,
384 self.bbs_type.get_thread_uri(), idx_dic)
386 def update(self, widget=None):
388 self.jump_request_num = 0
393 self.textbuffer.create_mark("1", self.enditer, True)
394 gobject.idle_add(create_mark)
396 line_count = datfile.get_dat_line_count(self.bbs_type)
397 if line_count < self.num:
401 gobject.idle_add(self.initialize_buffer)
403 if line_count > self.num:
404 datfile.load_dat_partly(
405 self.bbs_type, self.append_rawres_to_buffer, self.num+1)
408 if self.jump_request_num:
409 if self.jump_request_num <= num:
410 num = self.jump_request_num
411 self.jump_request_num = 0
412 self.jump_to_res(num)
414 self.jump_to_the_end()
416 gobject.idle_add(do_jump)
419 dat_path = misc.get_thread_dat_path(self.bbs_type)
420 dat_file = FileWrap(dat_path)
422 def save_line_and_append_to_buffer(line):
423 dat_file.seek(self.size)
425 self.append_rawres_to_buffer(line)
427 self.http_get_dat(save_line_and_append_to_buffer)
431 if self.jump_request_num:
432 num = self.jump_request_num
433 self.jump_request_num = 0
434 self.jump_to_res(num)
436 gobject.idle_add(do_jump)
442 self.progress = False
445 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
452 self.jump_request_num = 0
455 datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
460 if self.jump_request_num:
461 num = self.jump_request_num
462 self.jump_request_num = 0
463 self.jump_to_res(num)
465 self.jump_to_the_end()
467 gobject.idle_add(do_jump)
473 self.progress = False
476 t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
479 def append_rawres_to_buffer(self, line):
480 self.size += len(line)
483 if not self.title and self.num == 1:
484 title = self.bbs_type.get_title_from_dat(line)
487 gobject.idle_add(self.window.set_title, title)
489 line = line.decode(self.bbs_type.encoding, "replace")
490 m = self.bbs_type.dat_reg.match(line)
492 name = m.group("name")
493 mail = m.group("mail")
494 date = m.group("date")
497 num = int(m.group("num"))
499 # use simple counter num
511 self.reselems_to_buffer(num, name, mail, date, msg)
513 self.reselems_to_buffer(
514 str(self.num), "Invalid Name", "Invalid Mail",
515 "Invalid Date", line)
516 print "maybe syntax error.", self.num, line
518 def reselems_to_buffer(self, num, name, mail, date, msg):
519 pipe = HTMLParserToThreadView(self.threadview, num, 0)
520 p = barehtmlparser.BareHTMLParser(
521 pipe.from_html_parser, pipe.on_new_line)
523 # First, create a pango layout for num,name,mail,date
526 p.feed(str(num) + " ")
529 p.feed("<b>" + name + "</b>")
532 p.feed("[" + mail + "]")
538 pipe.to_thread_view()
541 # Second, create a pango layout for message
542 # 'margin left' is 20
544 pipe.set_left_margin(20)
545 p.feed(msg.lstrip(" "))
550 pipe.to_thread_view()
552 def href_tag(self, href):
553 tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
554 tag.set_data("href", href)
557 def jump(self, value):
558 gobject.idle_add(self.threadview.jump, value)
560 def jump_to_layout(self, layout):
561 gobject.idle_add(self.threadview.jump_to_layout, layout)
563 def jump_to_the_end(self):
564 gobject.idle_add(self.threadview.jump_to_the_end)
568 print "locked, try later."
576 self.lock_obj = False
579 def jump_to_res(self, resnum):
580 if self.threadview.jump_to_res(resnum):
582 self.jump_request_num = resnum
584 def load(self, update=False):
585 dat_path = misc.get_thread_dat_path(self.bbs_type)
586 dat_exists = os.path.exists(dat_path)
587 if update or not dat_exists:
594 states_path = misc.get_thread_states_path(self.bbs_type)
595 dat_path = misc.get_thread_dat_path(self.bbs_type)
597 # save only if dat file exists.
598 if os.path.exists(dat_path):
599 window_width, window_height = self.window.get_size()
600 toolbar_visible = self.toolbar.get_property("visible")
601 statusbar_visible = self.statusbar.get_property("visible")
603 dirname = os.path.dirname(states_path)
604 if not os.path.isdir(dirname):
607 f = file(states_path, "w")
609 f.write("window_width=" + str(window_width) + "\n")
610 f.write("window_height=" + str(window_height) + "\n")
611 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
612 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
616 traceback.print_exc()
622 toolbar_visible = True
623 statusbar_visible = True
626 key_base = config.gconf_app_key_base() + "/thread_states"
627 gconf_client = gconf.client_get_default()
628 width = gconf_client.get_int(key_base + "/window_width")
629 height = gconf_client.get_int(key_base + "/window_height")
630 toolbar_visible = gconf_client.get_bool(
631 key_base + "/toolbar")
632 statusbar_visible = gconf_client.get_bool(
633 key_base + "/statusbar")
637 window_height = height
639 traceback.print_exc()
641 states_path = misc.get_thread_states_path(self.bbs_type)
642 if os.path.exists(states_path):
643 for line in file(states_path):
644 if line.startswith("window_height="):
645 height = window_height
648 line[len("window_height="):].rstrip("\n"))
652 window_height = height
653 elif line.startswith("window_width="):
657 line[len("window_width="):].rstrip("\n"))
662 elif line.startswith("toolbar_visible="):
663 tbar = line[len("toolbar_visible="):].rstrip("\n")
664 toolbar_visible = tbar == "True"
665 elif line.startswith("statusbar_visible="):
666 sbar = line[len("statusbar_visible="):].rstrip("\n")
667 statusbar_visible = sbar == "True"
669 self.window.set_default_size(window_width, window_height)
671 if not toolbar_visible:
672 gobject.idle_add(self.toolbar.hide,
673 priority=gobject.PRIORITY_HIGH)
674 if not statusbar_visible:
675 gobject.idle_add(self.statusbar.hide,
676 priority=gobject.PRIORITY_HIGH)
678 traceback.print_exc()