OSDN Git Service

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