OSDN Git Service

Reimprement regist and unregist a window to session.
[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
34 import misc
35 import datfile
36 import barehtmlparser
37 import idxfile
38 import session
39 import board_window
40 import uri_opener
41 from http_sub import HTTPRedirectHandler302
42 from BbsType import bbs_type_judge_uri
43 from BbsType import bbs_type_exception
44 import config
45 import winwrapbase
46
47 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
48                          "..", "data")
49 GLADE_FILENAME = "thread_window.glade"
50
51 def open_thread(uri, update=False):
52     if not uri:
53         raise ValueError, "parameter must not be empty"
54
55     bbs_type = bbs_type_judge_uri.get_type(uri)
56     if not bbs_type.is_thread():
57         raise bbs_type_exception.BbsTypeError, \
58               "the uri does not represent thread: " + uri
59     uri = bbs_type.get_thread_uri()  # use strict thread uri
60
61     winwrap = session.get_window(uri)
62     if winwrap:
63         # already opened
64         winwrap.window.present()
65         if update:
66             winwrap.load(update)
67     else:
68         winwrap = WinWrap(bbs_type.uri)  # pass original uri
69         winwrap.load(update)
70
71     # jump to the res if necessary.
72     winwrap.jump_to_res(bbs_type.uri)
73
74
75 class ThreadInvoker(threading.Thread):
76     def __init__(self, on_end, *methods):
77         super(ThreadInvoker, self).__init__()
78         self.on_end = on_end
79         self.methods = methods
80     def run(self):
81         try:
82             for m in self.methods:
83                 m()
84         finally:
85             self.on_end()
86
87
88 class FileWrap:
89     def __init__(self, path):
90         self._file = None
91         self._path = path
92     def __del__(self):
93         self.close()
94     def seek(self, size):
95         self.file().seek(size)
96     def write(self, data):
97         self.file().write(data)
98     def close(self):
99         if self._file:
100             self._file.close()
101             self._file = None
102     def file(self):
103         if not self._file:
104             basedir = os.path.dirname(self._path)
105             if not os.path.isdir(basedir):
106                 os.makedirs(basedir)
107             self._file = file(self._path, "a+")
108         return self._file
109
110
111 class WinWrap(winwrapbase.WinWrapBase):
112     hovering_over_link = False
113     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
114     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
115
116
117     def __init__(self, uri):
118         from BbsType import bbs_type_judge_uri
119         from BbsType import bbs_type_exception
120         self.bbs_type = bbs_type_judge_uri.get_type(uri)
121         if not self.bbs_type.is_thread():
122             raise bbs_type_exception.BbsTypeError, \
123                   "the uri does not represent thread: " + uri
124         self.bbs = self.bbs_type.bbs_type
125         self.board = self.bbs_type.board
126         self.thread = self.bbs_type.thread
127         self.host = self.bbs_type.host
128         self.uri = self.bbs_type.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(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("appbar")
142         self.textview = self.widget_tree.get_widget("textview")
143         self.textbuffer = self.textview.get_buffer()
144         self.enditer = self.textbuffer.get_end_iter()
145         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
146         self.leftmargintag = self.textbuffer.create_tag()
147         self.leftmargintag.set_property("left-margin", 20)
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_thread_window_delete_event":
158                   self.on_thread_window_delete_event,
159                   "on_thread_window_destroy": self.on_thread_window_destroy}
160         self.widget_tree.signal_autoconnect(sigdic)
161
162         self.textview.connect("event-after", self.on_event_after)
163         self.textview.connect("motion-notify-event",
164                               self.on_motion_notify_event)
165         self.textview.connect("visibility-notify-event",
166                               self.on_visibility_notify_event)
167         self.restore()
168         self.window.show()
169
170         self.created()
171
172     def destroy(self):
173         self.save()
174         self.window.destroy()
175
176     def get_uri(self):
177         return self.bbs_type.get_thread_uri()
178
179     def on_compose_clicked(self, widget):
180         import submit_window
181         submit_window.open(self.bbs_type.get_thread_uri())
182
183     def on_toolbar_activate(self, widget):
184         if self.toolbar.parent.get_property("visible"):
185             self.toolbar.parent.hide()
186         else:
187             self.toolbar.parent.show()
188
189     def on_statusbar_activate(self, widget):
190         if self.statusbar.get_property("visible"):
191             self.statusbar.hide()
192         else:
193             self.statusbar.show()
194
195     def on_event_after(self, widget, event):
196         if event.type != gtk.gdk.BUTTON_RELEASE:
197             return False
198         if event.button != 1:
199             return False
200         buffer = widget.get_buffer()
201
202         try:
203             start, end = buffer.get_selection_bounds()
204         except ValueError:
205             pass
206         else:
207             if start.get_offset() != end.get_offset():
208                 return False
209
210         x, y = widget.window_to_buffer_coords(
211             gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
212         iter = widget.get_iter_at_location(x, y)
213         if not iter.has_tag(self.leftmargintag) or x > 20:
214             tags = iter.get_tags()
215             for tag in tags:
216                 href = tag.get_data("href")
217                 if href:
218                     self.on_link_clicked(widget, href)
219         return False
220
221     def on_link_clicked(self, widget, href):
222
223         if not href.startswith("http://"):
224             # maybe a relative uri.
225             href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
226
227         try:
228             uri_opener.open_uri(href)
229         except bbs_type_exception.BbsTypeError:
230             # not supported, show with the web browser.
231             gnome.url_show(href)
232
233     def on_motion_notify_event(self, widget, event):
234         x, y = widget.window_to_buffer_coords(
235             gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
236         self.set_cursor_if_appropriate(widget, x, y)
237         widget.window.get_pointer()
238         return False
239
240     def on_visibility_notify_event(self, widget, event):
241         wx, wy, mod = widget.window.get_pointer()
242         bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
243
244         self.set_cursor_if_appropriate(widget, bx, by)
245         return False
246
247     def set_cursor_if_appropriate(self, widget, x, y):
248         hovering = False
249
250         buffer = widget.get_buffer()
251         iter = widget.get_iter_at_location(x, y)
252         if not iter.has_tag(self.leftmargintag) or x > 20:
253             tags = iter.get_tags()
254             for tag in tags:
255                 href = tag.get_data("href")
256                 if href:
257                     hovering = True
258
259         if hovering != self.hovering_over_link:
260             self.hovering_over_link = hovering
261
262         if self.hovering_over_link:
263             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
264                 self.hand_cursor)
265         else:
266             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
267                 self.regular_cursor)
268
269     def on_close_activate(self, widget):
270         self.destroy()
271
272     def on_thread_window_delete_event(self, widget, event):
273         self.save()
274         return False
275         
276     def on_thread_window_destroy(self, widget):
277         self.destroyed()
278
279     def on_quit_activate(self, widget):
280         session.main_quit()
281
282     def on_show_board_activate(self, widget):
283         board_window.open_board(self.bbs_type.get_uri_base())
284
285     def http_get_dat(self, on_get_res):
286         datfile_url = self.bbs_type.get_dat_uri()
287
288         idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
289         lastmod = idx_dic["lastModified"]
290         etag = idx_dic["etag"]
291
292         req = urllib2.Request(datfile_url)
293         if self.size > 0:
294             req.add_header("Range", "bytes=" + str(self.size) + "-")
295         if lastmod:
296             req.add_header("If-Modified-Since", lastmod)
297         if etag:
298             req.add_header("If-None-Match", etag)
299         print req.headers
300
301         opener = urllib2.build_opener(HTTPRedirectHandler302)
302         res = opener.open(req)
303         headers = res.info()
304         print headers
305
306         line = res.readline()
307         maybe_incomplete = False
308         while line:
309             if not line.endswith("\n"):
310                 maybe_incomplete = True
311                 print "does not end with \\n. maybe incomplete"
312                 break
313             on_get_res(line)
314             line = res.readline()
315
316         res.close()
317
318         if maybe_incomplete:
319             lastmod = None
320             etag = None
321         else:
322             if "Last-Modified" in headers:
323                 lastmod = headers["Last-Modified"]
324             if "ETag" in headers:
325                 etag = headers["Etag"]
326
327         if self.num > 0:
328             if not self.title:
329                 title = datfile.get_title_from_dat(
330                     self.bbs, self.board, self.thread)
331                 if title:
332                     self.title = title
333                     gobject.idle_add(self.window.set_title, title)
334             # save idx
335             idx_dic = {"title": self.title, "lineCount": self.num,
336                    "lastModified": lastmod, "etag": etag}
337             idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
338
339             gobject.idle_add(session.thread_idx_updated,
340                              self.bbs_type.get_thread_uri(), idx_dic)
341
342     def update(self, widget=None):
343
344         self.jump_request_num = 0
345
346         def load():
347             if self.num == 0:
348                 def create_mark():
349                     self.textbuffer.create_mark("1", self.enditer, True)
350                 gobject.idle_add(create_mark)
351
352             line_count = datfile.get_dat_line_count(
353                 self.bbs, self.board, self.thread)
354             if line_count > self.num:
355                 datfile.load_dat_partly(
356                     self.bbs, self.board, self.thread,
357                     self.append_rawres_to_buffer, self.num+1)
358
359                 def do_jump(num):
360                     if self.jump_request_num:
361                         if self.jump_request_num <= num:
362                             # jump if enable, otherwize jump later.
363                             num = self.jump_request_num
364                             self.jump_request_num = 0
365                             mark = self.textbuffer.get_mark(str(num))
366                             if mark:
367                                 self.textview.scroll_to_mark(
368                                     mark, 0, True, 0, 0)
369                     else:
370                         self.jump_to_the_end(num)
371
372                 gobject.idle_add(do_jump, self.num)
373
374         def get():
375             dat_path = misc.get_thread_dat_path(
376                 self.bbs, self.board, self.thread)
377             dat_file = FileWrap(dat_path)
378
379             def save_line_and_append_to_buffer(line):
380                 dat_file.seek(self.size)
381                 dat_file.write(line)
382                 self.append_rawres_to_buffer(line)
383
384             self.http_get_dat(save_line_and_append_to_buffer)
385             dat_file.close()
386
387             def do_jump():
388                 if self.jump_request_num:
389                     num = self.jump_request_num
390                     self.jump_request_num = 0
391                     mark = self.textbuffer.get_mark(str(num))
392                     if mark:
393                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
394
395             gobject.idle_add(do_jump)
396
397         if self.lock():
398
399             def on_end():
400                 self.un_lock()
401                 self.progress = False
402
403             self.progress = True
404             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
405             t.start()
406
407     def load_dat(self):
408
409         self.size = 0
410         self.num = 0
411         self.jump_request_num = 0
412
413         def load():
414
415             def create_mark():
416                 self.textbuffer.create_mark("1", self.enditer, True)
417             gobject.idle_add(create_mark)
418
419             datfile.load_dat(self.bbs, self.board, self.thread,
420                              self.append_rawres_to_buffer)
421         def jump():
422
423             def do_jump(num):
424                 if self.jump_request_num:
425                     num = self.jump_request_num
426                     self.jump_request_num = 0
427                     mark = self.textbuffer.get_mark(str(num))
428                     if mark:
429                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
430                 else:
431                     self.jump_to_the_end(num)
432
433             gobject.idle_add(do_jump, self.num)
434
435         if self.lock():
436
437             def on_end():
438                 self.un_lock()
439                 self.progress = False
440
441             self.progress = True
442             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
443             t.start()
444
445     def append_rawres_to_buffer(self, line):
446         self.size += len(line)
447         self.num += 1
448
449         if not self.title and self.num == 1:
450             title = datfile.do_get_title_from_dat(line)
451             if title:
452                 self.title = title
453                 gobject.idle_add(self.window.set_title, title)
454
455         h = lambda name,mail,date,msg: self.reselems_to_buffer(
456             self.num, name, mail, date, msg)
457
458         self.res_queue = []
459         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
460
461         def process_res_queue(res_queue, num):
462             self.process_queue(res_queue)
463             # for next res
464             self.textbuffer.create_mark(str(num+1), self.enditer, True)
465
466         gobject.idle_add(
467             process_res_queue, self.res_queue, self.num)
468
469     def reselems_to_buffer(self, num, name, mail, date, msg):
470         p = barehtmlparser.BareHTMLParser(
471             lambda d,b,h: self.res_queue.append((d,b,h,False)))
472         # number
473         p.feed(str(num) + " ")
474
475         # name
476         p.feed("<b>" + name + "</b>")
477
478         # mail
479         p.feed("[" + mail + "]")
480
481         # date
482         p.feed(date)
483         p.feed("<br>")
484
485         # msg
486         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
487         p.feed(msg.lstrip(" "))
488
489         p.feed("<br><br>")
490         p.close()
491
492     def href_tag(self, href):
493         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
494         tag.set_data("href", href)
495         return tag
496
497     def process_queue(self, queue):
498         for data, bold, href, margin in queue:
499             taglist = []
500             if bold:
501                 taglist.append(self.boldtag)
502             if href:
503                 taglist.append(self.href_tag(href))
504             if margin:
505                 taglist.append(self.leftmargintag)
506
507             if taglist:
508                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
509             else:
510                 self.textbuffer.insert(self.enditer, data)
511
512     def jump_to_the_end(self, num):
513         mark = self.textbuffer.get_mark(str(num+1))
514         if mark:
515             self.textview.scroll_to_mark(mark, 0)
516
517     def lock(self):
518         if self.lock_obj:
519             print "locked, try later."
520             return False
521         else:
522             print "get lock"
523             self.lock_obj = True
524             return True
525
526     def un_lock(self):
527         self.lock_obj = False
528         print "unlock"
529
530     def jump_to_res(self, uri):
531         strict_uri = self.bbs_type.get_thread_uri()
532         if uri != strict_uri and uri.startswith(strict_uri):
533             resnum = uri[len(strict_uri):]
534             match = re.match("\d+", resnum)
535             if match:
536                 resnum = match.group()
537                 mark = self.textbuffer.get_mark(resnum)
538                 if mark:
539                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
540                 elif self.progress:
541                     # try later.
542                     self.jump_request_num = int(resnum)
543
544     def load(self, update=False):
545         dat_path = misc.get_thread_dat_path(
546             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
547         dat_exists = os.path.exists(dat_path)
548         if update or not dat_exists:
549             self.update()
550         else:
551             self.load_dat()
552
553     def save(self):
554         try:
555             states_path = misc.get_thread_states_path(
556                 self.bbs_type.bbs_type, self.bbs_type.board,
557                 self.bbs_type.thread)
558             dat_path = misc.get_thread_dat_path(
559                 self.bbs_type.bbs_type, self.bbs_type.board,
560                 self.bbs_type.thread)
561
562             # save only if dat file exists.
563             if os.path.exists(dat_path):
564                 window_width, window_height = self.window.get_size()
565                 toolbar_visible = self.toolbar.parent.get_property("visible")
566                 statusbar_visible = self.statusbar.get_property("visible")
567
568                 dirname = os.path.dirname(states_path)
569                 if not os.path.isdir(dirname):
570                     os.makedirs(dirname)
571
572                 f = file(states_path, "w")
573
574                 f.write("window_width=" + str(window_width) + "\n")
575                 f.write("window_height=" + str(window_height) + "\n")
576                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
577                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
578
579                 f.close()
580         except:
581             traceback.print_exc()
582
583     def restore(self):
584         try:
585             window_height = 600
586             window_width = 600
587             toolbar_visible = True
588             statusbar_visible = True
589
590             try:
591                 key_base = config.gconf_app_key_base() + "/thread_states"
592                 gconf_client = gconf.client_get_default()
593                 width = gconf_client.get_int(key_base + "/window_width")
594                 height = gconf_client.get_int(key_base + "/window_height")
595                 toolbar_visible = gconf_client.get_bool(
596                     key_base + "/toolbar")
597                 statusbar_visible = gconf_client.get_bool(
598                     key_base + "/statusbar")
599                 if width != 0:
600                     window_width = width
601                 if height != 0:
602                     window_height = height
603             except:
604                 traceback.print_exc()
605
606             states_path = misc.get_thread_states_path(
607                 self.bbs_type.bbs_type, self.bbs_type.board,
608                 self.bbs_type.thread)
609             if os.path.exists(states_path):
610                 for line in file(states_path):
611                     if line.startswith("window_height="):
612                         height = window_height
613                         try:
614                             height = int(
615                                 line[len("window_height="):].rstrip("\n"))
616                         except:
617                             pass
618                         else:
619                             window_height = height
620                     elif line.startswith("window_width="):
621                         width = window_width
622                         try:
623                             width = int(
624                                 line[len("window_width="):].rstrip("\n"))
625                         except:
626                             pass
627                         else:
628                             window_width = width
629                     elif line.startswith("toolbar_visible="):
630                         tbar = line[len("toolbar_visible="):].rstrip("\n")
631                         toolbar_visible = tbar == "True"
632                     elif line.startswith("statusbar_visible="):
633                         sbar = line[len("statusbar_visible="):].rstrip("\n")
634                         statusbar_visible = sbar == "True"
635
636             self.window.set_default_size(window_width, window_height)
637
638             if not toolbar_visible:
639                 gobject.idle_add(self.toolbar.parent.hide)
640             if not statusbar_visible:
641                 gobject.idle_add(self.statusbar.hide)
642         except:
643             traceback.print_exc()