OSDN Git Service

dbc758449a468da5d597a7cc6598dd2db0c658e4
[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         self.layout.add_text(data, bold, href)
113
114     def to_thread_view(self):
115         if self.layout is not None:
116             # gobject.idle_add(self.threadview.add_layout, self.layout)
117             gtk.gdk.threads_enter()
118             self.threadview.add_layout(self.layout)
119             gtk.gdk.threads_leave()
120             self.initialize()
121
122
123 class WinWrap(winwrapbase.WinWrapBase):
124     hovering_over_link = False
125     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
126     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
127
128     def __init__(self, uri):
129         from BbsType import bbs_type_judge_uri
130         from BbsType import bbs_type_exception
131         self.bbs_type = bbs_type_judge_uri.get_type(uri)
132         if not self.bbs_type.is_thread():
133             raise bbs_type_exception.BbsTypeError, \
134                   "the uri does not represent thread: " + uri
135         self.size = 0
136         self.num = 0
137         self.title = ""
138         self.lock_obj = False
139         self.jump_request_num = 0
140         self.progress = False
141
142         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
143         self.widget_tree = gtk.glade.XML(glade_path)
144         self._get_widgets()
145         self.widget_tree.signal_autoconnect(self)
146
147         self.toolbar.unset_style()
148
149         self.threadview = thread_view.ThreadView()
150         self.threadpopup = thread_popup.ThreadPopup(self.bbs_type)
151         self.threadpopup.push_thread_view(self.threadview)
152         self.vbox.pack_start(self.threadview)
153         self.vbox.reorder_child(self.threadview, 2)
154         self.window.set_focus(self.threadview.drawingarea)
155
156         self._get_popupmenu_widgets()
157
158         self.threadview.connect(
159             "uri-clicked-event", self.on_thread_view_uri_clicked)
160         self.threadpopup.connect(
161             "uri-clicked-event", self.on_thread_popup_uri_clicked)
162
163         self.statusbar_context_id = self.statusbar.get_context_id(
164             "Thread Window Status")
165         self.statusbar.push(self.statusbar_context_id, "OK.")
166
167         self.initialize_buffer()
168
169         self.restore()
170         self.window.show_all()
171
172         self.created()
173
174     def _get_widgets(self):
175         self.window = self.widget_tree.get_widget("thread_window")
176         self.toolbar = self.widget_tree.get_widget("toolbar")
177         self.statusbar = self.widget_tree.get_widget("statusbar")
178         self.vbox = self.widget_tree.get_widget("vbox")
179
180     def _get_popupmenu_widgets(self):
181         self.threadview.popupmenu = self.widget_tree.get_widget(
182             "popup_threadview_menu")
183         self.threadview.menu_openuri = self.widget_tree.get_widget(
184             "popup_threadview_menu_openuri")
185         self.threadview.menu_copylinkaddress = self.widget_tree.get_widget(
186             "popup_threadview_menu_copylinkaddress")
187         self.threadview.menu_separator_link = self.widget_tree.get_widget(
188             "popup_threadview_menu_separator_link")
189         self.threadview.menu_copyselection = self.widget_tree.get_widget(
190             "popup_threadview_menu_copyselection")
191         self.threadview.menu_openasuri = self.widget_tree.get_widget(
192             "popup_threadview_menu_openasuri")
193         self.threadview.menu_separator_selection = self.widget_tree.get_widget(
194             "popup_threadview_menu_separator_selection")
195
196     def initialize_buffer(self):
197         self.threadview.initialize_buffer()
198
199     def destroy(self):
200         self.save()
201         self.window.destroy()
202
203     def get_uri(self):
204         return self.bbs_type.get_thread_uri()
205
206     def _show_submit_window(self):
207         submit_window.open(self.bbs_type.get_thread_uri())
208
209     def _toggle_toolbar(self):
210         if self.toolbar.get_property("visible"):
211             self.toolbar.hide()
212         else:
213             self.toolbar.show()
214
215     def _toggle_statusbar(self):
216         if self.statusbar.get_property("visible"):
217             self.statusbar.hide()
218         else:
219             self.statusbar.show()
220
221     def _close_window(self):
222         self.destroy()
223
224     def _quit_session(self):
225         session.main_quit()
226
227     def _regist_as_bookmark(self):
228         bookmark_list.bookmark_list.add_bookmark_with_edit(
229             name=self.title, uri=self.bbs_type.uri)
230
231     def _manage_bookmarks(self):
232         bookmark_window.open()
233
234     def _show_board(self):
235         board_window.open_board(self.bbs_type.get_uri_base())
236
237     def _delete_log(self):
238         try:
239             dat_path = misc.get_thread_dat_path(self.bbs_type)
240             os.remove(dat_path)
241         except OSError:
242             traceback.print_exc()
243         try:
244             idx_path = misc.get_thread_idx_path(self.bbs_type)
245             os.remove(idx_path)
246         except OSError:
247             traceback.print_exc()
248         try:
249             states_path = misc.get_thread_states_path(self.bbs_type)
250             os.remove(states_path)
251         except OSError:
252             traceback.print_exc()
253
254     def _open_uri(self, uri):
255         if not uri.startswith("http://"):
256             # maybe a relative uri.
257             uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
258
259         try:
260             uri_opener.open_uri(uri)
261         except bbs_type_exception.BbsTypeError:
262             # not supported, show with the web browser.
263             gnome.url_show(uri)
264
265     def _copy_text_to_clipboard(self, text):
266         if text and len(text) > 0:
267             clip = gtk.Clipboard()
268             text = text.encode("utf8")
269             clip.set_text(text, len(text))
270
271     def _modify_uri(self, uri):
272         if not uri.startswith("http://"):
273             uri = "http://" + uri
274         return uri
275
276     def http_get_dat(self, on_get_res):
277         datfile_url = self.bbs_type.get_dat_uri()
278
279         idx_dic = idxfile.load_idx(self.bbs_type)
280         lastmod = idx_dic["lastModified"]
281         etag = idx_dic["etag"]
282
283         req = urllib2.Request(datfile_url)
284         req.add_header("User-agent", config.User_Agent)
285         if self.size > 0:
286             req.add_header("Range", "bytes=" + str(self.size) + "-")
287         if lastmod:
288             req.add_header("If-Modified-Since", lastmod)
289         if etag:
290             req.add_header("If-None-Match", etag)
291
292         def push():
293             self.statusbar.pop(self.statusbar_context_id)
294             self.statusbar.push(self.statusbar_context_id, "GET...")
295         gobject.idle_add(push)
296
297         req = self.bbs_type.set_extra_dat_request(req, self)
298
299         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
300         try:
301             res = opener.open(req)
302         except urllib2.HTTPError, e:
303             def push(code, msg):
304                 message = "%d %s" % (code, msg)
305                 self.statusbar.pop(self.statusbar_context_id)
306                 self.statusbar.push(self.statusbar_context_id, message)
307             gobject.idle_add(push, e.code, e.msg)
308         else:
309             headers = res.info()
310
311             if "Last-Modified" in headers:
312                 la = headers["Last-Modified"]
313                 def push(code, msg, lastm):
314                     message = "%d %s [%s]" % (code, msg, lastm)
315                     self.statusbar.pop(self.statusbar_context_id)
316                     self.statusbar.push(self.statusbar_context_id, message)
317                 gobject.idle_add(push, res.code, res.msg, la)
318             else:
319                 def push(code, msg):
320                     message = "%d %s" % (code, msg)
321                     self.statusbar.pop(self.statusbar_context_id)
322                     self.statusbar.push(self.statusbar_context_id, message)
323                 gobject.idle_add(push, res.code, res.msg)
324
325             maybe_incomplete = False
326             for line in res:
327                 if not line.endswith("\n"):
328                     maybe_incomplete = True
329                     print "does not end with \\n. maybe incomplete"
330                     break
331                 on_get_res(line)
332
333             res.close()
334
335             if maybe_incomplete:
336                 lastmod = None
337                 etag = None
338             else:
339                 if "Last-Modified" in headers:
340                     lastmod = headers["Last-Modified"]
341                 if "ETag" in headers:
342                     etag = headers["Etag"]
343
344             if self.num > 0:
345                 # save idx
346                 idx_dic = {"title": self.title, "lineCount": self.num,
347                        "lastModified": lastmod, "etag": etag}
348                 idxfile.save_idx(self.bbs_type, idx_dic)
349
350                 gobject.idle_add(session.thread_idx_updated,
351                                  self.bbs_type.get_thread_uri(), idx_dic)
352
353     def update(self):
354
355         self.jump_request_num = 0
356
357         def load():
358             line_count = datfile.get_dat_line_count(self.bbs_type)
359             if line_count < self.num:
360                 self.num = 0
361                 self.size = 0
362
363                 gobject.idle_add(self.initialize_buffer)
364
365             if line_count > self.num:
366                 datfile.load_dat_partly(
367                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
368
369                 def do_jump():
370                     if self.jump_request_num:
371                         if self.jump_request_num <= num:
372                             num = self.jump_request_num
373                             self.jump_request_num = 0
374                             self.jump_to_res(num)
375                     else:
376                         self.jump_to_the_end()
377
378                 gobject.idle_add(do_jump)
379
380         def get():
381             dat_path = misc.get_thread_dat_path(self.bbs_type)
382             dat_file = FileWrap(dat_path)
383
384             def save_line_and_append_to_buffer(line):
385                 dat_file.seek(self.size)
386                 dat_file.write(line)
387                 self.append_rawres_to_buffer(line)
388
389             self.http_get_dat(save_line_and_append_to_buffer)
390             gtk.gdk.threads_enter()
391             self.threadview.redraw()
392             gtk.gdk.threads_leave()
393             dat_file.close()
394
395             def do_jump():
396                 if self.jump_request_num:
397                     num = self.jump_request_num
398                     self.jump_request_num = 0
399                     self.jump_to_res(num)
400
401             gobject.idle_add(do_jump)
402
403         if self.lock():
404
405             def on_end():
406                 self.un_lock()
407                 self.progress = False
408
409             self.progress = True
410             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
411             t.start()
412
413     def load_dat(self):
414
415         self.size = 0
416         self.num = 0
417         self.jump_request_num = 0
418
419         def load():
420             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
421
422         def jump():
423
424             def do_jump():
425                 if self.jump_request_num:
426                     num = self.jump_request_num
427                     self.jump_request_num = 0
428                     self.jump_to_res(num)
429                 else:
430                     self.jump_to_the_end()
431
432             gobject.idle_add(do_jump)
433
434         if self.lock():
435
436             def on_end():
437                 self.un_lock()
438                 self.progress = False
439
440             self.progress = True
441             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
442             t.start()
443
444     def append_rawres_to_buffer(self, line):
445         self.size += len(line)
446         self.num += 1
447
448         if not self.title and self.num == 1:
449             title = self.bbs_type.get_title_from_dat(line)
450             if title:
451                 self.title = title
452                 gobject.idle_add(self.window.set_title, title)
453
454         line = line.decode(self.bbs_type.encoding, "replace")
455         m = self.bbs_type.dat_reg.match(line)
456         if m:
457             name = m.group("name")
458             mail = m.group("mail")
459             date = m.group("date")
460             msg = m.group("msg")
461             try:
462                 num = int(m.group("num"))
463             except IndexError:
464                 # use simple counter num
465                 num = self.num
466             else:
467                 # use num in dat
468                 self.num = num
469             try:
470                 id = m.group("id")
471             except IndexError:
472                 pass
473             else:
474                 if id:
475                     date += " ID:" + id
476             self.reselems_to_buffer(num, name, mail, date, msg)
477         else:
478             self.reselems_to_buffer(
479                 str(self.num), "Invalid Name", "Invalid Mail",
480                 "Invalid Date", line)
481             print "maybe syntax error.", self.num, line
482
483     def reselems_to_buffer(self, num, name, mail, date, msg):
484         pipe = HTMLParserToThreadView(self.threadview, num, 0)
485         p = barehtmlparser.BareHTMLParser(
486             pipe.from_html_parser, pipe.on_new_line)
487
488         # First, create a pango layout for num,name,mail,date
489         # 'margin left' is 0
490         # number
491         p.feed(str(num) + " ")
492
493         # name
494         p.feed("<b>" + name + "</b>")
495
496         # mail
497         p.feed("[" + mail + "]")
498
499         # date
500         p.feed(date)
501         p.flush()
502
503         pipe.to_thread_view()
504
505
506         # Second, create a pango layout for message
507         # 'margin left' is 20
508         # msg
509         pipe.set_left_margin(20)
510         p.feed(msg.lstrip(" "))
511
512         p.feed("<br>")
513         p.close()
514
515         pipe.to_thread_view()
516
517     def jump(self, value):
518         gobject.idle_add(self.threadview.jump, value)
519
520     def jump_to_layout(self, layout):
521         gobject.idle_add(self.threadview.jump_to_layout, layout)
522         
523     def jump_to_the_end(self):
524         gobject.idle_add(self.threadview.jump_to_the_end)
525
526     def lock(self):
527         if self.lock_obj:
528             print "locked, try later."
529             return False
530         else:
531             print "get lock"
532             self.lock_obj = True
533             return True
534
535     def un_lock(self):
536         self.lock_obj = False
537         print "unlock"
538
539     def jump_to_res(self, resnum):
540         if self.threadview.jump_to_res(resnum):
541             return
542         self.jump_request_num = resnum
543
544     def load(self, update=False):
545         dat_path = misc.get_thread_dat_path(self.bbs_type)
546         dat_exists = os.path.exists(dat_path)
547         if update or not dat_exists:
548             self.update()
549         else:
550             self.load_dat()
551
552     def save(self):
553         try:
554             states_path = misc.get_thread_states_path(self.bbs_type)
555             dat_path = misc.get_thread_dat_path(self.bbs_type)
556
557             # save only if dat file exists.
558             if os.path.exists(dat_path):
559                 window_width, window_height = self.window.get_size()
560                 toolbar_visible = self.toolbar.get_property("visible")
561                 statusbar_visible = self.statusbar.get_property("visible")
562
563                 dirname = os.path.dirname(states_path)
564                 if not os.path.isdir(dirname):
565                     os.makedirs(dirname)
566
567                 f = file(states_path, "w")
568
569                 f.write("window_width=" + str(window_width) + "\n")
570                 f.write("window_height=" + str(window_height) + "\n")
571                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
572                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
573
574                 f.close()
575         except:
576             traceback.print_exc()
577
578     def restore(self):
579         try:
580             window_height = 600
581             window_width = 600
582             toolbar_visible = True
583             statusbar_visible = True
584
585             try:
586                 key_base = config.gconf_app_key_base() + "/thread_states"
587                 gconf_client = gconf.client_get_default()
588                 width = gconf_client.get_int(key_base + "/window_width")
589                 height = gconf_client.get_int(key_base + "/window_height")
590                 toolbar_visible = gconf_client.get_bool(
591                     key_base + "/toolbar")
592                 statusbar_visible = gconf_client.get_bool(
593                     key_base + "/statusbar")
594                 if width != 0:
595                     window_width = width
596                 if height != 0:
597                     window_height = height
598             except:
599                 traceback.print_exc()
600
601             states_path = misc.get_thread_states_path(self.bbs_type)
602             if os.path.exists(states_path):
603                 for line in file(states_path):
604                     if line.startswith("window_height="):
605                         height = window_height
606                         try:
607                             height = int(
608                                 line[len("window_height="):].rstrip("\n"))
609                         except:
610                             pass
611                         else:
612                             window_height = height
613                     elif line.startswith("window_width="):
614                         width = window_width
615                         try:
616                             width = int(
617                                 line[len("window_width="):].rstrip("\n"))
618                         except:
619                             pass
620                         else:
621                             window_width = width
622                     elif line.startswith("toolbar_visible="):
623                         tbar = line[len("toolbar_visible="):].rstrip("\n")
624                         toolbar_visible = tbar == "True"
625                     elif line.startswith("statusbar_visible="):
626                         sbar = line[len("statusbar_visible="):].rstrip("\n")
627                         statusbar_visible = sbar == "True"
628
629             self.window.set_default_size(window_width, window_height)
630
631             if not toolbar_visible:
632                 gobject.idle_add(self.toolbar.hide,
633                                  priority=gobject.PRIORITY_HIGH)
634             if not statusbar_visible:
635                 gobject.idle_add(self.statusbar.hide,
636                                  priority=gobject.PRIORITY_HIGH)
637         except:
638             traceback.print_exc()
639
640
641     # signal handlers
642     
643     def on_thread_view_uri_clicked(self, widget, uri):
644         self._open_uri(uri)
645
646     def on_thread_popup_uri_clicked(self, widget, threadview, uri):
647         self._open_uri(uri)
648
649     def on_thread_window_delete_event(self, widget, event):
650         self.save()
651         return False
652
653     def on_thread_window_destroy(self, widget):
654         self.destroyed()
655
656
657
658
659     # menu commands
660
661     # menu file
662
663     def on_menu_file_show_board_activate(self, widget):
664         self._show_board()
665
666     def on_menu_file_compose_activate(self, widget):
667         self._show_submit_window()
668
669     def on_menu_file_delete_activate(self, widget):
670         self._delete_log()
671
672     def on_menu_file_close_activate(self, widget):
673         self._close_window()
674
675     def on_menu_file_quit_activate(self, widget):
676         self._quit_session()
677
678     # menu view
679     
680     def on_menu_view_refresh_activate(self, widget):
681         self.update()
682
683     def on_menu_view_toolbar_activate(self, widget):
684         self._toggle_toolbar()
685
686     def on_menu_view_statusbar_activate(self, widget):
687         self._toggle_statusbar()
688
689     # menu bookmarks
690
691     def on_menu_bookmarks_bookmarkthispage_activate(self, widget):
692         self._regist_as_bookmark()
693
694     def on_menu_bookmarks_showbookmarks_activate(self, widget):
695         self._manage_bookmarks()
696
697     # toolbuttons
698     
699     def on_toolbutton_refresh_activate(self, widget):
700         self.update()
701
702     def on_toolbutton_showboard_activate(self, widget):
703         self._show_board()
704
705     def on_toolbutton_compose_activate(self, widget):
706         self._show_submit_window()
707
708     def on_toolbutton_delete_activate(self, widget):
709         self._delete_log()
710
711     # popup menus
712     
713     def on_popup_threadview_menu_openuri_activate(self, widget):
714         self._open_uri(widget.uri)
715
716     def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
717         self._copy_text_to_clipboard(widget.uri)
718
719     def on_popup_threadview_menu_copyselection_activate(self, widget):
720         text = self.threadview.get_selected_text()
721         self._copy_text_to_clipboard(text)
722
723     def on_popup_threadview_menu_openasuri_activate(self, widget):
724         text = self.threadview.get_selected_text()
725         uri = self._modify_uri(text)
726         self._open_uri(uri)
727
728     def on_popup_threadview_menu_refresh_activate(self, widget):
729         self.update()