OSDN Git Service

Popup menu for ThreadView is not prepared by the glade xml. (#16526)
[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 import thread_popup
54 import submit_window
55
56 GLADE_FILENAME = "thread_window.glade"
57
58 def open_thread(uri, update=False):
59     if not uri:
60         raise ValueError, "parameter must not be empty"
61
62     bbs_type = bbs_type_judge_uri.get_type(uri)
63     if not bbs_type.is_thread():
64         raise bbs_type_exception.BbsTypeError, \
65               "the uri does not represent thread: " + uri
66     uri = bbs_type.get_thread_uri()  # use strict thread uri
67
68     winwrap = session.get_window(uri)
69     if winwrap:
70         # already opened
71         winwrap.window.present()
72         if update:
73             winwrap.load(update)
74     else:
75         winwrap = WinWrap(bbs_type.uri)  # pass original uri
76         winwrap.load(update)
77
78     # jump to the res if necessary.
79     strict_uri = bbs_type.get_thread_uri()
80     if (bbs_type.uri != strict_uri and
81         bbs_type.uri.startswith(strict_uri)):
82         resnum = bbs_type.uri[len(strict_uri):]
83         match = re.match("\d+", resnum)
84         if match:
85             resnum = int(match.group())
86             winwrap.jump_to_res(resnum)
87
88
89 class HTMLParserToThreadView:
90     def __init__(self, threadview, resnum, left_margin):
91         self.threadview = threadview
92         self.resnum = resnum
93         self.left_margin = left_margin
94         self.initialize()
95
96     def set_left_margin(self, left_margin):
97         self.left_margin = left_margin
98
99     def initialize(self):
100         self.layout = None
101
102     def on_new_line(self):
103         self.to_thread_view()
104         self.layout = self.threadview.create_res_layout(
105             self.left_margin, self.resnum)
106
107     def from_html_parser(self, data, bold, href):
108         if self.layout == None:
109             self.layout = self.threadview.create_res_layout(
110                 self.left_margin, self.resnum)
111
112         gtk.gdk.threads_enter()
113         self.layout.add_text(data, bold, href)
114         gtk.gdk.threads_leave()
115
116     def to_thread_view(self):
117         if self.layout is not None:
118             # gobject.idle_add(self.threadview.add_layout, self.layout)
119             gtk.gdk.threads_enter()
120             self.threadview.add_layout(self.layout)
121             gtk.gdk.threads_leave()
122             self.initialize()
123
124
125 class WinWrap(winwrapbase.WinWrapBase):
126     hovering_over_link = False
127     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
128     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
129
130     def __init__(self, uri):
131         from BbsType import bbs_type_judge_uri
132         from BbsType import bbs_type_exception
133         self.bbs_type = bbs_type_judge_uri.get_type(uri)
134         if not self.bbs_type.is_thread():
135             raise bbs_type_exception.BbsTypeError, \
136                   "the uri does not represent thread: " + uri
137         self.size = 0
138         self.num = 0
139         self.title = ""
140         self.lock_obj = False
141         self.jump_request_num = 0
142         self.progress = False
143
144         glade_path = os.path.join(config.glade_dir, GLADE_FILENAME)
145         self.widget_tree = gtk.glade.XML(glade_path)
146         self._get_widgets()
147         self.widget_tree.signal_autoconnect(self)
148
149         self.toolbar.unset_style()
150
151         self.threadview = thread_view.ThreadView()
152         container = thread_view.ThreadViewContainer(self.threadview)
153         self.threadpopup = thread_popup.ThreadPopup(self.bbs_type)
154         self.threadpopup.push_thread_view(self.threadview)
155         self.vbox.pack_start(container)
156         self.vbox.reorder_child(container, 2)
157         self.window.set_focus(self.threadview)
158
159         self.threadview.connect(
160             "populate-popup", self.on_thread_view_populate_popup)
161         self.threadview.connect(
162             "uri-clicked-event", self.on_thread_view_uri_clicked)
163         self.threadpopup.connect(
164             "uri-clicked-event", self.on_thread_popup_uri_clicked)
165
166         self.statusbar_context_id = self.statusbar.get_context_id(
167             "Thread Window Status")
168         self.statusbar.push(self.statusbar_context_id, "OK.")
169
170         self.initialize_buffer()
171
172         self.restore()
173         self.window.show_all()
174
175         self.created()
176
177     def _get_widgets(self):
178         self.window = self.widget_tree.get_widget("thread_window")
179         self.toolbar = self.widget_tree.get_widget("toolbar")
180         self.statusbar = self.widget_tree.get_widget("statusbar")
181         self.vbox = self.widget_tree.get_widget("vbox")
182
183     def initialize_buffer(self):
184         self.threadview.initialize_buffer()
185
186     def destroy(self):
187         self.save()
188         self.window.destroy()
189
190     def get_uri(self):
191         return self.bbs_type.get_thread_uri()
192
193     def show(self):
194         self.window.deiconify()
195
196     def hide(self):
197         self.window.iconify()
198
199     def _show_submit_window(self):
200         submit_window.open(self.bbs_type.get_thread_uri())
201
202     def _toggle_toolbar(self):
203         if self.toolbar.get_property("visible"):
204             self.toolbar.hide()
205         else:
206             self.toolbar.show()
207
208     def _toggle_statusbar(self):
209         if self.statusbar.get_property("visible"):
210             self.statusbar.hide()
211         else:
212             self.statusbar.show()
213
214     def _close_window(self):
215         self.destroy()
216
217     def _quit_session(self):
218         session.main_quit()
219
220     def _regist_as_bookmark(self):
221         bookmark_list.bookmark_list.add_bookmark_with_edit(
222             name=self.title, uri=self.bbs_type.uri)
223
224     def _manage_bookmarks(self):
225         bookmark_window.open()
226
227     def _show_board(self):
228         board_window.open_board(self.bbs_type.get_uri_base())
229
230     def _delete_log(self):
231         try:
232             dat_path = misc.get_thread_dat_path(self.bbs_type)
233             os.remove(dat_path)
234         except OSError:
235             traceback.print_exc()
236         try:
237             idx_path = misc.get_thread_idx_path(self.bbs_type)
238             os.remove(idx_path)
239         except OSError:
240             traceback.print_exc()
241         try:
242             states_path = misc.get_thread_states_path(self.bbs_type)
243             os.remove(states_path)
244         except OSError:
245             traceback.print_exc()
246
247     def _open_uri(self, uri):
248         if not uri.startswith("http://"):
249             # maybe a relative uri.
250             uri = urlparse.urljoin(self.bbs_type.get_uri_base(), uri)
251
252         try:
253             uri_opener.open_uri(uri)
254         except bbs_type_exception.BbsTypeError:
255             # not supported, show with the web browser.
256             gnome.url_show(uri)
257
258     def _copy_text_to_clipboard(self, text):
259         if text and len(text) > 0:
260             clip = gtk.Clipboard()
261             text = text.encode("utf8")
262             clip.set_text(text, len(text))
263
264     def _modify_uri(self, uri):
265         if not uri.startswith("http://"):
266             uri = "http://" + uri
267         return uri
268
269     def http_get_dat(self, on_get_res):
270         datfile_url = self.bbs_type.get_dat_uri()
271
272         idx_dic = idxfile.load_idx(self.bbs_type)
273         lastmod = idx_dic["lastModified"]
274         etag = idx_dic["etag"]
275
276         req = urllib2.Request(datfile_url)
277         req.add_header("User-agent", config.User_Agent)
278         if self.size > 0:
279             req.add_header("Range", "bytes=" + str(self.size) + "-")
280         if lastmod:
281             req.add_header("If-Modified-Since", lastmod)
282         if etag:
283             req.add_header("If-None-Match", etag)
284
285         def push():
286             self.statusbar.pop(self.statusbar_context_id)
287             self.statusbar.push(self.statusbar_context_id, "GET...")
288         gobject.idle_add(push)
289
290         req = self.bbs_type.set_extra_dat_request(req, self)
291
292         opener = urllib2.build_opener(HTTPRedirectHandler302, HTTPDebugHandler)
293         try:
294             res = opener.open(req)
295         except urllib2.HTTPError, e:
296             def push(code, msg):
297                 message = "%d %s" % (code, msg)
298                 self.statusbar.pop(self.statusbar_context_id)
299                 self.statusbar.push(self.statusbar_context_id, message)
300             gobject.idle_add(push, e.code, e.msg)
301         else:
302             headers = res.info()
303
304             if "Last-Modified" in headers:
305                 la = headers["Last-Modified"]
306                 def push(code, msg, lastm):
307                     message = "%d %s [%s]" % (code, msg, lastm)
308                     self.statusbar.pop(self.statusbar_context_id)
309                     self.statusbar.push(self.statusbar_context_id, message)
310                 gobject.idle_add(push, res.code, res.msg, la)
311             else:
312                 def push(code, msg):
313                     message = "%d %s" % (code, msg)
314                     self.statusbar.pop(self.statusbar_context_id)
315                     self.statusbar.push(self.statusbar_context_id, message)
316                 gobject.idle_add(push, res.code, res.msg)
317
318             maybe_incomplete = False
319             for line in res:
320                 if not line.endswith("\n"):
321                     maybe_incomplete = True
322                     print "does not end with \\n. maybe incomplete"
323                     break
324                 on_get_res(line)
325
326             res.close()
327
328             if maybe_incomplete:
329                 lastmod = None
330                 etag = None
331             else:
332                 if "Last-Modified" in headers:
333                     lastmod = headers["Last-Modified"]
334                 if "ETag" in headers:
335                     etag = headers["Etag"]
336
337             if self.num > 0:
338                 # save idx
339                 idx_dic = {"title": self.title, "lineCount": self.num,
340                        "lastModified": lastmod, "etag": etag}
341                 idxfile.save_idx(self.bbs_type, idx_dic)
342
343                 gobject.idle_add(session.thread_idx_updated,
344                                  self.bbs_type.get_thread_uri(), idx_dic)
345
346     def update(self):
347
348         self.jump_request_num = 0
349
350         def load():
351             line_count = datfile.get_dat_line_count(self.bbs_type)
352             if line_count < self.num:
353                 self.num = 0
354                 self.size = 0
355
356                 gobject.idle_add(self.initialize_buffer)
357
358             if line_count > self.num:
359                 datfile.load_dat_partly(
360                     self.bbs_type, self.append_rawres_to_buffer, self.num+1)
361
362                 def do_jump():
363                     if self.jump_request_num:
364                         if self.jump_request_num <= num:
365                             num = self.jump_request_num
366                             self.jump_request_num = 0
367                             self.jump_to_res(num)
368                     else:
369                         self.jump_to_the_end()
370
371                 gobject.idle_add(do_jump)
372
373         def get():
374             dat_path = misc.get_thread_dat_path(self.bbs_type)
375             dat_file = FileWrap(dat_path)
376
377             def save_line_and_append_to_buffer(line):
378                 dat_file.seek(self.size)
379                 dat_file.write(line)
380                 self.append_rawres_to_buffer(line)
381
382             self.http_get_dat(save_line_and_append_to_buffer)
383             gtk.gdk.threads_enter()
384             self.threadview.redraw()
385             gtk.gdk.threads_leave()
386             dat_file.close()
387
388             def do_jump():
389                 if self.jump_request_num:
390                     num = self.jump_request_num
391                     self.jump_request_num = 0
392                     self.jump_to_res(num)
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             datfile.load_dat(self.bbs_type, self.append_rawres_to_buffer)
414
415         def jump():
416
417             def do_jump():
418                 if self.jump_request_num:
419                     num = self.jump_request_num
420                     self.jump_request_num = 0
421                     self.jump_to_res(num)
422                 else:
423                     self.jump_to_the_end()
424
425             gobject.idle_add(do_jump)
426
427         if self.lock():
428
429             def on_end():
430                 self.un_lock()
431                 self.progress = False
432
433             self.progress = True
434             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
435             t.start()
436
437     def append_rawres_to_buffer(self, line):
438         self.size += len(line)
439         self.num += 1
440
441         if not self.title and self.num == 1:
442             title = self.bbs_type.get_title_from_dat(line)
443             if title:
444                 self.title = title
445                 gobject.idle_add(self.window.set_title, title)
446
447         line = line.decode(self.bbs_type.encoding, "replace")
448         m = self.bbs_type.dat_reg.match(line)
449         if m:
450             name = m.group("name")
451             mail = m.group("mail")
452             date = m.group("date")
453             msg = m.group("msg")
454             try:
455                 num = int(m.group("num"))
456             except IndexError:
457                 # use simple counter num
458                 num = self.num
459             else:
460                 # use num in dat
461                 self.num = num
462             try:
463                 id = m.group("id")
464             except IndexError:
465                 pass
466             else:
467                 if id:
468                     date += " ID:" + id
469             self.reselems_to_buffer(num, name, mail, date, msg)
470         else:
471             self.reselems_to_buffer(
472                 str(self.num), "Invalid Name", "Invalid Mail",
473                 "Invalid Date", line)
474             print "maybe syntax error.", self.num, line
475
476     def reselems_to_buffer(self, num, name, mail, date, msg):
477         pipe = HTMLParserToThreadView(self.threadview, num, 0)
478         p = barehtmlparser.BareHTMLParser(
479             pipe.from_html_parser, pipe.on_new_line)
480
481         # First, create a pango layout for num,name,mail,date
482         # 'margin left' is 0
483         # number
484         p.feed(str(num) + " ")
485
486         # name
487         p.feed("<b>" + name + "</b>")
488
489         # mail
490         p.feed("[" + mail + "]")
491
492         # date
493         p.feed(date)
494         p.flush()
495
496         pipe.to_thread_view()
497
498
499         # Second, create a pango layout for message
500         # 'margin left' is 20
501         # msg
502         pipe.set_left_margin(20)
503         p.feed(msg.lstrip(" "))
504
505         p.feed("<br>")
506         p.close()
507
508         pipe.to_thread_view()
509
510     def jump(self, value):
511         gobject.idle_add(self.threadview.jump, value)
512
513     def jump_to_layout(self, layout):
514         gobject.idle_add(self.threadview.jump_to_layout, layout)
515         
516     def jump_to_the_end(self):
517         gobject.idle_add(self.threadview.jump_to_the_end)
518
519     def lock(self):
520         if self.lock_obj:
521             print "locked, try later."
522             return False
523         else:
524             print "get lock"
525             self.lock_obj = True
526             return True
527
528     def un_lock(self):
529         self.lock_obj = False
530         print "unlock"
531
532     def jump_to_res(self, resnum):
533         if self.threadview.jump_to_res(resnum):
534             return
535         self.jump_request_num = resnum
536
537     def load(self, update=False):
538         dat_path = misc.get_thread_dat_path(self.bbs_type)
539         dat_exists = os.path.exists(dat_path)
540         if update or not dat_exists:
541             self.update()
542         else:
543             self.load_dat()
544
545     def save(self):
546         try:
547             states_path = misc.get_thread_states_path(self.bbs_type)
548             dat_path = misc.get_thread_dat_path(self.bbs_type)
549
550             # save only if dat file exists.
551             if os.path.exists(dat_path):
552                 window_width, window_height = self.window.get_size()
553                 toolbar_visible = self.toolbar.get_property("visible")
554                 statusbar_visible = self.statusbar.get_property("visible")
555
556                 dirname = os.path.dirname(states_path)
557                 if not os.path.isdir(dirname):
558                     os.makedirs(dirname)
559
560                 f = file(states_path, "w")
561
562                 f.write("window_width=" + str(window_width) + "\n")
563                 f.write("window_height=" + str(window_height) + "\n")
564                 f.write("toolbar_visible=" + str(toolbar_visible) + "\n")
565                 f.write("statusbar_visible=" + str(statusbar_visible) + "\n")
566
567                 f.close()
568         except:
569             traceback.print_exc()
570
571     def restore(self):
572         try:
573             window_height = 600
574             window_width = 600
575             toolbar_visible = True
576             statusbar_visible = True
577
578             try:
579                 key_base = config.gconf_app_key_base() + "/thread_states"
580                 gconf_client = gconf.client_get_default()
581                 width = gconf_client.get_int(key_base + "/window_width")
582                 height = gconf_client.get_int(key_base + "/window_height")
583                 toolbar_visible = gconf_client.get_bool(
584                     key_base + "/toolbar")
585                 statusbar_visible = gconf_client.get_bool(
586                     key_base + "/statusbar")
587                 if width != 0:
588                     window_width = width
589                 if height != 0:
590                     window_height = height
591             except:
592                 traceback.print_exc()
593
594             states_path = misc.get_thread_states_path(self.bbs_type)
595             if os.path.exists(states_path):
596                 for line in file(states_path):
597                     if line.startswith("window_height="):
598                         height = window_height
599                         try:
600                             height = int(
601                                 line[len("window_height="):].rstrip("\n"))
602                         except:
603                             pass
604                         else:
605                             window_height = height
606                     elif line.startswith("window_width="):
607                         width = window_width
608                         try:
609                             width = int(
610                                 line[len("window_width="):].rstrip("\n"))
611                         except:
612                             pass
613                         else:
614                             window_width = width
615                     elif line.startswith("toolbar_visible="):
616                         tbar = line[len("toolbar_visible="):].rstrip("\n")
617                         toolbar_visible = tbar == "True"
618                     elif line.startswith("statusbar_visible="):
619                         sbar = line[len("statusbar_visible="):].rstrip("\n")
620                         statusbar_visible = sbar == "True"
621
622             self.window.set_default_size(window_width, window_height)
623
624             if not toolbar_visible:
625                 gobject.idle_add(self.toolbar.hide,
626                                  priority=gobject.PRIORITY_HIGH)
627             if not statusbar_visible:
628                 gobject.idle_add(self.statusbar.hide,
629                                  priority=gobject.PRIORITY_HIGH)
630         except:
631             traceback.print_exc()
632
633
634     # signal handlers
635     
636     def on_thread_view_populate_popup(self, widget, menu):
637         menuitem = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
638         menuitem.connect("activate",
639                          self.on_popup_threadview_menu_refresh_activate)
640         menu.append(menuitem)
641
642     def on_thread_view_uri_clicked(self, widget, uri):
643         self._open_uri(uri)
644
645     def on_thread_popup_uri_clicked(self, widget, threadview, uri):
646         self._open_uri(uri)
647
648     def on_thread_window_delete_event(self, widget, event):
649         self.save()
650         return False
651
652     def on_thread_window_destroy(self, widget):
653         self.destroyed()
654
655
656
657
658     # menu commands
659
660     # menu file
661
662     def on_menu_file_show_board_activate(self, widget):
663         self._show_board()
664
665     def on_menu_file_compose_activate(self, widget):
666         self._show_submit_window()
667
668     def on_menu_file_delete_activate(self, widget):
669         self._delete_log()
670
671     def on_menu_file_close_activate(self, widget):
672         self._close_window()
673
674     def on_menu_file_quit_activate(self, widget):
675         self._quit_session()
676
677     # menu view
678     
679     def on_menu_view_refresh_activate(self, widget):
680         self.update()
681
682     def on_menu_view_toolbar_activate(self, widget):
683         self._toggle_toolbar()
684
685     def on_menu_view_statusbar_activate(self, widget):
686         self._toggle_statusbar()
687
688     # menu bookmarks
689
690     def on_menu_bookmarks_bookmarkthispage_activate(self, widget):
691         self._regist_as_bookmark()
692
693     def on_menu_bookmarks_showbookmarks_activate(self, widget):
694         self._manage_bookmarks()
695
696     # toolbuttons
697     
698     def on_toolbutton_refresh_activate(self, widget):
699         self.update()
700
701     def on_toolbutton_showboard_activate(self, widget):
702         self._show_board()
703
704     def on_toolbutton_compose_activate(self, widget):
705         self._show_submit_window()
706
707     def on_toolbutton_delete_activate(self, widget):
708         self._delete_log()
709
710     # popup menus
711     
712     def on_popup_threadview_menu_refresh_activate(self, widget):
713         self.update()