OSDN Git Service

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