OSDN Git Service

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