OSDN Git Service

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