OSDN Git Service

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