OSDN Git Service

Replace PangoLayout with ResLayout.
[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.threadview.popupmenu = self.widget_tree.get_widget(
152             "popup_threadview_menu")
153         self.threadview.menu_openuri = self.widget_tree.get_widget(
154             "popup_threadview_menu_openuri")
155         self.threadview.menu_copylinkaddress = self.widget_tree.get_widget(
156             "popup_threadview_menu_copylinkaddress")
157         self.threadview.menu_separator_link = self.widget_tree.get_widget(
158             "popup_threadview_menu_separator_link")
159         self.threadview.menu_copyselection = self.widget_tree.get_widget(
160             "popup_threadview_menu_copyselection")
161         self.threadview.menu_openasuri = self.widget_tree.get_widget(
162             "popup_threadview_menu_openasuri")
163         self.threadview.menu_separator_selection = self.widget_tree.get_widget(
164             "popup_threadview_menu_separator_selection")
165
166         self.initialize_buffer()
167
168         sigdic = {"on_refresh_activate": self.update,
169                   "on_compose_activate": self.on_compose_clicked,
170                   "on_toolbar_activate": self.on_toolbar_activate,
171                   "on_statusbar_activate": self.on_statusbar_activate,
172                   "on_refresh_activate": self.update,
173                   "on_close_activate": self.on_close_activate,
174                   "on_quit_activate": self.on_quit_activate,
175                   "on_show_board_activate": self.on_show_board_activate,
176                   "on_delete_activate": self.on_delete_activate,
177                   "on_thread_window_delete_event":
178                   self.on_thread_window_delete_event,
179                   "on_add_bookmark_activate": self.on_add_bookmark_activate,
180                   "on_manage_bookmarks_activate": \
181                   self.on_manage_bookmarks_activate,
182                   "on_popup_threadview_menu_openuri_activate":
183                   self.on_popup_threadview_menu_openuri_activate,
184                   "on_popup_threadview_menu_copylinkaddress_activate":
185                   self.on_popup_threadview_menu_copylinkaddress_activate,
186                   "on_popup_threadview_menu_copyselection_activate":
187                   self.on_popup_threadview_menu_copyselection_activate,
188                   "on_popup_threadview_menu_openasuri_activate":
189                   self.on_popup_threadview_menu_openasuri_activate,
190                   "on_popup_threadview_menu_refresh_activate":
191                   self.on_popup_threadview_menu_refresh_activate,
192                   "on_thread_window_destroy": self.on_thread_window_destroy}
193         self.widget_tree.signal_autoconnect(sigdic)
194
195         self.restore()
196         self.window.show_all()
197
198         self.created()
199
200     def initialize_buffer(self):
201         self.textbuffer = gtk.TextBuffer()
202
203         self.enditer = self.textbuffer.get_end_iter()
204         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
205         self.leftmargintag = self.textbuffer.create_tag()
206         self.leftmargintag.set_property("left-margin", 20)
207
208         self.threadview.initialize_buffer()
209
210     def destroy(self):
211         self.save()
212         self.window.destroy()
213
214     def get_uri(self):
215         return self.bbs_type.get_thread_uri()
216
217     def on_compose_clicked(self, widget):
218         import submit_window
219         submit_window.open(self.bbs_type.get_thread_uri())
220
221     def on_toolbar_activate(self, widget):
222         if self.toolbar.get_property("visible"):
223             self.toolbar.hide()
224         else:
225             self.toolbar.show()
226
227     def on_statusbar_activate(self, widget):
228         if self.statusbar.get_property("visible"):
229             self.statusbar.hide()
230         else:
231             self.statusbar.show()
232
233     def on_close_activate(self, widget):
234         self.destroy()
235
236     def on_thread_window_delete_event(self, widget, event):
237         self.save()
238         return False
239         
240     def on_thread_window_destroy(self, widget):
241         self.destroyed()
242
243     def on_quit_activate(self, widget):
244         session.main_quit()
245
246     def on_add_bookmark_activate(self, widget):
247         bookmark_list.bookmark_list.add_bookmark_with_edit(
248             name=self.title, uri=self.bbs_type.uri)
249
250     def on_manage_bookmarks_activate(self, widget):
251         bookmark_window.open()
252
253     def on_show_board_activate(self, widget):
254         board_window.open_board(self.bbs_type.get_uri_base())
255
256     def on_delete_activate(self, widget):
257         try:
258             dat_path = misc.get_thread_dat_path(self.bbs_type)
259             os.remove(dat_path)
260         except OSError:
261             traceback.print_exc()
262         try:
263             idx_path = misc.get_thread_idx_path(self.bbs_type)
264             os.remove(idx_path)
265         except OSError:
266             traceback.print_exc()
267         try:
268             states_path = misc.get_thread_states_path(self.bbs_type)
269             os.remove(states_path)
270         except OSError:
271             traceback.print_exc()
272
273     def on_threadview_uri_clicked(self, uri):
274
275         if not uri.startswith("http://"):
276             # maybe a relative uri.
277             uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
278
279         try:
280             uri_opener.open_uri(uri)
281         except bbs_type_exception.BbsTypeError:
282             # not supported, show with the web browser.
283             gnome.url_show(uri)
284             
285     def on_popup_threadview_menu_openuri_activate(self, widget):
286         self.on_threadview_uri_clicked(widget.uri)
287
288     def on_popup_threadview_menu_copylinkaddress_activate(self, widget):
289         clip = gtk.Clipboard()
290         clip.set_text(widget.uri, len(widget.uri))
291
292     def on_popup_threadview_menu_copyselection_activate(self, widget):
293         pass
294
295     def on_popup_threadview_menu_openasuri_activate(self, widget):
296         pass
297
298     def on_popup_threadview_menu_refresh_activate(self, widget):
299         self.update(widget)
300
301     def http_get_dat(self, on_get_res):
302         datfile_url = self.bbs_type.get_dat_uri()
303
304         idx_dic = idxfile.load_idx(self.bbs_type)
305         lastmod = idx_dic["lastModified"]
306         etag = idx_dic["etag"]
307
308         req = urllib2.Request(datfile_url)
309         req.add_header("User-agent", config.User_Agent)
310         if self.size > 0:
311             req.add_header("Range", "bytes=" + str(self.size) + "-")
312         if lastmod:
313             req.add_header("If-Modified-Since", lastmod)
314         if etag:
315             req.add_header("If-None-Match", etag)
316
317         req = self.bbs_type.set_extra_dat_request(req, self)
318
319         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
320         try:
321             res = opener.open(req)
322         except urllib2.HTTPError, e:
323             pass
324 #             gobject.idle_add(
325 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
326         else:
327             headers = res.info()
328 #             gobject.idle_add(
329 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
330
331             maybe_incomplete = False
332             for line in res:
333                 if not line.endswith("\n"):
334                     maybe_incomplete = True
335                     print "does not end with \\n. maybe incomplete"
336                     break
337                 on_get_res(line)
338
339             res.close()
340
341             if maybe_incomplete:
342                 lastmod = None
343                 etag = None
344             else:
345                 if "Last-Modified" in headers:
346                     lastmod = headers["Last-Modified"]
347                 if "ETag" in headers:
348                     etag = headers["Etag"]
349
350             if self.num > 0:
351                 # save idx
352                 idx_dic = {"title": self.title, "lineCount": self.num,
353                        "lastModified": lastmod, "etag": etag}
354                 idxfile.save_idx(self.bbs_type, idx_dic)
355
356                 gobject.idle_add(session.thread_idx_updated,
357                                  self.bbs_type.get_thread_uri(), idx_dic)
358
359     def update(self, widget=None):
360
361         self.jump_request_num = 0
362
363         def load():
364             if self.num == 0:
365                 def create_mark():
366                     self.textbuffer.create_mark("1", self.enditer, True)
367                 gobject.idle_add(create_mark)
368
369             line_count = datfile.get_dat_line_count(self.bbs_type)
370             if line_count < self.num:
371                 self.num = 0
372                 self.size = 0
373
374                 gobject.idle_add(self.initialize_buffer)
375
376             if line_count > self.num:
377                 datfile.load_dat_partly(
378                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
379
380                 def do_jump():
381                     if self.jump_request_num:
382                         if self.jump_request_num <= num:
383                             num = self.jump_request_num
384                             self.jump_request_num = 0
385                             self.jump_to_res(num)
386                     else:
387                         self.jump_to_the_end()
388
389                 gobject.idle_add(do_jump)
390
391         def get():
392             dat_path = misc.get_thread_dat_path(self.bbs_type)
393             dat_file = FileWrap(dat_path)
394
395             def save_line_and_append_to_buffer(line):
396                 dat_file.seek(self.size)
397                 dat_file.write(line)
398                 self.append_rawres_to_buffer(line)
399
400             self.http_get_dat(save_line_and_append_to_buffer)
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 href_tag(self, href):
526         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
527         tag.set_data("href", href)
528         return tag
529
530     def jump(self, value):
531         gobject.idle_add(self.threadview.jump, value)
532
533     def jump_to_layout(self, layout):
534         gobject.idle_add(self.threadview.jump_to_layout, layout)
535         
536     def jump_to_the_end(self):
537         gobject.idle_add(self.threadview.jump_to_the_end)
538
539     def lock(self):
540         if self.lock_obj:
541             print "locked, try later."
542             return False
543         else:
544             print "get lock"
545             self.lock_obj = True
546             return True
547
548     def un_lock(self):
549         self.lock_obj = False
550         print "unlock"
551
552     def jump_to_res(self, resnum):
553         if self.threadview.jump_to_res(resnum):
554             return
555         self.jump_request_num = resnum
556
557     def load(self, update=False):
558         dat_path = misc.get_thread_dat_path(self.bbs_type)
559         dat_exists = os.path.exists(dat_path)
560         if update or not dat_exists:
561             self.update()
562         else:
563             self.load_dat()
564
565     def save(self):
566         try:
567             states_path = misc.get_thread_states_path(self.bbs_type)
568             dat_path = misc.get_thread_dat_path(self.bbs_type)
569
570             # save only if dat file exists.
571             if os.path.exists(dat_path):
572                 window_width, window_height = self.window.get_size()
573                 toolbar_visible = self.toolbar.get_property("visible")
574                 statusbar_visible = self.statusbar.get_property("visible")
575
576                 dirname = os.path.dirname(states_path)
577                 if not os.path.isdir(dirname):
578                     os.makedirs(dirname)
579
580                 f = file(states_path, "w")
581
582                 f.write("window_width=" + str(window_width) + "\n")
583                 f.write("window_height=" + str(window_height) + "\n")
584                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
585                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
586
587                 f.close()
588         except:
589             traceback.print_exc()
590
591     def restore(self):
592         try:
593             window_height = 600
594             window_width = 600
595             toolbar_visible = True
596             statusbar_visible = True
597
598             try:
599                 key_base = config.gconf_app_key_base() + "/thread_states"
600                 gconf_client = gconf.client_get_default()
601                 width = gconf_client.get_int(key_base + "/window_width")
602                 height = gconf_client.get_int(key_base + "/window_height")
603                 toolbar_visible = gconf_client.get_bool(
604                     key_base + "/toolbar")
605                 statusbar_visible = gconf_client.get_bool(
606                     key_base + "/statusbar")
607                 if width != 0:
608                     window_width = width
609                 if height != 0:
610                     window_height = height
611             except:
612                 traceback.print_exc()
613
614             states_path = misc.get_thread_states_path(self.bbs_type)
615             if os.path.exists(states_path):
616                 for line in file(states_path):
617                     if line.startswith("window_height="):
618                         height = window_height
619                         try:
620                             height = int(
621                                 line[len("window_height="):].rstrip("\n"))
622                         except:
623                             pass
624                         else:
625                             window_height = height
626                     elif line.startswith("window_width="):
627                         width = window_width
628                         try:
629                             width = int(
630                                 line[len("window_width="):].rstrip("\n"))
631                         except:
632                             pass
633                         else:
634                             window_width = width
635                     elif line.startswith("toolbar_visible="):
636                         tbar = line[len("toolbar_visible="):].rstrip("\n")
637                         toolbar_visible = tbar == "True"
638                     elif line.startswith("statusbar_visible="):
639                         sbar = line[len("statusbar_visible="):].rstrip("\n")
640                         statusbar_visible = sbar == "True"
641
642             self.window.set_default_size(window_width, window_height)
643
644             if not toolbar_visible:
645                 gobject.idle_add(self.toolbar.hide,
646                                  priority=gobject.PRIORITY_HIGH)
647             if not statusbar_visible:
648                 gobject.idle_add(self.statusbar.hide,
649                                  priority=gobject.PRIORITY_HIGH)
650         except:
651             traceback.print_exc()