OSDN Git Service

6ffdc0dddae605f52cb0c018acdd6fbccb9e24cf
[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     winwrap.jump_to_res(bbs_type.uri)
77
78
79 class WinWrap(winwrapbase.WinWrapBase):
80     hovering_over_link = False
81     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
82     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
83
84
85     def relayout(self):
86         width = self.drawingarea.allocation.width
87         sum_height = 0
88         for layout in self.pangolayout:
89             layout.set_width(width * pango.SCALE)
90             layout.posY = sum_height
91             x, y = layout.get_pixel_size()
92             sum_height += y
93         self.drawingarea.set_size_request(-1, sum_height)
94
95     def draw_viewport(self, area):
96         gc = self.drawingarea.window.new_gc()
97         self.drawingarea.window.draw_rectangle(
98             self.drawingarea.style.base_gc[0],
99             True, area.x, area.y, area.width, area.height)
100
101         for layout in self.pangolayout:
102             w, h = layout.get_pixel_size()
103             if ((layout.posY >= area.y and
104                 layout.posY < area.y + area.height) or
105                 (layout.posY + h >= area.y and
106                  layout.posY + h < area.y + area.height) or
107                 (layout.posY <= area.y and layout.posY + h >= area.y)):
108                 self.drawingarea.window.draw_layout(gc, 0, layout.posY, layout)
109
110     def on_drawingarea_expose_event(self, widget, event, data=None):
111         self.draw_viewport(event.area)
112         return True
113
114     def on_drawingarea_size_allocate(self, widget, allocation, data=None):
115         if allocation.width != self.drawingarea.prev_width:
116             self.relayout()
117             self.drawingarea.prev_width = allocation.width
118         return False
119
120     def __init__(self, uri):
121         self.pangolayout = []
122
123         from BbsType import bbs_type_judge_uri
124         from BbsType import bbs_type_exception
125         self.bbs_type = bbs_type_judge_uri.get_type(uri)
126         if not self.bbs_type.is_thread():
127             raise bbs_type_exception.BbsTypeError, \
128                   "the uri does not represent thread: " + uri
129         self.size = 0
130         self.num = 0
131         self.title = ""
132         self.lock_obj = False
133         self.jump_request_num = 0
134         self.progress = False
135
136         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
137         self.widget_tree = gtk.glade.XML(glade_path)
138         self.window = self.widget_tree.get_widget("thread_window")
139         self.toolbar = self.widget_tree.get_widget("toolbar")
140         self.toolbar.unset_style()
141         self.statusbar = self.widget_tree.get_widget("statusbar")
142         self.drawingarea = self.widget_tree.get_widget("drawingarea")
143         self.viewport = self.drawingarea.parent
144
145         self.drawingarea.prev_width = 0
146
147         self.initialize_buffer()
148
149         sigdic = {"on_refresh_activate": self.update,
150                   "on_compose_activate": self.on_compose_clicked,
151                   "on_toolbar_activate": self.on_toolbar_activate,
152                   "on_statusbar_activate": self.on_statusbar_activate,
153                   "on_refresh_activate": self.update,
154                   "on_close_activate": self.on_close_activate,
155                   "on_quit_activate": self.on_quit_activate,
156                   "on_show_board_activate": self.on_show_board_activate,
157                   "on_delete_activate": self.on_delete_activate,
158                   "on_drawingarea_expose_event": self.on_drawingarea_expose_event,
159                   "on_drawingarea_size_allocate":
160                   self.on_drawingarea_size_allocate,
161                   "on_thread_window_delete_event":
162                   self.on_thread_window_delete_event,
163                   "on_add_bookmark_activate": self.on_add_bookmark_activate,
164                   "on_manage_bookmarks_activate": \
165                   self.on_manage_bookmarks_activate,
166                   "on_thread_window_destroy": self.on_thread_window_destroy}
167         self.widget_tree.signal_autoconnect(sigdic)
168
169         self.restore()
170         self.window.show_all()
171
172         self.created()
173
174     def initialize_buffer(self):
175         self.textbuffer = gtk.TextBuffer()
176
177         self.enditer = self.textbuffer.get_end_iter()
178         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
179         self.leftmargintag = self.textbuffer.create_tag()
180         self.leftmargintag.set_property("left-margin", 20)
181
182         
183
184     def destroy(self):
185         self.save()
186         self.window.destroy()
187
188     def get_uri(self):
189         return self.bbs_type.get_thread_uri()
190
191     def on_compose_clicked(self, widget):
192         import submit_window
193         submit_window.open(self.bbs_type.get_thread_uri())
194
195     def on_toolbar_activate(self, widget):
196         if self.toolbar.get_property("visible"):
197             self.toolbar.hide()
198         else:
199             self.toolbar.show()
200
201     def on_statusbar_activate(self, widget):
202         if self.statusbar.get_property("visible"):
203             self.statusbar.hide()
204         else:
205             self.statusbar.show()
206
207     def on_close_activate(self, widget):
208         self.destroy()
209
210     def on_thread_window_delete_event(self, widget, event):
211         self.save()
212         return False
213         
214     def on_thread_window_destroy(self, widget):
215         self.destroyed()
216
217     def on_quit_activate(self, widget):
218         session.main_quit()
219
220     def on_add_bookmark_activate(self, widget):
221         bookmark_list.bookmark_list.add_bookmark_with_edit(
222             name=self.title, uri=self.bbs_type.uri)
223
224     def on_manage_bookmarks_activate(self, widget):
225         bookmark_window.open()
226
227     def on_show_board_activate(self, widget):
228         board_window.open_board(self.bbs_type.get_uri_base())
229
230     def on_delete_activate(self, widget):
231         try:
232             dat_path = misc.get_thread_dat_path(self.bbs_type)
233             os.remove(dat_path)
234         except OSError:
235             traceback.print_exc()
236         try:
237             idx_path = misc.get_thread_idx_path(self.bbs_type)
238             os.remove(idx_path)
239         except OSError:
240             traceback.print_exc()
241         try:
242             states_path = misc.get_thread_states_path(self.bbs_type)
243             os.remove(states_path)
244         except OSError:
245             traceback.print_exc()
246
247     def http_get_dat(self, on_get_res):
248         datfile_url = self.bbs_type.get_dat_uri()
249
250         idx_dic = idxfile.load_idx(self.bbs_type)
251         lastmod = idx_dic["lastModified"]
252         etag = idx_dic["etag"]
253
254         req = urllib2.Request(datfile_url)
255         req.add_header("User-agent", config.User_Agent)
256         if self.size > 0:
257             req.add_header("Range", "bytes=" + str(self.size) + "-")
258         if lastmod:
259             req.add_header("If-Modified-Since", lastmod)
260         if etag:
261             req.add_header("If-None-Match", etag)
262
263         req = self.bbs_type.set_extra_dat_request(req, self)
264
265         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
266         try:
267             res = opener.open(req)
268         except urllib2.HTTPError, e:
269             pass
270 #             gobject.idle_add(
271 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (e.code, e.msg))
272         else:
273             headers = res.info()
274 #             gobject.idle_add(
275 #                 lambda x: self.statusbar.push(0, x), "%d %s" % (res.code, res.msg))
276
277             maybe_incomplete = False
278             for line in res:
279                 if not line.endswith("\n"):
280                     maybe_incomplete = True
281                     print "does not end with \\n. maybe incomplete"
282                     break
283                 on_get_res(line)
284
285             res.close()
286
287             if maybe_incomplete:
288                 lastmod = None
289                 etag = None
290             else:
291                 if "Last-Modified" in headers:
292                     lastmod = headers["Last-Modified"]
293                 if "ETag" in headers:
294                     etag = headers["Etag"]
295
296             if self.num > 0:
297                 # save idx
298                 idx_dic = {"title": self.title, "lineCount": self.num,
299                        "lastModified": lastmod, "etag": etag}
300                 idxfile.save_idx(self.bbs_type, idx_dic)
301
302                 gobject.idle_add(session.thread_idx_updated,
303                                  self.bbs_type.get_thread_uri(), idx_dic)
304
305     def update(self, widget=None):
306
307         self.jump_request_num = 0
308
309         def load():
310             if self.num == 0:
311                 def create_mark():
312                     self.textbuffer.create_mark("1", self.enditer, True)
313                 gobject.idle_add(create_mark)
314
315             line_count = datfile.get_dat_line_count(self.bbs_type)
316             if line_count < self.num:
317                 self.num = 0
318                 self.size = 0
319
320                 gobject.idle_add(self.initialize_buffer)
321
322             if line_count > self.num:
323                 datfile.load_dat_partly(
324                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
325
326                 def do_jump(num):
327                     if self.jump_request_num:
328                         if self.jump_request_num <= num:
329                             # jump if enable, otherwize jump later.
330                             num = self.jump_request_num
331                             self.jump_request_num = 0
332                             mark = self.textbuffer.get_mark(str(num))
333 #                            if mark:
334 #                                self.textview.scroll_to_mark(
335 #                                    mark, 0, True, 0, 0)
336                     else:
337                         self.jump_to_the_end(num)
338
339                 gobject.idle_add(do_jump, self.num)
340
341         def get():
342             dat_path = misc.get_thread_dat_path(self.bbs_type)
343             dat_file = FileWrap(dat_path)
344
345             def save_line_and_append_to_buffer(line):
346                 dat_file.seek(self.size)
347                 dat_file.write(line)
348                 self.append_rawres_to_buffer(line)
349
350             self.http_get_dat(save_line_and_append_to_buffer)
351             dat_file.close()
352
353             def do_jump():
354                 if self.jump_request_num:
355                     num = self.jump_request_num
356                     self.jump_request_num = 0
357                     mark = self.textbuffer.get_mark(str(num))
358 #                    if mark:
359 #                        self.textview.scroll_to_mark(mark, 0, True, 0, 0)
360
361             gobject.idle_add(do_jump)
362
363         if self.lock():
364
365             def on_end():
366                 self.un_lock()
367                 self.progress = False
368
369             self.progress = True
370             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
371             t.start()
372
373     def load_dat(self):
374
375         self.size = 0
376         self.num = 0
377         self.jump_request_num = 0
378
379         def load():
380
381             def create_mark():
382                 self.textbuffer.create_mark("1", self.enditer, True)
383             gobject.idle_add(create_mark)
384
385             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
386         def jump():
387
388             def do_jump(num):
389                 if self.jump_request_num:
390                     num = self.jump_request_num
391                     self.jump_request_num = 0
392                     mark = self.textbuffer.get_mark(str(num))
393 #                    if mark:
394 #                        self.textview.scroll_to_mark(mark, 0, True, 0, 0)
395                 else:
396                     self.jump_to_the_end(num)
397
398             gobject.idle_add(do_jump, self.num)
399
400         if self.lock():
401
402             def on_end():
403                 self.un_lock()
404                 self.progress = False
405
406             self.progress = True
407             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
408             t.start()
409
410     def append_rawres_to_buffer(self, line):
411         self.size += len(line)
412         self.num += 1
413
414         if not self.title and self.num == 1:
415             title = self.bbs_type.get_title_from_dat(line)
416             if title:
417                 self.title = title
418                 gobject.idle_add(self.window.set_title, title)
419
420         self.res_queue = []
421
422         line = line.decode(self.bbs_type.encoding, "replace")
423         m = self.bbs_type.dat_reg.match(line)
424         if m:
425             name = m.group("name")
426             mail = m.group("mail")
427             date = m.group("date")
428             msg = m.group("msg")
429             try:
430                 num = int(m.group("num"))
431             except IndexError:
432                 # use simple counter num
433                 num = self.num
434             else:
435                 # use num in dat
436                 self.num = num
437             try:
438                 id = m.group("id")
439             except IndexError:
440                 pass
441             else:
442                 if id:
443                     date += " ID:" + id
444             self.reselems_to_buffer(num, name, mail, date, msg)
445         else:
446             self.res_queue.append((str(self.num)+"\n", False, None, False))
447             self.res_queue.append((line, False, None, True))
448             print "maybe syntax error.", self.num, line
449
450         def process_res_queue(res_queue, num):
451             self.process_queue(res_queue)
452             # for next res
453             #self.textbuffer.create_mark(str(num+1), self.enditer, True)
454
455         gobject.idle_add(
456             process_res_queue, self.res_queue, self.num)
457
458     def reselems_to_buffer(self, num, name, mail, date, msg):
459         p = barehtmlparser.BareHTMLParser(
460             lambda d,b,h: self.res_queue.append((d,b,h,False)))
461         # number
462         p.feed(str(num) + " ")
463
464         # name
465         p.feed("<b>" + name + "</b>")
466
467         # mail
468         p.feed("[" + mail + "]")
469
470         # date
471         p.feed(date)
472         p.feed("<br>")
473
474         # msg
475         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
476         p.feed(msg.lstrip(" "))
477
478         p.feed("<br><br>")
479         p.close()
480
481     def href_tag(self, href):
482         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
483         tag.set_data("href", href)
484         return tag
485
486     def process_queue(self, queue):
487         text = ""
488         for data, bold, href, margin in queue:
489             text += data
490         layout = self.drawingarea.create_pango_layout(text)
491         layout.set_wrap(pango.WRAP_CHAR)
492         layout.posY = 0
493         self.pangolayout.append(layout)
494         self.relayout()
495 #             taglist = []
496 #             if bold:
497 #                 taglist.append(self.boldtag)
498 #             if href:
499 #                 taglist.append(self.href_tag(href))
500 #             if margin:
501 #                 taglist.append(self.leftmargintag)
502 #
503 #            if taglist:
504 #                self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
505 #            else:
506 #                self.textbuffer.insert(self.enditer, data)
507
508     def jump_to_the_end(self, num):
509         mark = self.textbuffer.get_mark(str(num+1))
510 #        if mark:
511 #            self.textview.scroll_to_mark(mark, 0)
512
513     def lock(self):
514         if self.lock_obj:
515             print "locked, try later."
516             return False
517         else:
518             print "get lock"
519             self.lock_obj = True
520             return True
521
522     def un_lock(self):
523         self.lock_obj = False
524         print "unlock"
525
526     def jump_to_res(self, uri):
527         strict_uri = self.bbs_type.get_thread_uri()
528         if uri != strict_uri and uri.startswith(strict_uri):
529             resnum = uri[len(strict_uri):]
530             match = re.match("\d+", resnum)
531             if match:
532                 resnum = match.group()
533                 mark = self.textbuffer.get_mark(resnum)
534 #                if mark:
535 #                    self.textview.scroll_to_mark(mark, 0, True, 0, 0)
536 #                elif self.progress:
537 #                    # try later.
538 #                    self.jump_request_num = int(resnum)
539
540     def load(self, update=False):
541         dat_path = misc.get_thread_dat_path(self.bbs_type)
542         dat_exists = os.path.exists(dat_path)
543         if update or not dat_exists:
544             self.update()
545         else:
546             self.load_dat()
547
548     def save(self):
549         try:
550             states_path = misc.get_thread_states_path(self.bbs_type)
551             dat_path = misc.get_thread_dat_path(self.bbs_type)
552
553             # save only if dat file exists.
554             if os.path.exists(dat_path):
555                 window_width, window_height = self.window.get_size()
556                 toolbar_visible = self.toolbar.get_property("visible")
557                 statusbar_visible = self.statusbar.get_property("visible")
558
559                 dirname = os.path.dirname(states_path)
560                 if not os.path.isdir(dirname):
561                     os.makedirs(dirname)
562
563                 f = file(states_path, "w")
564
565                 f.write("window_width=" + str(window_width) + "\n")
566                 f.write("window_height=" + str(window_height) + "\n")
567                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
568                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
569
570                 f.close()
571         except:
572             traceback.print_exc()
573
574     def restore(self):
575         try:
576             window_height = 600
577             window_width = 600
578             toolbar_visible = True
579             statusbar_visible = True
580
581             try:
582                 key_base = config.gconf_app_key_base() + "/thread_states"
583                 gconf_client = gconf.client_get_default()
584                 width = gconf_client.get_int(key_base + "/window_width")
585                 height = gconf_client.get_int(key_base + "/window_height")
586                 toolbar_visible = gconf_client.get_bool(
587                     key_base + "/toolbar")
588                 statusbar_visible = gconf_client.get_bool(
589                     key_base + "/statusbar")
590                 if width != 0:
591                     window_width = width
592                 if height != 0:
593                     window_height = height
594             except:
595                 traceback.print_exc()
596
597             states_path = misc.get_thread_states_path(self.bbs_type)
598             if os.path.exists(states_path):
599                 for line in file(states_path):
600                     if line.startswith("window_height="):
601                         height = window_height
602                         try:
603                             height = int(
604                                 line[len("window_height="):].rstrip("\n"))
605                         except:
606                             pass
607                         else:
608                             window_height = height
609                     elif line.startswith("window_width="):
610                         width = window_width
611                         try:
612                             width = int(
613                                 line[len("window_width="):].rstrip("\n"))
614                         except:
615                             pass
616                         else:
617                             window_width = width
618                     elif line.startswith("toolbar_visible="):
619                         tbar = line[len("toolbar_visible="):].rstrip("\n")
620                         toolbar_visible = tbar == "True"
621                     elif line.startswith("statusbar_visible="):
622                         sbar = line[len("statusbar_visible="):].rstrip("\n")
623                         statusbar_visible = sbar == "True"
624
625             self.window.set_default_size(window_width, window_height)
626
627             if not toolbar_visible:
628                 gobject.idle_add(self.toolbar.hide,
629                                  priority=gobject.PRIORITY_HIGH)
630             if not statusbar_visible:
631                 gobject.idle_add(self.statusbar.hide,
632                                  priority=gobject.PRIORITY_HIGH)
633         except:
634             traceback.print_exc()