OSDN Git Service

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