OSDN Git Service

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