OSDN Git Service

e847dff50ac7e837b25925a252c652d9954b6908
[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
53 GLADE_FILENAME = "thread_window.glade"
54
55 def open_thread(uri, update=False):
56     if not uri:
57         raise ValueError, "parameter must not be empty"
58
59     bbs_type = bbs_type_judge_uri.get_type(uri)
60     if not bbs_type.is_thread():
61         raise bbs_type_exception.BbsTypeError, \
62               "the uri does not represent thread: " + uri
63     uri = bbs_type.get_thread_uri()  # use strict thread uri
64
65     winwrap = session.get_window(uri)
66     if winwrap:
67         # already opened
68         winwrap.window.present()
69         if update:
70             winwrap.load(update)
71     else:
72         winwrap = WinWrap(bbs_type.uri)  # pass original uri
73         winwrap.load(update)
74
75     # jump to the res if necessary.
76     strict_uri = winwrap.bbs_type.get_thread_uri()
77     if (winwrap.bbs_type.uri != strict_uri and
78         winwrap.bbs_type.uri.startswith(strict_uri)):
79         resnum = winwrap.bbs_type.uri[len(strict_uri):]
80         match = re.match("\d+", resnum)
81         if match:
82             resnum = int(match.group())
83             winwrap.jump_to_res(resnum)
84
85 class WinWrap(winwrapbase.WinWrapBase):
86     hovering_over_link = False
87     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
88     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
89
90
91     def relayout(self):
92         width = self.drawingarea.allocation.width
93         sum_height = 0
94         for layout in self.pangolayout:
95             layout.set_width(width * pango.SCALE)
96             layout.posY = sum_height
97             x, y = layout.get_pixel_size()
98             sum_height += y
99         self.drawingarea.set_size_request(-1, sum_height)
100
101     def draw_viewport(self, area):
102         gc = self.drawingarea.window.new_gc()
103         self.drawingarea.window.draw_rectangle(
104             self.drawingarea.style.base_gc[0],
105             True, area.x, area.y, area.width, area.height)
106
107         for layout in self.pangolayout:
108             w, h = layout.get_pixel_size()
109             if ((layout.posY >= area.y and
110                 layout.posY < area.y + area.height) or
111                 (layout.posY + h >= area.y and
112                  layout.posY + h < area.y + area.height) or
113                 (layout.posY <= area.y and layout.posY + h >= area.y)):
114                 self.drawingarea.window.draw_layout(gc, 0, layout.posY, layout)
115
116     def on_drawingarea_expose_event(self, widget, event, data=None):
117         self.draw_viewport(event.area)
118         return True
119
120     def on_drawingarea_size_allocate(self, widget, allocation, data=None):
121         if allocation.width != self.drawingarea.prev_width:
122             self.relayout()
123             self.drawingarea.prev_width = allocation.width
124         return False
125
126     def __init__(self, uri):
127         self.pangolayout = []
128
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.window = self.widget_tree.get_widget("thread_window")
145         self.toolbar = self.widget_tree.get_widget("toolbar")
146         self.toolbar.unset_style()
147         self.statusbar = self.widget_tree.get_widget("statusbar")
148         self.drawingarea = self.widget_tree.get_widget("drawingarea")
149         self.viewport = self.drawingarea.parent
150         self.adjustment = self.viewport.get_vadjustment()
151
152         self.drawingarea.prev_width = 0
153
154         self.initialize_buffer()
155
156         sigdic = {"on_refresh_activate": self.update,
157                   "on_compose_activate": self.on_compose_clicked,
158                   "on_toolbar_activate": self.on_toolbar_activate,
159                   "on_statusbar_activate": self.on_statusbar_activate,
160                   "on_refresh_activate": self.update,
161                   "on_close_activate": self.on_close_activate,
162                   "on_quit_activate": self.on_quit_activate,
163                   "on_show_board_activate": self.on_show_board_activate,
164                   "on_delete_activate": self.on_delete_activate,
165                   "on_drawingarea_expose_event": self.on_drawingarea_expose_event,
166                   "on_drawingarea_size_allocate":
167                   self.on_drawingarea_size_allocate,
168                   "on_thread_window_delete_event":
169                   self.on_thread_window_delete_event,
170                   "on_add_bookmark_activate": self.on_add_bookmark_activate,
171                   "on_manage_bookmarks_activate": \
172                   self.on_manage_bookmarks_activate,
173                   "on_thread_window_destroy": self.on_thread_window_destroy}
174         self.widget_tree.signal_autoconnect(sigdic)
175
176         self.restore()
177         self.window.show_all()
178
179         self.created()
180
181     def initialize_buffer(self):
182         self.textbuffer = gtk.TextBuffer()
183
184         self.enditer = self.textbuffer.get_end_iter()
185         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
186         self.leftmargintag = self.textbuffer.create_tag()
187         self.leftmargintag.set_property("left-margin", 20)
188
189         self.pangolayout = []
190
191     def destroy(self):
192         self.save()
193         self.window.destroy()
194
195     def get_uri(self):
196         return self.bbs_type.get_thread_uri()
197
198     def on_compose_clicked(self, widget):
199         import submit_window
200         submit_window.open(self.bbs_type.get_thread_uri())
201
202     def on_toolbar_activate(self, widget):
203         if self.toolbar.get_property("visible"):
204             self.toolbar.hide()
205         else:
206             self.toolbar.show()
207
208     def on_statusbar_activate(self, widget):
209         if self.statusbar.get_property("visible"):
210             self.statusbar.hide()
211         else:
212             self.statusbar.show()
213
214     def on_close_activate(self, widget):
215         self.destroy()
216
217     def on_thread_window_delete_event(self, widget, event):
218         self.save()
219         return False
220         
221     def on_thread_window_destroy(self, widget):
222         self.destroyed()
223
224     def on_quit_activate(self, widget):
225         session.main_quit()
226
227     def on_add_bookmark_activate(self, widget):
228         bookmark_list.bookmark_list.add_bookmark_with_edit(
229             name=self.title, uri=self.bbs_type.uri)
230
231     def on_manage_bookmarks_activate(self, widget):
232         bookmark_window.open()
233
234     def on_show_board_activate(self, widget):
235         board_window.open_board(self.bbs_type.get_uri_base())
236
237     def on_delete_activate(self, widget):
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 http_get_dat(self, on_get_res):
255         datfile_url = self.bbs_type.get_dat_uri()
256
257         idx_dic = idxfile.load_idx(self.bbs_type)
258         lastmod = idx_dic["lastModified"]
259         etag = idx_dic["etag"]
260
261         req = urllib2.Request(datfile_url)
262         req.add_header("User-agent", config.User_Agent)
263         if self.size > 0:
264             req.add_header("Range", "bytes=" + str(self.size) + "-")
265         if lastmod:
266             req.add_header("If-Modified-Since", lastmod)
267         if etag:
268             req.add_header("If-None-Match", etag)
269
270         req = self.bbs_type.set_extra_dat_request(req, self)
271
272         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
273         try:
274             res = opener.open(req)
275         except urllib2.HTTPError, e:
276             pass
277 #             gobject.idle_add(
278 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
279         else:
280             headers = res.info()
281 #             gobject.idle_add(
282 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
283
284             maybe_incomplete = False
285             for line in res:
286                 if not line.endswith("\n"):
287                     maybe_incomplete = True
288                     print "does not end with \\n. maybe incomplete"
289                     break
290                 on_get_res(line)
291
292             res.close()
293
294             if maybe_incomplete:
295                 lastmod = None
296                 etag = None
297             else:
298                 if "Last-Modified" in headers:
299                     lastmod = headers["Last-Modified"]
300                 if "ETag" in headers:
301                     etag = headers["Etag"]
302
303             if self.num > 0:
304                 # save idx
305                 idx_dic = {"title": self.title, "lineCount": self.num,
306                        "lastModified": lastmod, "etag": etag}
307                 idxfile.save_idx(self.bbs_type, idx_dic)
308
309                 gobject.idle_add(session.thread_idx_updated,
310                                  self.bbs_type.get_thread_uri(), idx_dic)
311
312     def update(self, widget=None):
313
314         self.jump_request_num = 0
315
316         def load():
317             if self.num == 0:
318                 def create_mark():
319                     self.textbuffer.create_mark("1", self.enditer, True)
320                 gobject.idle_add(create_mark)
321
322             line_count = datfile.get_dat_line_count(self.bbs_type)
323             if line_count < self.num:
324                 self.num = 0
325                 self.size = 0
326
327                 gobject.idle_add(self.initialize_buffer)
328
329             if line_count > self.num:
330                 datfile.load_dat_partly(
331                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
332
333                 def do_jump():
334                     if self.jump_request_num:
335                         if self.jump_request_num <= num:
336                             num = self.jump_request_num
337                             self.jump_request_num = 0
338                             self.jump_to_res(num)
339                     else:
340                         self.jump_to_the_end()
341
342                 gobject.idle_add(do_jump)
343
344         def get():
345             dat_path = misc.get_thread_dat_path(self.bbs_type)
346             dat_file = FileWrap(dat_path)
347
348             def save_line_and_append_to_buffer(line):
349                 dat_file.seek(self.size)
350                 dat_file.write(line)
351                 self.append_rawres_to_buffer(line)
352
353             self.http_get_dat(save_line_and_append_to_buffer)
354             dat_file.close()
355
356             def do_jump():
357                 if self.jump_request_num:
358                     num = self.jump_request_num
359                     self.jump_request_num = 0
360                     self.jump_to_res(num)
361
362             gobject.idle_add(do_jump)
363
364         if self.lock():
365
366             def on_end():
367                 self.un_lock()
368                 self.progress = False
369
370             self.progress = True
371             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
372             t.start()
373
374     def load_dat(self):
375
376         self.size = 0
377         self.num = 0
378         self.jump_request_num = 0
379
380         def load():
381             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
382
383         def jump():
384
385             def do_jump():
386                 if self.jump_request_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         if self.lock():
396
397             def on_end():
398                 self.un_lock()
399                 self.progress = False
400
401             self.progress = True
402             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
403             t.start()
404
405     def append_rawres_to_buffer(self, line):
406         self.size += len(line)
407         self.num += 1
408
409         if not self.title and self.num == 1:
410             title = self.bbs_type.get_title_from_dat(line)
411             if title:
412                 self.title = title
413                 gobject.idle_add(self.window.set_title, title)
414
415         self.res_queue = []
416
417         line = line.decode(self.bbs_type.encoding, "replace")
418         m = self.bbs_type.dat_reg.match(line)
419         if m:
420             name = m.group("name")
421             mail = m.group("mail")
422             date = m.group("date")
423             msg = m.group("msg")
424             try:
425                 num = int(m.group("num"))
426             except IndexError:
427                 # use simple counter num
428                 num = self.num
429             else:
430                 # use num in dat
431                 self.num = num
432             try:
433                 id = m.group("id")
434             except IndexError:
435                 pass
436             else:
437                 if id:
438                     date += " ID:" + id
439             self.reselems_to_buffer(num, name, mail, date, msg)
440         else:
441             self.res_queue.append((str(self.num)+"\n", False, None, False))
442             self.res_queue.append((line, False, None, True))
443             print "maybe syntax error.", self.num, line
444
445         def process_res_queue(res_queue, num):
446             self.process_queue(res_queue, num)
447             # for next res
448             #self.textbuffer.create_mark(str(num+1), self.enditer, True)
449
450         gobject.idle_add(
451             process_res_queue, self.res_queue, self.num)
452
453     def reselems_to_buffer(self, num, name, mail, date, msg):
454         p = barehtmlparser.BareHTMLParser(
455             lambda d,b,h: self.res_queue.append((d,b,h,False)))
456         # number
457         p.feed(str(num) + " ")
458
459         # name
460         p.feed("<b>" + name + "</b>")
461
462         # mail
463         p.feed("[" + mail + "]")
464
465         # date
466         p.feed(date)
467         p.feed("<br>")
468
469         # msg
470         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
471         p.feed(msg.lstrip(" "))
472
473         p.feed("<br><br>")
474         p.close()
475
476     def href_tag(self, href):
477         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
478         tag.set_data("href", href)
479         return tag
480
481     def process_queue(self, queue, num):
482         text = ""
483         for data, bold, href, margin in queue:
484             text += data
485         layout = self.drawingarea.create_pango_layout(text)
486         layout.set_wrap(pango.WRAP_CHAR)
487         layout.posY = 0
488         layout.resnum = num
489         self.pangolayout.append(layout)
490         self.relayout()
491 #             taglist = []
492 #             if bold:
493 #                 taglist.append(self.boldtag)
494 #             if href:
495 #                 taglist.append(self.href_tag(href))
496 #             if margin:
497 #                 taglist.append(self.leftmargintag)
498 #
499 #            if taglist:
500 #                self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
501 #            else:
502 #                self.textbuffer.insert(self.enditer, data)
503
504     def jump(self, value):
505         def j():
506             if value > self.adjustment.upper - self.viewport.allocation.height:
507                 self.jump_to_the_end()
508             else:
509                 self.adjustment.set_value(value)
510         gobject.idle_add(j)
511
512     def jump_to_layout(self, layout):
513         gobject.idle_add(lambda : self.jump(layout.posY))
514         
515     def jump_to_the_end(self):
516         def j():
517             self.adjustment.set_value(
518                 self.adjustment.upper - self.viewport.allocation.height)
519         gobject.idle_add(j)
520 #        mark = self.textbuffer.get_mark(str(num+1))
521 #        if mark:
522 #            self.textview.scroll_to_mark(mark, 0)
523
524     def lock(self):
525         if self.lock_obj:
526             print "locked, try later."
527             return False
528         else:
529             print "get lock"
530             self.lock_obj = True
531             return True
532
533     def un_lock(self):
534         self.lock_obj = False
535         print "unlock"
536
537     def jump_to_res(self, resnum):
538         for layout in self.pangolayout:
539             if layout.resnum == resnum:
540                 self.jump_to_layout(layout)
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()