OSDN Git Service

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