OSDN Git Service

Statusbar, ThreadWindow
[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
54 GLADE_FILENAME = "thread_window.glade"
55
56 def open_thread(uri, update=False):
57     if not uri:
58         raise ValueError, "parameter must not be empty"
59
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
65
66     winwrap = session.get_window(uri)
67     if winwrap:
68         # already opened
69         winwrap.window.present()
70         if update:
71             winwrap.load(update)
72     else:
73         winwrap = WinWrap(bbs_type.uri)  # pass original uri
74         winwrap.load(update)
75
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)
82         if match:
83             resnum = int(match.group())
84             winwrap.jump_to_res(resnum)
85
86
87 class HTMLParserToThreadView:
88     def __init__(self, threadview, resnum, left_margin):
89         self.threadview = threadview
90         self.resnum = resnum
91         self.left_margin = left_margin
92         self.initialize()
93
94     def set_left_margin(self, left_margin):
95         self.left_margin = left_margin
96
97     def initialize(self):
98         self.layout = None
99
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)
104
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)
109
110         self.layout.add_text(data, bold, href)
111
112     def to_thread_view(self):
113         if self.layout is not None:
114             gobject.idle_add(self.threadview.add_layout, self.layout)
115             self.initialize()
116
117
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)
122
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
130         self.size = 0
131         self.num = 0
132         self.title = ""
133         self.lock_obj = False
134         self.jump_request_num = 0
135         self.progress = False
136
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")
144
145         self.threadview = thread_view.ThreadView()
146         self.vbox.pack_start(self.threadview)
147         self.vbox.reorder_child(self.threadview, 2)
148
149         self.threadview.on_uri_clicked = self.on_threadview_uri_clicked
150
151         self.statusbar_context_id = self.statusbar.get_context_id(
152             "Thread Window Status")
153         self.statusbar.push(self.statusbar_context_id, "OK.")
154
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")
169
170         self.initialize_buffer()
171
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)
198
199         self.restore()
200         self.window.show_all()
201
202         self.created()
203
204     def initialize_buffer(self):
205         self.textbuffer = gtk.TextBuffer()
206
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)
211
212         self.threadview.initialize_buffer()
213
214     def destroy(self):
215         self.save()
216         self.window.destroy()
217
218     def get_uri(self):
219         return self.bbs_type.get_thread_uri()
220
221     def on_compose_clicked(self, widget):
222         import submit_window
223         submit_window.open(self.bbs_type.get_thread_uri())
224
225     def on_toolbar_activate(self, widget):
226         if self.toolbar.get_property("visible"):
227             self.toolbar.hide()
228         else:
229             self.toolbar.show()
230
231     def on_statusbar_activate(self, widget):
232         if self.statusbar.get_property("visible"):
233             self.statusbar.hide()
234         else:
235             self.statusbar.show()
236
237     def on_close_activate(self, widget):
238         self.destroy()
239
240     def on_thread_window_delete_event(self, widget, event):
241         self.save()
242         return False
243         
244     def on_thread_window_destroy(self, widget):
245         self.destroyed()
246
247     def on_quit_activate(self, widget):
248         session.main_quit()
249
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)
253
254     def on_manage_bookmarks_activate(self, widget):
255         bookmark_window.open()
256
257     def on_show_board_activate(self, widget):
258         board_window.open_board(self.bbs_type.get_uri_base())
259
260     def on_delete_activate(self, widget):
261         try:
262             dat_path = misc.get_thread_dat_path(self.bbs_type)
263             os.remove(dat_path)
264         except OSError:
265             traceback.print_exc()
266         try:
267             idx_path = misc.get_thread_idx_path(self.bbs_type)
268             os.remove(idx_path)
269         except OSError:
270             traceback.print_exc()
271         try:
272             states_path = misc.get_thread_states_path(self.bbs_type)
273             os.remove(states_path)
274         except OSError:
275             traceback.print_exc()
276
277     def on_threadview_uri_clicked(self, uri):
278
279         if not uri.startswith("http://"):
280             # maybe a relative uri.
281             uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
282
283         try:
284             uri_opener.open_uri(uri)
285         except bbs_type_exception.BbsTypeError:
286             # not supported, show with the web browser.
287             gnome.url_show(uri)
288             
289     def on_popup_threadview_menu_openuri_activate(self, widget):
290         self.on_threadview_uri_clicked(widget.uri)
291
292     def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
293         clip = gtk.Clipboard()
294         clip.set_text(widget.uri, len(widget.uri))
295
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))
302
303     def on_popup_threadview_menu_openasuri_activate(self, widget):
304         self.on_threadview_uri_clicked(self.threadview.get_selected_text())
305
306     def on_popup_threadview_menu_refresh_activate(self, widget):
307         self.update(widget)
308
309     def http_get_dat(self, on_get_res):
310         datfile_url = self.bbs_type.get_dat_uri()
311
312         idx_dic = idxfile.load_idx(self.bbs_type)
313         lastmod = idx_dic["lastModified"]
314         etag = idx_dic["etag"]
315
316         req = urllib2.Request(datfile_url)
317         req.add_header("User-agent", config.User_Agent)
318         if self.size > 0:
319             req.add_header("Range", "bytes=" + str(self.size) + "-")
320         if lastmod:
321             req.add_header("If-Modified-Since", lastmod)
322         if etag:
323             req.add_header("If-None-Match", etag)
324
325         def push():
326             self.statusbar.pop(self.statusbar_context_id)
327             self.statusbar.push(self.statusbar_context_id, "GET...")
328         gobject.idle_add(push)
329
330         req = self.bbs_type.set_extra_dat_request(req, self)
331
332         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
333         try:
334             res = opener.open(req)
335         except urllib2.HTTPError, e:
336             def push(code, msg):
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)
341         else:
342             headers = res.info()
343
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)
351             else:
352                 def push(code, msg):
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)
357
358             maybe_incomplete = False
359             for line in res:
360                 if not line.endswith("\n"):
361                     maybe_incomplete = True
362                     print "does not end with \\n. maybe incomplete"
363                     break
364                 on_get_res(line)
365
366             res.close()
367
368             if maybe_incomplete:
369                 lastmod = None
370                 etag = None
371             else:
372                 if "Last-Modified" in headers:
373                     lastmod = headers["Last-Modified"]
374                 if "ETag" in headers:
375                     etag = headers["Etag"]
376
377             if self.num > 0:
378                 # save idx
379                 idx_dic = {"title": self.title, "lineCount": self.num,
380                        "lastModified": lastmod, "etag": etag}
381                 idxfile.save_idx(self.bbs_type, idx_dic)
382
383                 gobject.idle_add(session.thread_idx_updated,
384                                  self.bbs_type.get_thread_uri(), idx_dic)
385
386     def update(self, widget=None):
387
388         self.jump_request_num = 0
389
390         def load():
391             if self.num == 0:
392                 def create_mark():
393                     self.textbuffer.create_mark("1", self.enditer, True)
394                 gobject.idle_add(create_mark)
395
396             line_count = datfile.get_dat_line_count(self.bbs_type)
397             if line_count < self.num:
398                 self.num = 0
399                 self.size = 0
400
401                 gobject.idle_add(self.initialize_buffer)
402
403             if line_count > self.num:
404                 datfile.load_dat_partly(
405                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
406
407                 def do_jump():
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)
413                     else:
414                         self.jump_to_the_end()
415
416                 gobject.idle_add(do_jump)
417
418         def get():
419             dat_path = misc.get_thread_dat_path(self.bbs_type)
420             dat_file = FileWrap(dat_path)
421
422             def save_line_and_append_to_buffer(line):
423                 dat_file.seek(self.size)
424                 dat_file.write(line)
425                 self.append_rawres_to_buffer(line)
426
427             self.http_get_dat(save_line_and_append_to_buffer)
428             dat_file.close()
429
430             def do_jump():
431                 if self.jump_request_num:
432                     num = self.jump_request_num
433                     self.jump_request_num = 0
434                     self.jump_to_res(num)
435
436             gobject.idle_add(do_jump)
437
438         if self.lock():
439
440             def on_end():
441                 self.un_lock()
442                 self.progress = False
443
444             self.progress = True
445             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
446             t.start()
447
448     def load_dat(self):
449
450         self.size = 0
451         self.num = 0
452         self.jump_request_num = 0
453
454         def load():
455             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
456
457         def jump():
458
459             def do_jump():
460                 if self.jump_request_num:
461                     num = self.jump_request_num
462                     self.jump_request_num = 0
463                     self.jump_to_res(num)
464                 else:
465                     self.jump_to_the_end()
466
467             gobject.idle_add(do_jump)
468
469         if self.lock():
470
471             def on_end():
472                 self.un_lock()
473                 self.progress = False
474
475             self.progress = True
476             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
477             t.start()
478
479     def append_rawres_to_buffer(self, line):
480         self.size += len(line)
481         self.num += 1
482
483         if not self.title and self.num == 1:
484             title = self.bbs_type.get_title_from_dat(line)
485             if title:
486                 self.title = title
487                 gobject.idle_add(self.window.set_title, title)
488
489         line = line.decode(self.bbs_type.encoding, "replace")
490         m = self.bbs_type.dat_reg.match(line)
491         if m:
492             name = m.group("name")
493             mail = m.group("mail")
494             date = m.group("date")
495             msg = m.group("msg")
496             try:
497                 num = int(m.group("num"))
498             except IndexError:
499                 # use simple counter num
500                 num = self.num
501             else:
502                 # use num in dat
503                 self.num = num
504             try:
505                 id = m.group("id")
506             except IndexError:
507                 pass
508             else:
509                 if id:
510                     date += " ID:" + id
511             self.reselems_to_buffer(num, name, mail, date, msg)
512         else:
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
517
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)
522
523         # First, create a pango layout for num,name,mail,date
524         # 'margin left' is 0
525         # number
526         p.feed(str(num) + " ")
527
528         # name
529         p.feed("<b>" + name + "</b>")
530
531         # mail
532         p.feed("[" + mail + "]")
533
534         # date
535         p.feed(date)
536         p.flush()
537
538         pipe.to_thread_view()
539
540
541         # Second, create a pango layout for message
542         # 'margin left' is 20
543         # msg
544         pipe.set_left_margin(20)
545         p.feed(msg.lstrip(" "))
546
547         p.feed("<br>")
548         p.close()
549
550         pipe.to_thread_view()
551
552     def href_tag(self, href):
553         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
554         tag.set_data("href", href)
555         return tag
556
557     def jump(self, value):
558         gobject.idle_add(self.threadview.jump, value)
559
560     def jump_to_layout(self, layout):
561         gobject.idle_add(self.threadview.jump_to_layout, layout)
562         
563     def jump_to_the_end(self):
564         gobject.idle_add(self.threadview.jump_to_the_end)
565
566     def lock(self):
567         if self.lock_obj:
568             print "locked, try later."
569             return False
570         else:
571             print "get lock"
572             self.lock_obj = True
573             return True
574
575     def un_lock(self):
576         self.lock_obj = False
577         print "unlock"
578
579     def jump_to_res(self, resnum):
580         if self.threadview.jump_to_res(resnum):
581             return
582         self.jump_request_num = resnum
583
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:
588             self.update()
589         else:
590             self.load_dat()
591
592     def save(self):
593         try:
594             states_path = misc.get_thread_states_path(self.bbs_type)
595             dat_path = misc.get_thread_dat_path(self.bbs_type)
596
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")
602
603                 dirname = os.path.dirname(states_path)
604                 if not os.path.isdir(dirname):
605                     os.makedirs(dirname)
606
607                 f = file(states_path, "w")
608
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")
613
614                 f.close()
615         except:
616             traceback.print_exc()
617
618     def restore(self):
619         try:
620             window_height = 600
621             window_width = 600
622             toolbar_visible = True
623             statusbar_visible = True
624
625             try:
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")
634                 if width != 0:
635                     window_width = width
636                 if height != 0:
637                     window_height = height
638             except:
639                 traceback.print_exc()
640
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
646                         try:
647                             height = int(
648                                 line[len("window_height="):].rstrip("\n"))
649                         except:
650                             pass
651                         else:
652                             window_height = height
653                     elif line.startswith("window_width="):
654                         width = window_width
655                         try:
656                             width = int(
657                                 line[len("window_width="):].rstrip("\n"))
658                         except:
659                             pass
660                         else:
661                             window_width = width
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"
668
669             self.window.set_default_size(window_width, window_height)
670
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)
677         except:
678             traceback.print_exc()