OSDN Git Service

ThreadView, character widths are cached. This is expected to improve performance.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_window.py
1 # Copyright (C) 2006 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
3 #
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.
8 #
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.
13 #
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
17
18 import pygtk
19 pygtk.require('2.0')
20 import gtk
21 import gtk.glade
22 import os.path
23 import codecs
24 import re
25 import pango
26 import urllib2
27 import urlparse
28 import gnome
29 import gobject
30 import threading
31 import gconf
32 import traceback
33 import itertools
34 import os
35 import sys
36
37 import misc
38 from misc import FileWrap, ThreadInvoker
39 import datfile
40 import barehtmlparser
41 import idxfile
42 import session
43 import board_window
44 import uri_opener
45 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
46 from BbsType import bbs_type_judge_uri
47 from BbsType import bbs_type_exception
48 import config
49 import winwrapbase
50 import bookmark_list
51 import bookmark_window
52 import thread_view
53 import thread_popup
54 import submit_window
55
56 GLADE_FILENAME = "thread_window.glade"
57
58 def open_thread(uri, update=False):
59     if not uri:
60         raise ValueError, "parameter must not be empty"
61
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
67
68     winwrap = session.get_window(uri)
69     if winwrap:
70         # already opened
71         winwrap.window.present()
72         if update:
73             winwrap.load(update)
74     else:
75         winwrap = WinWrap(bbs_type.uri)  # pass original uri
76         winwrap.load(update)
77
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)
84         if match:
85             resnum = int(match.group())
86             winwrap.jump_to_res(resnum)
87
88
89 class HTMLParserToThreadView:
90     def __init__(self, threadview, resnum, left_margin):
91         self.threadview = threadview
92         self.resnum = resnum
93         self.left_margin = left_margin
94         self.initialize()
95
96     def set_left_margin(self, left_margin):
97         self.left_margin = left_margin
98
99     def initialize(self):
100         self.layout = None
101
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)
106
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)
111
112         gtk.gdk.threads_enter()
113         self.layout.add_text(data, bold, href)
114         gtk.gdk.threads_leave()
115
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()
122             self.initialize()
123
124
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)
129
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
137         self.size = 0
138         self.num = 0
139         self.title = ""
140         self.lock_obj = False
141         self.jump_request_num = 0
142         self.progress = False
143
144         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
145         self.widget_tree = gtk.glade.XML(glade_path)
146         self._get_widgets()
147         self.widget_tree.signal_autoconnect(self)
148
149         self.toolbar.unset_style()
150
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)
157
158         self._get_popupmenu_widgets()
159
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)
164
165         self.statusbar_context_id = self.statusbar.get_context_id(
166             "Thread Window Status")
167         self.statusbar.push(self.statusbar_context_id, "OK.")
168
169         self.initialize_buffer()
170
171         self.restore()
172         self.window.show_all()
173
174         self.created()
175
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")
181
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")
197
198     def initialize_buffer(self):
199         self.threadview.initialize_buffer()
200
201     def destroy(self):
202         self.save()
203         self.window.destroy()
204
205     def get_uri(self):
206         return self.bbs_type.get_thread_uri()
207
208     def show(self):
209         self.window.deiconify()
210
211     def hide(self):
212         self.window.iconify()
213
214     def _show_submit_window(self):
215         submit_window.open(self.bbs_type.get_thread_uri())
216
217     def _toggle_toolbar(self):
218         if self.toolbar.get_property("visible"):
219             self.toolbar.hide()
220         else:
221             self.toolbar.show()
222
223     def _toggle_statusbar(self):
224         if self.statusbar.get_property("visible"):
225             self.statusbar.hide()
226         else:
227             self.statusbar.show()
228
229     def _close_window(self):
230         self.destroy()
231
232     def _quit_session(self):
233         session.main_quit()
234
235     def _regist_as_bookmark(self):
236         bookmark_list.bookmark_list.add_bookmark_with_edit(
237             name=self.title, uri=self.bbs_type.uri)
238
239     def _manage_bookmarks(self):
240         bookmark_window.open()
241
242     def _show_board(self):
243         board_window.open_board(self.bbs_type.get_uri_base())
244
245     def _delete_log(self):
246         try:
247             dat_path = misc.get_thread_dat_path(self.bbs_type)
248             os.remove(dat_path)
249         except OSError:
250             traceback.print_exc()
251         try:
252             idx_path = misc.get_thread_idx_path(self.bbs_type)
253             os.remove(idx_path)
254         except OSError:
255             traceback.print_exc()
256         try:
257             states_path = misc.get_thread_states_path(self.bbs_type)
258             os.remove(states_path)
259         except OSError:
260             traceback.print_exc()
261
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)
266
267         try:
268             uri_opener.open_uri(uri)
269         except bbs_type_exception.BbsTypeError:
270             # not supported, show with the web browser.
271             gnome.url_show(uri)
272
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))
278
279     def _modify_uri(self, uri):
280         if not uri.startswith("http://"):
281             uri = "http://" + uri
282         return uri
283
284     def http_get_dat(self, on_get_res):
285         datfile_url = self.bbs_type.get_dat_uri()
286
287         idx_dic = idxfile.load_idx(self.bbs_type)
288         lastmod = idx_dic["lastModified"]
289         etag = idx_dic["etag"]
290
291         req = urllib2.Request(datfile_url)
292         req.add_header("User-agent", config.User_Agent)
293         if self.size > 0:
294             req.add_header("Range", "bytes=" + str(self.size) + "-")
295         if lastmod:
296             req.add_header("If-Modified-Since", lastmod)
297         if etag:
298             req.add_header("If-None-Match", etag)
299
300         def push():
301             self.statusbar.pop(self.statusbar_context_id)
302             self.statusbar.push(self.statusbar_context_id, "GET...")
303         gobject.idle_add(push)
304
305         req = self.bbs_type.set_extra_dat_request(req, self)
306
307         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
308         try:
309             res = opener.open(req)
310         except urllib2.HTTPError, e:
311             def push(code, msg):
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)
316         else:
317             headers = res.info()
318
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)
326             else:
327                 def push(code, msg):
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)
332
333             maybe_incomplete = False
334             for line in res:
335                 if not line.endswith("\n"):
336                     maybe_incomplete = True
337                     print "does not end with \\n. maybe incomplete"
338                     break
339                 on_get_res(line)
340
341             res.close()
342
343             if maybe_incomplete:
344                 lastmod = None
345                 etag = None
346             else:
347                 if "Last-Modified" in headers:
348                     lastmod = headers["Last-Modified"]
349                 if "ETag" in headers:
350                     etag = headers["Etag"]
351
352             if self.num > 0:
353                 # save idx
354                 idx_dic = {"title": self.title, "lineCount": self.num,
355                        "lastModified": lastmod, "etag": etag}
356                 idxfile.save_idx(self.bbs_type, idx_dic)
357
358                 gobject.idle_add(session.thread_idx_updated,
359                                  self.bbs_type.get_thread_uri(), idx_dic)
360
361     def update(self):
362
363         self.jump_request_num = 0
364
365         def load():
366             line_count = datfile.get_dat_line_count(self.bbs_type)
367             if line_count < self.num:
368                 self.num = 0
369                 self.size = 0
370
371                 gobject.idle_add(self.initialize_buffer)
372
373             if line_count > self.num:
374                 datfile.load_dat_partly(
375                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
376
377                 def do_jump():
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)
383                     else:
384                         self.jump_to_the_end()
385
386                 gobject.idle_add(do_jump)
387
388         def get():
389             dat_path = misc.get_thread_dat_path(self.bbs_type)
390             dat_file = FileWrap(dat_path)
391
392             def save_line_and_append_to_buffer(line):
393                 dat_file.seek(self.size)
394                 dat_file.write(line)
395                 self.append_rawres_to_buffer(line)
396
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()
401             dat_file.close()
402
403             def do_jump():
404                 if self.jump_request_num:
405                     num = self.jump_request_num
406                     self.jump_request_num = 0
407                     self.jump_to_res(num)
408
409             gobject.idle_add(do_jump)
410
411         if self.lock():
412
413             def on_end():
414                 self.un_lock()
415                 self.progress = False
416
417             self.progress = True
418             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
419             t.start()
420
421     def load_dat(self):
422
423         self.size = 0
424         self.num = 0
425         self.jump_request_num = 0
426
427         def load():
428             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
429
430         def jump():
431
432             def do_jump():
433                 if self.jump_request_num:
434                     num = self.jump_request_num
435                     self.jump_request_num = 0
436                     self.jump_to_res(num)
437                 else:
438                     self.jump_to_the_end()
439
440             gobject.idle_add(do_jump)
441
442         if self.lock():
443
444             def on_end():
445                 self.un_lock()
446                 self.progress = False
447
448             self.progress = True
449             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
450             t.start()
451
452     def append_rawres_to_buffer(self, line):
453         self.size += len(line)
454         self.num += 1
455
456         if not self.title and self.num == 1:
457             title = self.bbs_type.get_title_from_dat(line)
458             if title:
459                 self.title = title
460                 gobject.idle_add(self.window.set_title, title)
461
462         line = line.decode(self.bbs_type.encoding, "replace")
463         m = self.bbs_type.dat_reg.match(line)
464         if m:
465             name = m.group("name")
466             mail = m.group("mail")
467             date = m.group("date")
468             msg = m.group("msg")
469             try:
470                 num = int(m.group("num"))
471             except IndexError:
472                 # use simple counter num
473                 num = self.num
474             else:
475                 # use num in dat
476                 self.num = num
477             try:
478                 id = m.group("id")
479             except IndexError:
480                 pass
481             else:
482                 if id:
483                     date += " ID:" + id
484             self.reselems_to_buffer(num, name, mail, date, msg)
485         else:
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
490
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)
495
496         # First, create a pango layout for num,name,mail,date
497         # 'margin left' is 0
498         # number
499         p.feed(str(num) + " ")
500
501         # name
502         p.feed("<b>" + name + "</b>")
503
504         # mail
505         p.feed("[" + mail + "]")
506
507         # date
508         p.feed(date)
509         p.flush()
510
511         pipe.to_thread_view()
512
513
514         # Second, create a pango layout for message
515         # 'margin left' is 20
516         # msg
517         pipe.set_left_margin(20)
518         p.feed(msg.lstrip(" "))
519
520         p.feed("<br>")
521         p.close()
522
523         pipe.to_thread_view()
524
525     def jump(self, value):
526         gobject.idle_add(self.threadview.jump, value)
527
528     def jump_to_layout(self, layout):
529         gobject.idle_add(self.threadview.jump_to_layout, layout)
530         
531     def jump_to_the_end(self):
532         gobject.idle_add(self.threadview.jump_to_the_end)
533
534     def lock(self):
535         if self.lock_obj:
536             print "locked, try later."
537             return False
538         else:
539             print "get lock"
540             self.lock_obj = True
541             return True
542
543     def un_lock(self):
544         self.lock_obj = False
545         print "unlock"
546
547     def jump_to_res(self, resnum):
548         if self.threadview.jump_to_res(resnum):
549             return
550         self.jump_request_num = resnum
551
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:
556             self.update()
557         else:
558             self.load_dat()
559
560     def save(self):
561         try:
562             states_path = misc.get_thread_states_path(self.bbs_type)
563             dat_path = misc.get_thread_dat_path(self.bbs_type)
564
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")
570
571                 dirname = os.path.dirname(states_path)
572                 if not os.path.isdir(dirname):
573                     os.makedirs(dirname)
574
575                 f = file(states_path, "w")
576
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")
581
582                 f.close()
583         except:
584             traceback.print_exc()
585
586     def restore(self):
587         try:
588             window_height = 600
589             window_width = 600
590             toolbar_visible = True
591             statusbar_visible = True
592
593             try:
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")
602                 if width != 0:
603                     window_width = width
604                 if height != 0:
605                     window_height = height
606             except:
607                 traceback.print_exc()
608
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
614                         try:
615                             height = int(
616                                 line[len("window_height="):].rstrip("\n"))
617                         except:
618                             pass
619                         else:
620                             window_height = height
621                     elif line.startswith("window_width="):
622                         width = window_width
623                         try:
624                             width = int(
625                                 line[len("window_width="):].rstrip("\n"))
626                         except:
627                             pass
628                         else:
629                             window_width = width
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"
636
637             self.window.set_default_size(window_width, window_height)
638
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)
645         except:
646             traceback.print_exc()
647
648
649     # signal handlers
650     
651     def on_thread_view_uri_clicked(self, widget, uri):
652         self._open_uri(uri)
653
654     def on_thread_popup_uri_clicked(self, widget, threadview, uri):
655         self._open_uri(uri)
656
657     def on_thread_window_delete_event(self, widget, event):
658         self.save()
659         return False
660
661     def on_thread_window_destroy(self, widget):
662         self.destroyed()
663
664
665
666
667     # menu commands
668
669     # menu file
670
671     def on_menu_file_show_board_activate(self, widget):
672         self._show_board()
673
674     def on_menu_file_compose_activate(self, widget):
675         self._show_submit_window()
676
677     def on_menu_file_delete_activate(self, widget):
678         self._delete_log()
679
680     def on_menu_file_close_activate(self, widget):
681         self._close_window()
682
683     def on_menu_file_quit_activate(self, widget):
684         self._quit_session()
685
686     # menu view
687     
688     def on_menu_view_refresh_activate(self, widget):
689         self.update()
690
691     def on_menu_view_toolbar_activate(self, widget):
692         self._toggle_toolbar()
693
694     def on_menu_view_statusbar_activate(self, widget):
695         self._toggle_statusbar()
696
697     # menu bookmarks
698
699     def on_menu_bookmarks_bookmarkthispage_activate(self, widget):
700         self._regist_as_bookmark()
701
702     def on_menu_bookmarks_showbookmarks_activate(self, widget):
703         self._manage_bookmarks()
704
705     # toolbuttons
706     
707     def on_toolbutton_refresh_activate(self, widget):
708         self.update()
709
710     def on_toolbutton_showboard_activate(self, widget):
711         self._show_board()
712
713     def on_toolbutton_compose_activate(self, widget):
714         self._show_submit_window()
715
716     def on_toolbutton_delete_activate(self, widget):
717         self._delete_log()
718
719     # popup menus
720     
721     def on_popup_threadview_menu_openuri_activate(self, widget):
722         self._open_uri(widget.uri)
723
724     def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
725         self._copy_text_to_clipboard(widget.uri)
726
727     def on_popup_threadview_menu_copyselection_activate(self, widget):
728         text = self.threadview.get_selected_text()
729         self._copy_text_to_clipboard(text)
730
731     def on_popup_threadview_menu_openasuri_activate(self, widget):
732         text = self.threadview.get_selected_text()
733         uri = self._modify_uri(text)
734         self._open_uri(uri)
735
736     def on_popup_threadview_menu_refresh_activate(self, widget):
737         self.update()