OSDN Git Service

Gui improvement. save window states.
[fukui-no-namari/fukui-no-namari.git] / src / Hage1 / 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
33 import misc
34 import datfile
35 import barehtmlparser
36 import idxfile
37 import session
38 import board_window
39 import uri_opener
40 from http_sub import HTTPRedirectHandler302
41 from BbsType import bbs_type_judge_uri
42 from BbsType import bbs_type_exception
43 import config
44
45 GLADE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
46                          "..", "data")
47 GLADE_FILENAME = "thread_window.glade"
48
49 def open_thread(uri, update=False):
50     if not uri:
51         raise ValueError, "parameter must not be empty"
52
53     bbs_type = bbs_type_judge_uri.get_type(uri)
54     if not bbs_type.is_thread():
55         raise bbs_type_exception.BbsTypeError, \
56               "the uri does not represent thread: " + uri
57     uri = bbs_type.get_thread_uri()  # use strict thread uri
58
59     winwrap = session.get_window(uri)
60     if winwrap:
61         # already opened
62         winwrap.window.present()
63         if update:
64             winwrap.load(update)
65     else:
66         winwrap = WinWrap(bbs_type.uri)  # pass original uri
67         session.window_created(uri, winwrap)
68         winwrap.load(update)
69
70     # jump to the res if necessary.
71     winwrap.jump_to_res(bbs_type.uri)
72
73
74 class ThreadInvoker(threading.Thread):
75     def __init__(self, on_end, *methods):
76         super(ThreadInvoker, self).__init__()
77         self.on_end = on_end
78         self.methods = methods
79     def run(self):
80         try:
81             for m in self.methods:
82                 m()
83         finally:
84             self.on_end()
85
86
87 class FileWrap:
88     def __init__(self, path):
89         self._file = None
90         self._path = path
91     def __del__(self):
92         self.close()
93     def seek(self, size):
94         self.file().seek(size)
95     def write(self, data):
96         self.file().write(data)
97     def close(self):
98         if self._file:
99             self._file.close()
100             self._file = None
101     def file(self):
102         if not self._file:
103             basedir = os.path.dirname(self._path)
104             if not os.path.isdir(basedir):
105                 os.makedirs(basedir)
106             self._file = file(self._path, "a+")
107         return self._file
108
109
110 class WinWrap:
111     hovering_over_link = False
112     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
113     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
114
115
116     def __init__(self, uri):
117         from BbsType import bbs_type_judge_uri
118         from BbsType import bbs_type_exception
119         self.bbs_type = bbs_type_judge_uri.get_type(uri)
120         if not self.bbs_type.is_thread():
121             raise bbs_type_exception.BbsTypeError, \
122                   "the uri does not represent thread: " + uri
123         self.bbs = self.bbs_type.bbs_type
124         self.board = self.bbs_type.board
125         self.thread = self.bbs_type.thread
126         self.host = self.bbs_type.host
127         self.uri = self.bbs_type.uri
128         self.size = 0
129         self.num = 0
130         self.title = ""
131         self.lock_obj = False
132         self.jump_request_num = 0
133         self.progress = False
134
135         glade_path = os.path.join(GLADE_DIR, GLADE_FILENAME)
136         self.widget_tree = gtk.glade.XML(glade_path)
137         self.window = self.widget_tree.get_widget("thread_window")
138         self.toolbar = self.widget_tree.get_widget("toolbar")
139         self.statusbar = self.widget_tree.get_widget("appbar")
140         self.textview = self.widget_tree.get_widget("textview")
141         self.textbuffer = self.textview.get_buffer()
142         self.enditer = self.textbuffer.get_end_iter()
143         self.boldtag = self.textbuffer.create_tag(weight=pango.WEIGHT_BOLD)
144         self.leftmargintag = self.textbuffer.create_tag()
145         self.leftmargintag.set_property("left-margin", 20)
146
147         sigdic = {"on_refresh_activate": self.update,
148                   "on_compose_activate": self.on_compose_clicked,
149                   "on_toolbar_activate": self.on_toolbar_activate,
150                   "on_statusbar_activate": self.on_statusbar_activate,
151                   "on_refresh_activate": self.update,
152                   "on_close_activate": self.on_close_activate,
153                   "on_quit_activate": self.on_quit_activate,
154                   "on_show_board_activate": self.on_show_board_activate,
155                   "on_thread_window_delete_event":
156                   self.on_thread_window_delete_event,
157                   "on_thread_window_destroy": self.on_thread_window_destroy}
158         self.widget_tree.signal_autoconnect(sigdic)
159
160         self.textview.connect("event-after", self.on_event_after)
161         self.textview.connect("motion-notify-event",
162                               self.on_motion_notify_event)
163         self.textview.connect("visibility-notify-event",
164                               self.on_visibility_notify_event)
165
166         self.gconf_client = gconf.client_get_default()
167         self.gconf_key_base = "/apps/" + config.APPNAME.lower() + \
168                               "/thread_states/"
169
170         width = self.gconf_client.get_int(
171             self.gconf_key_base + "window_width")
172         height = self.gconf_client.get_int(
173             self.gconf_key_base + "window_height")
174         self.window.resize(width, height)
175
176         if not self.gconf_client.get_bool(self.gconf_key_base + "toolbar"):
177             self.toolbar.parent.hide()
178         if not self.gconf_client.get_bool(self.gconf_key_base + "statusbar"):
179             self.statusbar.hide()
180
181     def on_compose_clicked(self, widget):
182         import submit_window
183         submit_window.open(self.bbs_type.get_thread_uri())
184
185     def on_toolbar_activate(self, widget):
186         if self.toolbar.parent.get_property("visible"):
187             self.toolbar.parent.hide()
188             self.gconf_client.set_bool(self.gconf_key_base + "toolbar", False)
189         else:
190             self.toolbar.parent.show()
191             self.gconf_client.set_bool(self.gconf_key_base + "toolbar", True)
192
193     def on_statusbar_activate(self, widget):
194         if self.statusbar.get_property("visible"):
195             self.statusbar.hide()
196             self.gconf_client.set_bool(self.gconf_key_base+"statusbar", False)
197         else:
198             self.statusbar.show()
199             self.gconf_client.set_bool(self.gconf_key_base + "statusbar", True)
200
201     def on_event_after(self, widget, event):
202         if event.type != gtk.gdk.BUTTON_RELEASE:
203             return False
204         if event.button != 1:
205             return False
206         buffer = widget.get_buffer()
207
208         try:
209             start, end = buffer.get_selection_bounds()
210         except ValueError:
211             pass
212         else:
213             if start.get_offset() != end.get_offset():
214                 return False
215
216         x, y = widget.window_to_buffer_coords(
217             gtk.TEXT_WINDOW_WIDGET, int (event.x), int(event.y))
218         iter = widget.get_iter_at_location(x, y)
219         if not iter.has_tag(self.leftmargintag) or x > 20:
220             tags = iter.get_tags()
221             for tag in tags:
222                 href = tag.get_data("href")
223                 if href:
224                     self.on_link_clicked(widget, href)
225         return False
226
227     def on_link_clicked(self, widget, href):
228
229         if not href.startswith("http://"):
230             # maybe a relative uri.
231             href = urlparse.urljoin(self.bbs_type.get_uri_base(), href)
232
233         try:
234             uri_opener.open_uri(href)
235         except bbs_type_exception.BbsTypeError:
236             # not supported, show with the web browser.
237             gnome.url_show(href)
238
239     def on_motion_notify_event(self, widget, event):
240         x, y = widget.window_to_buffer_coords(
241             gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y))
242         self.set_cursor_if_appropriate(widget, x, y)
243         widget.window.get_pointer()
244         return False
245
246     def on_visibility_notify_event(self, widget, event):
247         wx, wy, mod = widget.window.get_pointer()
248         bx, by = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy)
249
250         self.set_cursor_if_appropriate(widget, bx, by)
251         return False
252
253     def set_cursor_if_appropriate(self, widget, x, y):
254         hovering = False
255
256         buffer = widget.get_buffer()
257         iter = widget.get_iter_at_location(x, y)
258         if not iter.has_tag(self.leftmargintag) or x > 20:
259             tags = iter.get_tags()
260             for tag in tags:
261                 href = tag.get_data("href")
262                 if href:
263                     hovering = True
264
265         if hovering != self.hovering_over_link:
266             self.hovering_over_link = hovering
267
268         if self.hovering_over_link:
269             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
270                 self.hand_cursor)
271         else:
272             widget.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
273                 self.regular_cursor)
274
275     def on_close_activate(self, widget):
276         self.window.destroy()
277
278     def on_thread_window_delete_event(self, widget, event):
279         w, h = widget.get_size()
280         self.gconf_client.set_int(self.gconf_key_base + "window_width", w)
281         self.gconf_client.set_int(self.gconf_key_base + "window_height", h)
282
283         return False
284         
285     def on_thread_window_destroy(self, widget):
286         -1
287
288     def on_quit_activate(self, widget):
289         session.main_quit()
290
291     def on_show_board_activate(self, widget):
292         board_window.open_board(self.bbs_type.get_uri_base())
293
294     def http_get_dat(self, on_get_res):
295         datfile_url = self.bbs_type.get_dat_uri()
296
297         idx_dic = idxfile.load_idx(self.bbs, self.board, self.thread)
298         lastmod = idx_dic["lastModified"]
299         etag = idx_dic["etag"]
300
301         req = urllib2.Request(datfile_url)
302         if self.size > 0:
303             req.add_header("Range", "bytes=" + str(self.size) + "-")
304         if lastmod:
305             req.add_header("If-Modified-Since", lastmod)
306         if etag:
307             req.add_header("If-None-Match", etag)
308         print req.headers
309
310         opener = urllib2.build_opener(HTTPRedirectHandler302)
311         res = opener.open(req)
312         headers = res.info()
313         print headers
314
315         line = res.readline()
316         maybe_incomplete = False
317         while line:
318             if not line.endswith("\n"):
319                 maybe_incomplete = True
320                 print "does not end with \\n. maybe incomplete"
321                 break
322             on_get_res(line)
323             line = res.readline()
324
325         res.close()
326
327         if maybe_incomplete:
328             lastmod = None
329             etag = None
330         else:
331             if "Last-Modified" in headers:
332                 lastmod = headers["Last-Modified"]
333             if "ETag" in headers:
334                 etag = headers["Etag"]
335
336         if self.num > 0:
337             if not self.title:
338                 title = datfile.get_title_from_dat(
339                     self.bbs, self.board, self.thread)
340                 if title:
341                     self.title = title
342                     gobject.idle_add(self.window.set_title, title)
343             # save idx
344             idx_dic = {"title": self.title, "lineCount": self.num,
345                    "lastModified": lastmod, "etag": etag}
346             idxfile.save_idx(self.bbs, self.board, self.thread, idx_dic)
347
348             gobject.idle_add(session.thread_idx_updated,
349                              self.bbs_type.get_thread_uri(), idx_dic)
350
351     def update(self, widget=None):
352
353         self.jump_request_num = 0
354
355         def load():
356             if self.num == 0:
357                 def create_mark():
358                     self.textbuffer.create_mark("1", self.enditer, True)
359                 gobject.idle_add(create_mark)
360
361             line_count = datfile.get_dat_line_count(
362                 self.bbs, self.board, self.thread)
363             if line_count > self.num:
364                 datfile.load_dat_partly(
365                     self.bbs, self.board, self.thread,
366                     self.append_rawres_to_buffer, self.num+1)
367
368                 def do_jump(num):
369                     if self.jump_request_num:
370                         if self.jump_request_num <= num:
371                             # jump if enable, otherwize jump later.
372                             num = self.jump_request_num
373                             self.jump_request_num = 0
374                             mark = self.textbuffer.get_mark(str(num))
375                             if mark:
376                                 self.textview.scroll_to_mark(
377                                     mark, 0, True, 0, 0)
378                     else:
379                         self.jump_to_the_end(num)
380
381                 gobject.idle_add(do_jump, self.num)
382
383         def get():
384             dat_path = misc.get_thread_dat_path(
385                 self.bbs, self.board, self.thread)
386             dat_file = FileWrap(dat_path)
387
388             def save_line_and_append_to_buffer(line):
389                 dat_file.seek(self.size)
390                 dat_file.write(line)
391                 self.append_rawres_to_buffer(line)
392
393             self.http_get_dat(save_line_and_append_to_buffer)
394             dat_file.close()
395
396             def do_jump():
397                 if self.jump_request_num:
398                     num = self.jump_request_num
399                     self.jump_request_num = 0
400                     mark = self.textbuffer.get_mark(str(num))
401                     if mark:
402                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
403
404             gobject.idle_add(do_jump)
405
406         if self.lock():
407
408             def on_end():
409                 self.un_lock()
410                 self.progress = False
411
412             self.progress = True
413             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, get)
414             t.start()
415
416     def load_dat(self):
417
418         self.size = 0
419         self.num = 0
420         self.jump_request_num = 0
421
422         def load():
423
424             def create_mark():
425                 self.textbuffer.create_mark("1", self.enditer, True)
426             gobject.idle_add(create_mark)
427
428             datfile.load_dat(self.bbs, self.board, self.thread,
429                              self.append_rawres_to_buffer)
430         def jump():
431
432             def do_jump(num):
433                 if self.jump_request_num:
434                     num = self.jump_request_num
435                     self.jump_request_num = 0
436                     mark = self.textbuffer.get_mark(str(num))
437                     if mark:
438                         self.textview.scroll_to_mark(mark, 0, True, 0, 0)
439                 else:
440                     self.jump_to_the_end(num)
441
442             gobject.idle_add(do_jump, self.num)
443
444         if self.lock():
445
446             def on_end():
447                 self.un_lock()
448                 self.progress = False
449
450             self.progress = True
451             t = ThreadInvoker(lambda : gobject.idle_add(on_end), load, jump)
452             t.start()
453
454     def append_rawres_to_buffer(self, line):
455         self.size += len(line)
456         self.num += 1
457
458         if not self.title and self.num == 1:
459             title = datfile.do_get_title_from_dat(line)
460             if title:
461                 self.title = title
462                 gobject.idle_add(self.window.set_title, title)
463
464         h = lambda name,mail,date,msg: self.reselems_to_buffer(
465             self.num, name, mail, date, msg)
466
467         self.res_queue = []
468         datfile.split_line_to_elems(line.decode("cp932", "replace"), h)
469
470         def process_res_queue(res_queue, num):
471             self.process_queue(res_queue)
472             # for next res
473             self.textbuffer.create_mark(str(num+1), self.enditer, True)
474
475         gobject.idle_add(
476             process_res_queue, self.res_queue, self.num)
477
478     def reselems_to_buffer(self, num, name, mail, date, msg):
479         p = barehtmlparser.BareHTMLParser(
480             lambda d,b,h: self.res_queue.append((d,b,h,False)))
481         # number
482         p.feed(str(num) + " ")
483
484         # name
485         p.feed("<b>" + name + "</b>")
486
487         # mail
488         p.feed("[" + mail + "]")
489
490         # date
491         p.feed(date)
492         p.feed("<br>")
493
494         # msg
495         p.reset_func(lambda d,b,h: self.res_queue.append((d,b,h,True)))
496         p.feed(msg.lstrip(" "))
497
498         p.feed("<br><br>")
499         p.close()
500
501     def href_tag(self, href):
502         tag = self.textbuffer.create_tag(underline=pango.UNDERLINE_SINGLE)
503         tag.set_data("href", href)
504         return tag
505
506     def process_queue(self, queue):
507         for data, bold, href, margin in queue:
508             taglist = []
509             if bold:
510                 taglist.append(self.boldtag)
511             if href:
512                 taglist.append(self.href_tag(href))
513             if margin:
514                 taglist.append(self.leftmargintag)
515
516             if taglist:
517                 self.textbuffer.insert_with_tags(self.enditer, data, *taglist)
518             else:
519                 self.textbuffer.insert(self.enditer, data)
520
521     def jump_to_the_end(self, num):
522         mark = self.textbuffer.get_mark(str(num+1))
523         if mark:
524             self.textview.scroll_to_mark(mark, 0)
525
526     def lock(self):
527         if self.lock_obj:
528             print "locked, try later."
529             return False
530         else:
531             print "get lock"
532             self.lock_obj = True
533             return True
534
535     def un_lock(self):
536         self.lock_obj = False
537         print "unlock"
538
539     def jump_to_res(self, uri):
540         strict_uri = self.bbs_type.get_thread_uri()
541         if uri != strict_uri and uri.startswith(strict_uri):
542             resnum = uri[len(strict_uri):]
543             match = re.match("\d+", resnum)
544             if match:
545                 resnum = match.group()
546                 mark = self.textbuffer.get_mark(resnum)
547                 if mark:
548                     self.textview.scroll_to_mark(mark, 0, True, 0, 0)
549                 elif self.progress:
550                     # try later.
551                     self.jump_request_num = int(resnum)
552
553     def load(self, update=False):
554         dat_path = misc.get_thread_dat_path(
555             self.bbs_type.bbs_type, self.bbs_type.board, self.bbs_type.thread)
556         dat_exists = os.path.exists(dat_path)
557         if update or not dat_exists:
558             self.update()
559         else:
560             self.load_dat()