OSDN Git Service

Move ThreadInvoker and FileWrap to misc.py
[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         res = opener.open(req)
269         headers = res.info()
270         print headers
271
272         line = res.readline()
273         maybe_incomplete = False
274         while line:
275             if not line.endswith("\n"):
276                 maybe_incomplete = True
277                 print "does not end with \\n. maybe incomplete"
278                 break
279             on_get_res(line)
280             line = res.readline()
281
282         res.close()
283
284         if maybe_incomplete:
285             lastmod = None
286             etag = None
287         else:
288             if "Last-Modified" in headers:
289                 lastmod = headers["Last-Modified"]
290             if "ETag" in headers:
291                 etag = headers["Etag"]
292
293         if self.num > 0:
294             if not self.title:
295                 title = datfile.get_title_from_dat(
296                     self.bbs, self.board, self.thread)
297                 if title:
298                     self.title = title
299                     gobject.idle_add(self.window.set_title, title)
300             # save idx
301             idx_dic = {"title": self.title, "lineCount": self.num,
302                    "lastModified": lastmod, "etag": etag}
303             idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
304
305             gobject.idle_add(session.thread_idx_updated,
306                              self.bbs_type.get_thread_uri(), idx_dic)
307
308     def update(self, widget=None):
309
310         self.jump_request_num = 0
311
312         def load():
313             if self.num == 0:
314                 def create_mark():
315                     self.textbuffer.create_mark("1", self.enditer, True)
316                 gobject.idle_add(create_mark)
317
318             line_count = datfile.get_dat_line_count(
319                 self.bbs, self.board, self.thread)
320             if line_count > self.num:
321                 datfile.load_dat_partly(
322                     self.bbs, self.board, self.thread,
323                     self.append_rawres_to_buffer, self.num+1)
324
325                 def do_jump(num):
326                     if self.jump_request_num:
327                         if self.jump_request_num <= num:
328                             # jump if enable, otherwize jump later.
329                             num = self.jump_request_num
330                             self.jump_request_num = 0
331                             mark = self.textbuffer.get_mark(str(num))
332                             if mark:
333                                 self.textview.scroll_to_mark(
334                                     mark, 0, True, 0, 0)
335                     else:
336                         self.jump_to_the_end(num)
337
338                 gobject.idle_add(do_jump, self.num)
339
340         def get():
341             dat_path = misc.get_thread_dat_path(
342                 self.bbs, self.board, self.thread)
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, self.board, self.thread,
386                              self.append_rawres_to_buffer)
387         def jump():
388
389             def do_jump(num):
390                 if self.jump_request_num:
391                     num = self.jump_request_num
392                     self.jump_request_num = 0
393                     mark = self.textbuffer.get_mark(str(num))
394                     if mark:
395                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
396                 else:
397                     self.jump_to_the_end(num)
398
399             gobject.idle_add(do_jump, self.num)
400
401         if self.lock():
402
403             def on_end():
404                 self.un_lock()
405                 self.progress = False
406
407             self.progress = True
408             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
409             t.start()
410
411     def append_rawres_to_buffer(self, line):
412         self.size += len(line)
413         self.num += 1
414
415         if not self.title and self.num == 1:
416             title = datfile.do_get_title_from_dat(line)
417             if title:
418                 self.title = title
419                 gobject.idle_add(self.window.set_title, title)
420
421         h = lambda name,mail,date,msg: self.reselems_to_buffer(
422             self.num, name, mail, date, msg)
423
424         self.res_queue = []
425         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
426
427         def process_res_queue(res_queue, num):
428             self.process_queue(res_queue)
429             # for next res
430             self.textbuffer.create_mark(str(num+1), self.enditer, True)
431
432         gobject.idle_add(
433             process_res_queue, self.res_queue, self.num)
434
435     def reselems_to_buffer(self, num, name, mail, date, msg):
436         p = barehtmlparser.BareHTMLParser(
437             lambda d,b,h: self.res_queue.append((d,b,h,False)))
438         # number
439         p.feed(str(num) + " ")
440
441         # name
442         p.feed("<b>" + name + "</b>")
443
444         # mail
445         p.feed("[" + mail + "]")
446
447         # date
448         p.feed(date)
449         p.feed("<br>")
450
451         # msg
452         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
453         p.feed(msg.lstrip(" "))
454
455         p.feed("<br><br>")
456         p.close()
457
458     def href_tag(self, href):
459         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
460         tag.set_data("href", href)
461         return tag
462
463     def process_queue(self, queue):
464         for data, bold, href, margin in queue:
465             taglist = []
466             if bold:
467                 taglist.append(self.boldtag)
468             if href:
469                 taglist.append(self.href_tag(href))
470             if margin:
471                 taglist.append(self.leftmargintag)
472
473             if taglist:
474                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
475             else:
476                 self.textbuffer.insert(self.enditer, data)
477
478     def jump_to_the_end(self, num):
479         mark = self.textbuffer.get_mark(str(num+1))
480         if mark:
481             self.textview.scroll_to_mark(mark, 0)
482
483     def lock(self):
484         if self.lock_obj:
485             print "locked, try later."
486             return False
487         else:
488             print "get lock"
489             self.lock_obj = True
490             return True
491
492     def un_lock(self):
493         self.lock_obj = False
494         print "unlock"
495
496     def jump_to_res(self, uri):
497         strict_uri = self.bbs_type.get_thread_uri()
498         if uri != strict_uri and uri.startswith(strict_uri):
499             resnum = uri[len(strict_uri):]
500             match = re.match("\d+", resnum)
501             if match:
502                 resnum = match.group()
503                 mark = self.textbuffer.get_mark(resnum)
504                 if mark:
505                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
506                 elif self.progress:
507                     # try later.
508                     self.jump_request_num = int(resnum)
509
510     def load(self, update=False):
511         dat_path = misc.get_thread_dat_path(
512             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
513         dat_exists = os.path.exists(dat_path)
514         if update or not dat_exists:
515             self.update()
516         else:
517             self.load_dat()
518
519     def save(self):
520         try:
521             states_path = misc.get_thread_states_path(
522                 self.bbs_type.bbs_type, self.bbs_type.board,
523                 self.bbs_type.thread)
524             dat_path = misc.get_thread_dat_path(
525                 self.bbs_type.bbs_type, self.bbs_type.board,
526                 self.bbs_type.thread)
527
528             # save only if dat file exists.
529             if os.path.exists(dat_path):
530                 window_width, window_height = self.window.get_size()
531                 toolbar_visible = self.toolbar.parent.get_property("visible")
532                 statusbar_visible = self.statusbar.get_property("visible")
533
534                 dirname = os.path.dirname(states_path)
535                 if not os.path.isdir(dirname):
536                     os.makedirs(dirname)
537
538                 f = file(states_path, "w")
539
540                 f.write("window_width=" + str(window_width) + "\n")
541                 f.write("window_height=" + str(window_height) + "\n")
542                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
543                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
544
545                 f.close()
546         except:
547             traceback.print_exc()
548
549     def restore(self):
550         try:
551             window_height = 600
552             window_width = 600
553             toolbar_visible = True
554             statusbar_visible = True
555
556             try:
557                 key_base = config.gconf_app_key_base() + "/thread_states"
558                 gconf_client = gconf.client_get_default()
559                 width = gconf_client.get_int(key_base + "/window_width")
560                 height = gconf_client.get_int(key_base + "/window_height")
561                 toolbar_visible = gconf_client.get_bool(
562                     key_base + "/toolbar")
563                 statusbar_visible = gconf_client.get_bool(
564                     key_base + "/statusbar")
565                 if width != 0:
566                     window_width = width
567                 if height != 0:
568                     window_height = height
569             except:
570                 traceback.print_exc()
571
572             states_path = misc.get_thread_states_path(
573                 self.bbs_type.bbs_type, self.bbs_type.board,
574                 self.bbs_type.thread)
575             if os.path.exists(states_path):
576                 for line in file(states_path):
577                     if line.startswith("window_height="):
578                         height = window_height
579                         try:
580                             height = int(
581                                 line[len("window_height="):].rstrip("\n"))
582                         except:
583                             pass
584                         else:
585                             window_height = height
586                     elif line.startswith("window_width="):
587                         width = window_width
588                         try:
589                             width = int(
590                                 line[len("window_width="):].rstrip("\n"))
591                         except:
592                             pass
593                         else:
594                             window_width = width
595                     elif line.startswith("toolbar_visible="):
596                         tbar = line[len("toolbar_visible="):].rstrip("\n")
597                         toolbar_visible = tbar == "True"
598                     elif line.startswith("statusbar_visible="):
599                         sbar = line[len("statusbar_visible="):].rstrip("\n")
600                         statusbar_visible = sbar == "True"
601
602             self.window.set_default_size(window_width, window_height)
603
604             if not toolbar_visible:
605                 gobject.idle_add(self.toolbar.parent.hide)
606             if not statusbar_visible:
607                 gobject.idle_add(self.statusbar.hide)
608         except:
609             traceback.print_exc()