OSDN Git Service

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