OSDN Git Service

For install.
[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 itertools
34 import os
35 import sys
36
37 import misc
38 from misc import FileWrap, ThreadInvoker
39 import datfile
40 import barehtmlparser
41 import idxfile
42 import session
43 import board_window
44 import uri_opener
45 from http_sub import HTTPRedirectHandler302, HTTPDebugHandler
46 from BbsType import bbs_type_judge_uri
47 from BbsType import bbs_type_exception
48 import config
49 import winwrapbase
50 import bookmark_list
51 import bookmark_window
52
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(config.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         self.hint = HintWrap()
111
112         sigdic = {"on_refresh_activate": self.update,
113                   "on_compose_activate": self.on_compose_clicked,
114                   "on_toolbar_activate": self.on_toolbar_activate,
115                   "on_statusbar_activate": self.on_statusbar_activate,
116                   "on_refresh_activate": self.update,
117                   "on_close_activate": self.on_close_activate,
118                   "on_quit_activate": self.on_quit_activate,
119                   "on_show_board_activate": self.on_show_board_activate,
120                   "on_delete_activate": self.on_delete_activate,
121                   "on_thread_window_delete_event":
122                   self.on_thread_window_delete_event,
123                   "on_add_bookmark_activate": self.on_add_bookmark_activate,
124                   "on_manage_bookmarks_activate": \
125                   self.on_manage_bookmarks_activate,
126                   "on_thread_window_destroy": self.on_thread_window_destroy}
127         self.widget_tree.signal_autoconnect(sigdic)
128
129         self.textview.connect("event-after", self.on_event_after)
130         self.textview.connect("motion-notify-event",
131                               self.on_motion_notify_event)
132         self.textview.connect("visibility-notify-event",
133                               self.on_visibility_notify_event)
134         self.restore()
135         self.window.show()
136
137         self.created()
138
139     def initialize_buffer(self):
140         self.textbuffer = gtk.TextBuffer()
141         self.textview.set_buffer(self.textbuffer)
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     def destroy(self):
148         self.save()
149         self.window.destroy()
150
151     def get_uri(self):
152         return self.bbs_type.get_thread_uri()
153
154     def on_compose_clicked(self, widget):
155         import submit_window
156         submit_window.open(self.bbs_type.get_thread_uri())
157
158     def on_toolbar_activate(self, widget):
159         if self.toolbar.parent.get_property("visible"):
160             self.toolbar.parent.hide()
161         else:
162             self.toolbar.parent.show()
163
164     def on_statusbar_activate(self, widget):
165         if self.statusbar.get_property("visible"):
166             self.statusbar.hide()
167         else:
168             self.statusbar.show()
169
170     def on_event_after(self, widget, event):
171         if event.type != gtk.gdk.BUTTON_RELEASE:
172             return False
173         if event.button != 1:
174             return False
175         buffer = widget.get_buffer()
176
177         try:
178             start, end = buffer.get_selection_bounds()
179         except ValueError:
180             pass
181         else:
182             if start.get_offset() != end.get_offset():
183                 return False
184
185         x, y = widget.window_to_buffer_coords(
186             gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
187         iter = widget.get_iter_at_location(x, y)
188         if not iter.has_tag(self.leftmargintag) or x > 20:
189             tags = iter.get_tags()
190             for tag in tags:
191                 href = tag.get_data("href")
192                 if href:
193                     self.on_link_clicked(widget, href)
194         return False
195
196     def on_link_clicked(self, widget, href):
197
198         if not href.startswith("http://"):
199             # maybe a relative uri.
200             href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
201
202         try:
203             uri_opener.open_uri(href)
204         except bbs_type_exception.BbsTypeError:
205             # not supported, show with the web browser.
206             gnome.url_show(href)
207
208     def on_motion_notify_event(self, widget, event):
209         x, y = widget.window_to_buffer_coords(
210             gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
211         self.set_cursor_if_appropriate(widget, x, y)
212         widget.window.get_pointer()
213         return False
214
215     def on_visibility_notify_event(self, widget, event):
216         wx, wy, mod = widget.window.get_pointer()
217         bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
218
219         self.set_cursor_if_appropriate(widget, bx, by)
220         return False
221
222     def set_cursor_if_appropriate(self, widget, x, y):
223         hovering = False
224         href = ""
225
226         buffer = widget.get_buffer()
227         iter = widget.get_iter_at_location(x, y)
228         if not iter.has_tag(self.leftmargintag) or x > 20:
229             tags = iter.get_tags()
230             for tag in tags:
231                 href = tag.get_data("href")
232                 if href:
233                     hovering = True
234
235         if hovering != self.hovering_over_link:
236             self.hovering_over_link = hovering
237
238         if self.hovering_over_link:
239             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
240                 self.hand_cursor)
241             if href:
242                 if not href.startswith("http://"):
243                     href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
244
245                 strict_uri = self.bbs_type.get_thread_uri()
246                 if href != strict_uri and href.startswith(strict_uri):
247                     resnum = href[len(strict_uri):]
248                     match = re.match("\d+", resnum)
249                     if match:
250                         resnum = int(match.group())
251                         if not self.hint.visible(resnum):
252                             mark = self.textbuffer.get_mark(str(resnum))
253                             n_mark = self.textbuffer.get_mark(str(resnum+1))
254                             if mark and n_mark:
255                                 iter = self.textbuffer.get_iter_at_mark(mark)
256                                 n_iter = self.textbuffer.get_iter_at_mark(
257                                     n_mark)
258                                 text = self.textbuffer.get_text(
259                                     iter, n_iter, False)
260                                 if text:
261                                     iter = self.textview.get_iter_at_location(
262                                         x, y)
263                                     rect = self.textview.get_iter_location(
264                                         iter)
265                                     x, y = \
266                                        self.textview.buffer_to_window_coords(
267                                         gtk.TEXT_WINDOW_WIDGET, rect.x, rect.y)
268                                     x, y = self.textview.translate_coordinates(
269                                         self.window, x, y)
270                                     wx, wy = self.window.get_position()
271                                     x += wx
272                                     y += wy
273                                     y += rect.height
274                                     self.hint.show(x, y, text, resnum)
275         else:
276             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
277                 self.regular_cursor)
278             self.hint.destroy()
279
280     def on_close_activate(self, widget):
281         self.destroy()
282
283     def on_thread_window_delete_event(self, widget, event):
284         self.save()
285         return False
286         
287     def on_thread_window_destroy(self, widget):
288         self.destroyed()
289
290     def on_quit_activate(self, widget):
291         session.main_quit()
292
293     def on_add_bookmark_activate(self, widget):
294         bookmark_list.bookmark_list.add_bookmark_with_edit(
295             name=self.title, uri=self.bbs_type.uri)
296
297     def on_manage_bookmarks_activate(self, widget):
298         bookmark_window.open()
299
300     def on_show_board_activate(self, widget):
301         board_window.open_board(self.bbs_type.get_uri_base())
302
303     def on_delete_activate(self, widget):
304         try:
305             dat_path = misc.get_thread_dat_path(self.bbs_type)
306             os.remove(dat_path)
307         except OSError:
308             traceback.print_exc()
309         try:
310             idx_path = misc.get_thread_idx_path(self.bbs_type)
311             os.remove(idx_path)
312         except OSError:
313             traceback.print_exc()
314         try:
315             states_path = misc.get_thread_states_path(self.bbs_type)
316             os.remove(states_path)
317         except OSError:
318             traceback.print_exc()
319
320     def http_get_dat(self, on_get_res):
321         datfile_url = self.bbs_type.get_dat_uri()
322
323         idx_dic = idxfile.load_idx(self.bbs_type)
324         lastmod = idx_dic["lastModified"]
325         etag = idx_dic["etag"]
326
327         req = urllib2.Request(datfile_url)
328         req.add_header("User-agent", config.User_Agent)
329         if self.size > 0:
330             req.add_header("Range", "bytes=" + str(self.size) + "-")
331         if lastmod:
332             req.add_header("If-Modified-Since", lastmod)
333         if etag:
334             req.add_header("If-None-Match", etag)
335
336         req = self.bbs_type.set_extra_dat_request(req, self)
337
338         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
339         try:
340             res = opener.open(req)
341         except urllib2.HTTPError, e:
342             gobject.idle_add(
343                 self.statusbar.set_status, "%d %s" % (e.code, e.msg))
344         else:
345             headers = res.info()
346             gobject.idle_add(
347                 self.statusbar.set_status, "%d %s" % (res.code, res.msg))
348
349             maybe_incomplete = False
350             for line in res:
351                 if not line.endswith("\n"):
352                     maybe_incomplete = True
353                     print "does not end with \\n. maybe incomplete"
354                     break
355                 on_get_res(line)
356
357             res.close()
358
359             if maybe_incomplete:
360                 lastmod = None
361                 etag = None
362             else:
363                 if "Last-Modified" in headers:
364                     lastmod = headers["Last-Modified"]
365                 if "ETag" in headers:
366                     etag = headers["Etag"]
367
368             if self.num > 0:
369                 # save idx
370                 idx_dic = {"title": self.title, "lineCount": self.num,
371                        "lastModified": lastmod, "etag": etag}
372                 idxfile.save_idx(self.bbs_type, idx_dic)
373
374                 gobject.idle_add(session.thread_idx_updated,
375                                  self.bbs_type.get_thread_uri(), idx_dic)
376
377     def update(self, widget=None):
378
379         self.jump_request_num = 0
380
381         def load():
382             if self.num == 0:
383                 def create_mark():
384                     self.textbuffer.create_mark("1", self.enditer, True)
385                 gobject.idle_add(create_mark)
386
387             line_count = datfile.get_dat_line_count(self.bbs_type)
388             if line_count < self.num:
389                 self.num = 0
390                 self.size = 0
391
392                 gobject.idle_add(self.initialize_buffer)
393
394             if line_count > self.num:
395                 datfile.load_dat_partly(
396                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
397
398                 def do_jump(num):
399                     if self.jump_request_num:
400                         if self.jump_request_num <= num:
401                             # jump if enable, otherwize jump later.
402                             num = self.jump_request_num
403                             self.jump_request_num = 0
404                             mark = self.textbuffer.get_mark(str(num))
405                             if mark:
406                                 self.textview.scroll_to_mark(
407                                     mark, 0, True, 0, 0)
408                     else:
409                         self.jump_to_the_end(num)
410
411                 gobject.idle_add(do_jump, self.num)
412
413         def get():
414             dat_path = misc.get_thread_dat_path(self.bbs_type)
415             dat_file = FileWrap(dat_path)
416
417             def save_line_and_append_to_buffer(line):
418                 dat_file.seek(self.size)
419                 dat_file.write(line)
420                 self.append_rawres_to_buffer(line)
421
422             self.http_get_dat(save_line_and_append_to_buffer)
423             dat_file.close()
424
425             def do_jump():
426                 if self.jump_request_num:
427                     num = self.jump_request_num
428                     self.jump_request_num = 0
429                     mark = self.textbuffer.get_mark(str(num))
430                     if mark:
431                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
432
433             gobject.idle_add(do_jump)
434
435         if self.lock():
436
437             def on_end():
438                 self.un_lock()
439                 self.progress = False
440
441             self.progress = True
442             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
443             t.start()
444
445     def load_dat(self):
446
447         self.size = 0
448         self.num = 0
449         self.jump_request_num = 0
450
451         def load():
452
453             def create_mark():
454                 self.textbuffer.create_mark("1", self.enditer, True)
455             gobject.idle_add(create_mark)
456
457             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
458         def jump():
459
460             def do_jump(num):
461                 if self.jump_request_num:
462                     num = self.jump_request_num
463                     self.jump_request_num = 0
464                     mark = self.textbuffer.get_mark(str(num))
465                     if mark:
466                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
467                 else:
468                     self.jump_to_the_end(num)
469
470             gobject.idle_add(do_jump, self.num)
471
472         if self.lock():
473
474             def on_end():
475                 self.un_lock()
476                 self.progress = False
477
478             self.progress = True
479             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
480             t.start()
481
482     def append_rawres_to_buffer(self, line):
483         self.size += len(line)
484         self.num += 1
485
486         if not self.title and self.num == 1:
487             title = self.bbs_type.get_title_from_dat(line)
488             if title:
489                 self.title = title
490                 gobject.idle_add(self.window.set_title, title)
491
492         self.res_queue = []
493
494         line = line.decode(self.bbs_type.encoding, "replace")
495         m = self.bbs_type.dat_reg.match(line)
496         if m:
497             name = m.group("name")
498             mail = m.group("mail")
499             date = m.group("date")
500             msg = m.group("msg")
501             try:
502                 num = int(m.group("num"))
503             except IndexError:
504                 # use simple counter num
505                 num = self.num
506             else:
507                 # use num in dat
508                 self.num = num
509             try:
510                 id = m.group("id")
511             except IndexError:
512                 pass
513             else:
514                 if id:
515                     date += " ID:" + id
516             self.reselems_to_buffer(num, name, mail, date, msg)
517         else:
518             self.res_queue.append((str(self.num)+"\n", False, None, False))
519             self.res_queue.append((line, False, None, True))
520             print "maybe syntax error.", self.num, line
521
522         def process_res_queue(res_queue, num):
523             self.process_queue(res_queue)
524             # for next res
525             self.textbuffer.create_mark(str(num+1), self.enditer, True)
526
527         gobject.idle_add(
528             process_res_queue, self.res_queue, self.num)
529
530     def reselems_to_buffer(self, num, name, mail, date, msg):
531         p = barehtmlparser.BareHTMLParser(
532             lambda d,b,h: self.res_queue.append((d,b,h,False)))
533         # number
534         p.feed(str(num) + " ")
535
536         # name
537         p.feed("<b>" + name + "</b>")
538
539         # mail
540         p.feed("[" + mail + "]")
541
542         # date
543         p.feed(date)
544         p.feed("<br>")
545
546         # msg
547         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
548         p.feed(msg.lstrip(" "))
549
550         p.feed("<br><br>")
551         p.close()
552
553     def href_tag(self, href):
554         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
555         tag.set_data("href", href)
556         return tag
557
558     def process_queue(self, queue):
559         for data, bold, href, margin in queue:
560             taglist = []
561             if bold:
562                 taglist.append(self.boldtag)
563             if href:
564                 taglist.append(self.href_tag(href))
565             if margin:
566                 taglist.append(self.leftmargintag)
567
568             if taglist:
569                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
570             else:
571                 self.textbuffer.insert(self.enditer, data)
572
573     def jump_to_the_end(self, num):
574         mark = self.textbuffer.get_mark(str(num+1))
575         if mark:
576             self.textview.scroll_to_mark(mark, 0)
577
578     def lock(self):
579         if self.lock_obj:
580             print "locked, try later."
581             return False
582         else:
583             print "get lock"
584             self.lock_obj = True
585             return True
586
587     def un_lock(self):
588         self.lock_obj = False
589         print "unlock"
590
591     def jump_to_res(self, uri):
592         strict_uri = self.bbs_type.get_thread_uri()
593         if uri != strict_uri and uri.startswith(strict_uri):
594             resnum = uri[len(strict_uri):]
595             match = re.match("\d+", resnum)
596             if match:
597                 resnum = match.group()
598                 mark = self.textbuffer.get_mark(resnum)
599                 if mark:
600                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
601                 elif self.progress:
602                     # try later.
603                     self.jump_request_num = int(resnum)
604
605     def load(self, update=False):
606         dat_path = misc.get_thread_dat_path(self.bbs_type)
607         dat_exists = os.path.exists(dat_path)
608         if update or not dat_exists:
609             self.update()
610         else:
611             self.load_dat()
612
613     def save(self):
614         try:
615             states_path = misc.get_thread_states_path(self.bbs_type)
616             dat_path = misc.get_thread_dat_path(self.bbs_type)
617
618             # save only if dat file exists.
619             if os.path.exists(dat_path):
620                 window_width, window_height = self.window.get_size()
621                 toolbar_visible = self.toolbar.parent.get_property("visible")
622                 statusbar_visible = self.statusbar.get_property("visible")
623
624                 dirname = os.path.dirname(states_path)
625                 if not os.path.isdir(dirname):
626                     os.makedirs(dirname)
627
628                 f = file(states_path, "w")
629
630                 f.write("window_width=" + str(window_width) + "\n")
631                 f.write("window_height=" + str(window_height) + "\n")
632                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
633                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
634
635                 f.close()
636         except:
637             traceback.print_exc()
638
639     def restore(self):
640         try:
641             window_height = 600
642             window_width = 600
643             toolbar_visible = True
644             statusbar_visible = True
645
646             try:
647                 key_base = config.gconf_app_key_base() + "/thread_states"
648                 gconf_client = gconf.client_get_default()
649                 width = gconf_client.get_int(key_base + "/window_width")
650                 height = gconf_client.get_int(key_base + "/window_height")
651                 toolbar_visible = gconf_client.get_bool(
652                     key_base + "/toolbar")
653                 statusbar_visible = gconf_client.get_bool(
654                     key_base + "/statusbar")
655                 if width != 0:
656                     window_width = width
657                 if height != 0:
658                     window_height = height
659             except:
660                 traceback.print_exc()
661
662             states_path = misc.get_thread_states_path(self.bbs_type)
663             if os.path.exists(states_path):
664                 for line in file(states_path):
665                     if line.startswith("window_height="):
666                         height = window_height
667                         try:
668                             height = int(
669                                 line[len("window_height="):].rstrip("\n"))
670                         except:
671                             pass
672                         else:
673                             window_height = height
674                     elif line.startswith("window_width="):
675                         width = window_width
676                         try:
677                             width = int(
678                                 line[len("window_width="):].rstrip("\n"))
679                         except:
680                             pass
681                         else:
682                             window_width = width
683                     elif line.startswith("toolbar_visible="):
684                         tbar = line[len("toolbar_visible="):].rstrip("\n")
685                         toolbar_visible = tbar == "True"
686                     elif line.startswith("statusbar_visible="):
687                         sbar = line[len("statusbar_visible="):].rstrip("\n")
688                         statusbar_visible = sbar == "True"
689
690             self.window.set_default_size(window_width, window_height)
691
692             if not toolbar_visible:
693                 gobject.idle_add(self.toolbar.parent.hide,
694                                  priority=gobject.PRIORITY_HIGH)
695             if not statusbar_visible:
696                 gobject.idle_add(self.statusbar.hide,
697                                  priority=gobject.PRIORITY_HIGH)
698         except:
699             traceback.print_exc()
700
701
702 class HintWrap:
703
704     def __init__(self):
705         self.window = None
706         self.textview = None
707         self.nums = None
708
709     def __del__(self):
710         print "destruct"
711         self.destroy()
712
713     def destroy(self):
714         if self.window:
715             self.window.destroy()
716         self.window = None
717         self.textview = None
718         self.nums = None
719
720     def show(self, x, y, text, *nums):
721         self.destroy()
722
723         self.window = gtk.Window(gtk.WINDOW_POPUP)
724         self.window.set_default_size(400, 10)
725         self.window.move(x, y)
726
727         self.textview = gtk.TextView()
728         self.window.add(self.textview)
729
730         self.textview.set_wrap_mode(gtk.WRAP_CHAR)
731         self.textview.set_editable(False)
732
733         buffer = self.textview.get_buffer()
734         buffer.set_text(text.rstrip())
735         self.nums = nums
736
737         self.window.show_all()
738
739     def visible(self, *nums):
740         if not self.nums or len(self.nums) != len(nums):
741             return False
742
743         for num, mun in itertools.izip(self.nums, nums):
744             if num != mun:
745                 return False
746         return True