OSDN Git Service

cd5caedb0017f21c6d4524587dfcae9baddd975b
[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):
89         self.threadview = threadview
90         self.resnum = resnum
91         self.initialize()
92
93     def initialize(self):
94         self.buf = ""
95         self.attrlist = pango.AttrList()
96         self.urilist = []
97
98     def from_html_parser(self, data, bold, href):
99         data = data.encode("utf8")
100         start = len(self.buf)
101         end = start + len(data)
102         self.buf += data
103         if bold:
104             attr = pango.AttrWeight(pango.WEIGHT_BOLD, start, end)
105             self.attrlist.insert(attr)
106         if href:
107             attr = pango.AttrUnderline(pango.UNDERLINE_SINGLE, start, end)
108             self.attrlist.insert(attr)
109             self.urilist.append((start, end, href))
110
111     def to_thread_view(self, marginleft):
112         layout = self.threadview.create_pango_layout(self.buf)
113         layout.posY = 0
114         layout.resnum = self.resnum
115         layout.marginleft = marginleft
116         layout.set_attributes(self.attrlist)
117         layout.urilist = self.urilist
118         gobject.idle_add(self.threadview.add_layout, layout)
119         self.initialize()
120
121
122 class WinWrap(winwrapbase.WinWrapBase):
123     hovering_over_link = False
124     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
125     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
126
127     def __init__(self, uri):
128         from BbsType import bbs_type_judge_uri
129         from BbsType import bbs_type_exception
130         self.bbs_type = bbs_type_judge_uri.get_type(uri)
131         if not self.bbs_type.is_thread():
132             raise bbs_type_exception.BbsTypeError, \
133                   "the uri does not represent thread: " + uri
134         self.size = 0
135         self.num = 0
136         self.title = ""
137         self.lock_obj = False
138         self.jump_request_num = 0
139         self.progress = False
140
141         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
142         self.widget_tree = gtk.glade.XML(glade_path)
143         self.window = self.widget_tree.get_widget("thread_window")
144         self.toolbar = self.widget_tree.get_widget("toolbar")
145         self.toolbar.unset_style()
146         self.statusbar = self.widget_tree.get_widget("statusbar")
147         self.vbox = self.widget_tree.get_widget("vbox")
148
149         self.threadview = thread_view.ThreadView()
150         self.vbox.pack_start(self.threadview)
151         self.vbox.reorder_child(self.threadview, 2)
152
153         self.threadview.on_uri_clicked = self.on_threadview_uri_clicked
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         pass
298
299     def on_popup_threadview_menu_openasuri_activate(self, widget):
300         pass
301
302     def on_popup_threadview_menu_refresh_activate(self, widget):
303         self.update(widget)
304
305     def http_get_dat(self, on_get_res):
306         datfile_url = self.bbs_type.get_dat_uri()
307
308         idx_dic = idxfile.load_idx(self.bbs_type)
309         lastmod = idx_dic["lastModified"]
310         etag = idx_dic["etag"]
311
312         req = urllib2.Request(datfile_url)
313         req.add_header("User-agent", config.User_Agent)
314         if self.size > 0:
315             req.add_header("Range", "bytes=" + str(self.size) + "-")
316         if lastmod:
317             req.add_header("If-Modified-Since", lastmod)
318         if etag:
319             req.add_header("If-None-Match", etag)
320
321         req = self.bbs_type.set_extra_dat_request(req, self)
322
323         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
324         try:
325             res = opener.open(req)
326         except urllib2.HTTPError, e:
327             pass
328 #             gobject.idle_add(
329 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
330         else:
331             headers = res.info()
332 #             gobject.idle_add(
333 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
334
335             maybe_incomplete = False
336             for line in res:
337                 if not line.endswith("\n"):
338                     maybe_incomplete = True
339                     print "does not end with \\n. maybe incomplete"
340                     break
341                 on_get_res(line)
342
343             res.close()
344
345             if maybe_incomplete:
346                 lastmod = None
347                 etag = None
348             else:
349                 if "Last-Modified" in headers:
350                     lastmod = headers["Last-Modified"]
351                 if "ETag" in headers:
352                     etag = headers["Etag"]
353
354             if self.num > 0:
355                 # save idx
356                 idx_dic = {"title": self.title, "lineCount": self.num,
357                        "lastModified": lastmod, "etag": etag}
358                 idxfile.save_idx(self.bbs_type, idx_dic)
359
360                 gobject.idle_add(session.thread_idx_updated,
361                                  self.bbs_type.get_thread_uri(), idx_dic)
362
363     def update(self, widget=None):
364
365         self.jump_request_num = 0
366
367         def load():
368             if self.num == 0:
369                 def create_mark():
370                     self.textbuffer.create_mark("1", self.enditer, True)
371                 gobject.idle_add(create_mark)
372
373             line_count = datfile.get_dat_line_count(self.bbs_type)
374             if line_count < self.num:
375                 self.num = 0
376                 self.size = 0
377
378                 gobject.idle_add(self.initialize_buffer)
379
380             if line_count > self.num:
381                 datfile.load_dat_partly(
382                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
383
384                 def do_jump():
385                     if self.jump_request_num:
386                         if self.jump_request_num <= num:
387                             num = self.jump_request_num
388                             self.jump_request_num = 0
389                             self.jump_to_res(num)
390                     else:
391                         self.jump_to_the_end()
392
393                 gobject.idle_add(do_jump)
394
395         def get():
396             dat_path = misc.get_thread_dat_path(self.bbs_type)
397             dat_file = FileWrap(dat_path)
398
399             def save_line_and_append_to_buffer(line):
400                 dat_file.seek(self.size)
401                 dat_file.write(line)
402                 self.append_rawres_to_buffer(line)
403
404             self.http_get_dat(save_line_and_append_to_buffer)
405             dat_file.close()
406
407             def do_jump():
408                 if self.jump_request_num:
409                     num = self.jump_request_num
410                     self.jump_request_num = 0
411                     self.jump_to_res(num)
412
413             gobject.idle_add(do_jump)
414
415         if self.lock():
416
417             def on_end():
418                 self.un_lock()
419                 self.progress = False
420
421             self.progress = True
422             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
423             t.start()
424
425     def load_dat(self):
426
427         self.size = 0
428         self.num = 0
429         self.jump_request_num = 0
430
431         def load():
432             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
433
434         def jump():
435
436             def do_jump():
437                 if self.jump_request_num:
438                     num = self.jump_request_num
439                     self.jump_request_num = 0
440                     self.jump_to_res(num)
441                 else:
442                     self.jump_to_the_end()
443
444             gobject.idle_add(do_jump)
445
446         if self.lock():
447
448             def on_end():
449                 self.un_lock()
450                 self.progress = False
451
452             self.progress = True
453             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
454             t.start()
455
456     def append_rawres_to_buffer(self, line):
457         self.size += len(line)
458         self.num += 1
459
460         if not self.title and self.num == 1:
461             title = self.bbs_type.get_title_from_dat(line)
462             if title:
463                 self.title = title
464                 gobject.idle_add(self.window.set_title, title)
465
466         line = line.decode(self.bbs_type.encoding, "replace")
467         m = self.bbs_type.dat_reg.match(line)
468         if m:
469             name = m.group("name")
470             mail = m.group("mail")
471             date = m.group("date")
472             msg = m.group("msg")
473             try:
474                 num = int(m.group("num"))
475             except IndexError:
476                 # use simple counter num
477                 num = self.num
478             else:
479                 # use num in dat
480                 self.num = num
481             try:
482                 id = m.group("id")
483             except IndexError:
484                 pass
485             else:
486                 if id:
487                     date += " ID:" + id
488             self.reselems_to_buffer(num, name, mail, date, msg)
489         else:
490             self.reselems_to_buffer(
491                 str(self.num), "Invalid Name", "Invalid Mail",
492                 "Invalid Date", line)
493             print "maybe syntax error.", self.num, line
494
495     def reselems_to_buffer(self, num, name, mail, date, msg):
496         pipe = HTMLParserToThreadView(self.threadview, num)
497         p = barehtmlparser.BareHTMLParser(pipe.from_html_parser)
498
499         # First, create a pango layout for num,name,mail,date
500         # 'margin left' is 0
501         # number
502         p.feed(str(num) + " ")
503
504         # name
505         p.feed("<b>" + name + "</b>")
506
507         # mail
508         p.feed("[" + mail + "]")
509
510         # date
511         p.feed(date)
512         p.flush()
513
514         pipe.to_thread_view(0)
515
516
517         # Second, create a pango layout for message
518         # 'margin left' is 20
519         # msg
520         p.feed(msg.lstrip(" "))
521
522         p.feed("<br>")
523         p.close()
524
525         pipe.to_thread_view(20)
526
527     def href_tag(self, href):
528         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
529         tag.set_data("href", href)
530         return tag
531
532     def jump(self, value):
533         gobject.idle_add(self.threadview.jump, value)
534
535     def jump_to_layout(self, layout):
536         gobject.idle_add(self.threadview.jump_to_layout, layout)
537         
538     def jump_to_the_end(self):
539         gobject.idle_add(self.threadview.jump_to_the_end)
540
541     def lock(self):
542         if self.lock_obj:
543             print "locked, try later."
544             return False
545         else:
546             print "get lock"
547             self.lock_obj = True
548             return True
549
550     def un_lock(self):
551         self.lock_obj = False
552         print "unlock"
553
554     def jump_to_res(self, resnum):
555         if self.threadview.jump_to_res(resnum):
556             return
557         self.jump_request_num = resnum
558
559     def load(self, update=False):
560         dat_path = misc.get_thread_dat_path(self.bbs_type)
561         dat_exists = os.path.exists(dat_path)
562         if update or not dat_exists:
563             self.update()
564         else:
565             self.load_dat()
566
567     def save(self):
568         try:
569             states_path = misc.get_thread_states_path(self.bbs_type)
570             dat_path = misc.get_thread_dat_path(self.bbs_type)
571
572             # save only if dat file exists.
573             if os.path.exists(dat_path):
574                 window_width, window_height = self.window.get_size()
575                 toolbar_visible = self.toolbar.get_property("visible")
576                 statusbar_visible = self.statusbar.get_property("visible")
577
578                 dirname = os.path.dirname(states_path)
579                 if not os.path.isdir(dirname):
580                     os.makedirs(dirname)
581
582                 f = file(states_path, "w")
583
584                 f.write("window_width=" + str(window_width) + "\n")
585                 f.write("window_height=" + str(window_height) + "\n")
586                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
587                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
588
589                 f.close()
590         except:
591             traceback.print_exc()
592
593     def restore(self):
594         try:
595             window_height = 600
596             window_width = 600
597             toolbar_visible = True
598             statusbar_visible = True
599
600             try:
601                 key_base = config.gconf_app_key_base() + "/thread_states"
602                 gconf_client = gconf.client_get_default()
603                 width = gconf_client.get_int(key_base + "/window_width")
604                 height = gconf_client.get_int(key_base + "/window_height")
605                 toolbar_visible = gconf_client.get_bool(
606                     key_base + "/toolbar")
607                 statusbar_visible = gconf_client.get_bool(
608                     key_base + "/statusbar")
609                 if width != 0:
610                     window_width = width
611                 if height != 0:
612                     window_height = height
613             except:
614                 traceback.print_exc()
615
616             states_path = misc.get_thread_states_path(self.bbs_type)
617             if os.path.exists(states_path):
618                 for line in file(states_path):
619                     if line.startswith("window_height="):
620                         height = window_height
621                         try:
622                             height = int(
623                                 line[len("window_height="):].rstrip("\n"))
624                         except:
625                             pass
626                         else:
627                             window_height = height
628                     elif line.startswith("window_width="):
629                         width = window_width
630                         try:
631                             width = int(
632                                 line[len("window_width="):].rstrip("\n"))
633                         except:
634                             pass
635                         else:
636                             window_width = width
637                     elif line.startswith("toolbar_visible="):
638                         tbar = line[len("toolbar_visible="):].rstrip("\n")
639                         toolbar_visible = tbar == "True"
640                     elif line.startswith("statusbar_visible="):
641                         sbar = line[len("statusbar_visible="):].rstrip("\n")
642                         statusbar_visible = sbar == "True"
643
644             self.window.set_default_size(window_width, window_height)
645
646             if not toolbar_visible:
647                 gobject.idle_add(self.toolbar.hide,
648                                  priority=gobject.PRIORITY_HIGH)
649             if not statusbar_visible:
650                 gobject.idle_add(self.statusbar.hide,
651                                  priority=gobject.PRIORITY_HIGH)
652         except:
653             traceback.print_exc()