Implemented somewhat proper background-fetching of pages.
[automanga.git] / manga / reader.py
CommitLineData
03feea07
FT
1import threading, gtk, gio, gobject
2import lib
3
4class notdone(Exception): pass
5
6class future(threading.Thread):
7 prog = None
8
9 def __init__(self):
10 super(future, self).__init__()
11 self._val = None
12 self._exc = None
c9e2eced
FT
13 self._notlist = []
14 self._started = False
15
16 def start(self):
17 if not self._started:
18 super(future, self).start()
19 self._started = True
03feea07
FT
20
21 def run(self):
22 try:
23 val = self.value()
24 except Exception as e:
c9e2eced
FT
25 with gtk.gdk.lock:
26 self._exc = e
27 for cb in self._notlist:
28 cb()
29 self._notlist = []
03feea07 30 else:
c9e2eced
FT
31 with gtk.gdk.lock:
32 self._val = [val]
33 for cb in self._notlist:
34 cb()
35 self._notlist = []
36
37 # Caller must hold GDK lock
38 def notify(self, cb):
39 self.start()
40 if not self.done:
41 self._notlist.append(cb)
42 else:
43 cb()
44
45 def progcb(self):
46 with gtk.gdk.lock:
47 nls = []
48 for cb in self._notlist:
49 if cb():
50 nls.append(cb)
51 self._notlist = nls
03feea07
FT
52
53 @property
54 def val(self):
c9e2eced 55 self.start()
03feea07
FT
56 if self._exc is not None:
57 raise self._exc
58 if self._val is None:
59 raise notdone()
60 return self._val[0]
61
62 @property
63 def done(self):
c9e2eced 64 self.start()
03feea07
FT
65 return self._exc != None or self._val != None
66
c9e2eced
FT
67 def wait(self):
68 self.start()
69 while self.is_alive():
70 self.join()
71 return self.val
72
03feea07
FT
73class imgload(future):
74 def __init__(self, page):
c9e2eced 75 super(imgload, self).__init__()
03feea07
FT
76 self.page = page
77 self.st = None
c9e2eced 78 self.start()
03feea07
FT
79
80 def value(self):
81 buf = bytearray()
82 with self.page.open() as st:
83 self.p = 0
84 self.st = st
85 while True:
86 read = st.read(1024)
87 if read == "":
88 break
89 self.p += len(read)
90 buf.extend(read)
c9e2eced 91 self.progcb()
03feea07
FT
92 self.st = None
93 return gtk.gdk.pixbuf_new_from_stream(gio.memory_input_stream_new_from_data(str(buf)))
94
95 @property
96 def prog(self):
97 if self.st is None or self.st.clen is None:
98 return None
99 return float(self.p) / float(self.st.clen)
100
c9e2eced
FT
101class pagecache(object):
102 def __init__(self, sz=50):
103 self.sz = sz
104 self.bk = []
105
106 def __getitem__(self, page):
107 idl = page.idlist()
108 for ol, f in self.bk:
109 if ol == idl:
110 return f
111 f = imgload(page)
112 self.bk.append((idl, f))
113 if len(self.bk) > self.sz:
114 self.bk = self.bk[-sz:]
115 return f
116
117class relpageget(future):
118 def __init__(self, cur, prev, cache=None):
119 super(relpageget, self).__init__()
120 self.cur = lib.cursor(cur)
121 self.prev = prev
122 self.cache = cache
123 self.start()
124
125 def value(self):
126 try:
127 if self.prev:
128 page = self.cur.prev()
129 else:
130 page = self.cur.next()
131 except StopIteration:
132 page = None
133 else:
134 if self.cache:
135 self.cache[page]
136 return page
137
138class pageget(future):
139 def __init__(self, fnode):
140 super(pageget, self).__init__()
141 self.fnode = fnode
142 self.start()
143
144 def value(self):
145 return lib.cursor(self.fnode).cur
146
147class ccursor(object):
148 def __init__(self, ob, cache=None):
149 self.cur = lib.cursor(ob)
150 self.prev = relpageget(self.cur, True, cache)
151 self.next = relpageget(self.cur, False, cache)
152
03feea07
FT
153class pageview(gtk.Widget):
154 def __init__(self, pixbuf):
155 super(pageview, self).__init__()
156 self.pixbuf = pixbuf
157 self.zoomed = None, None
158 self.fit = True
159 self.zoom = 1.0
160 self.interp = gtk.gdk.INTERP_HYPER
161 self.off = 0, 0
162
163 def get_osize(self):
164 return self.pixbuf.get_width(), self.pixbuf.get_height()
165
166 def get_asize(self):
167 return self.allocation.width, self.allocation.height
168
169 def do_realize(self):
170 self.set_flags(self.flags() | gtk.REALIZED)
171 alloc = self.allocation
172 self.window = gtk.gdk.Window(self.get_parent_window(),
173 width=alloc.width, height=alloc.height,
174 window_type = gtk.gdk.WINDOW_CHILD,
175 wclass = gtk.gdk.INPUT_OUTPUT,
176 event_mask = self.get_events() | gtk.gdk.EXPOSURE_MASK
177 )
178 self.window.set_user_data(self)
179 self.style.attach(self.window)
180 self.style.set_background(self.window, gtk.STATE_NORMAL)
181 self.window.move_resize(*alloc)
182
183 def do_unrealize(self):
184 self.window.set_user_data(None)
185
186 def do_size_request(self, req):
187 req.width, req.height = self.get_osize()
188
189 def fitzoom(self):
190 w, h = self.get_osize()
191 alloc = self.allocation
192 return min(float(alloc.width) / float(w), float(alloc.height) / float(h))
193
194 def do_size_allocate(self, alloc):
195 self.allocation = alloc
196 if self.fit:
197 self.zoom = self.fitzoom()
198 if self.flags() & gtk.REALIZED:
199 self.window.move_resize(*alloc)
200
201 def get_zoomed(self):
202 zoom = self.zoom
203 pz, zbuf = self.zoomed
204 if pz != zoom:
205 w, h = self.get_osize()
206 zbuf = self.pixbuf.scale_simple(int(w * zoom), int(h * zoom), self.interp)
207 self.zoomed = zoom, zbuf
208 return zbuf
209
210 def get_zsize(self):
211 zbuf = self.get_zoomed()
212 return zbuf.get_width(), zbuf.get_height()
213
214 def do_expose_event(self, event):
215 aw, ah = self.get_asize()
216 dw, dh = aw, ah
217 zbuf = self.get_zoomed()
218 zw, zh = self.get_zsize()
219 ox, oy = self.off
220 dx, dy = 0, 0
221 if zw < aw:
222 dx = (aw - zw) / 2
223 dw = zw
224 if zh < ah:
225 dy = (ah - zh) / 2
226 dh = zh
227 gc = self.style.fg_gc[gtk.STATE_NORMAL]
228 self.window.draw_pixbuf(gc, zbuf, ox, oy, dx, dy, dw, dh)
229
230 def set_off(self, off):
231 aw, ah = self.get_asize()
232 zw, zh = self.get_zsize()
233 ox, oy = off
234 ox, oy = int(ox), int(oy)
235 if ox > zw - aw: ox = zw - aw
236 if oy > zh - ah: oy = zh - ah
237 if ox < 0: ox = 0
238 if oy < 0: oy = 0
239 self.off = ox, oy
240 self.queue_draw()
241
242 def set_zoom(self, zoom):
243 if zoom is not None: zoom = float(zoom)
244 aw, ah = self.get_asize()
245 zw, zh = self.get_zsize()
246 dw, dh = zw - aw, zh - ah
247 ox, oy = self.off
248 xa = float(ox) / float(dw) if dw > 0 else 0.5
249 ya = float(oy) / float(dh) if dh > 0 else 0.5
250
251 if zoom is None:
252 self.fit = True
253 self.zoom = self.fitzoom()
254 else:
255 self.fit = False
256 self.zoom = zoom
257
258 zw, zh = self.get_zsize()
259 dw, dh = zw - aw, zh - ah
260 ox = int(xa * dw) if dw > 0 else 0
261 oy = int(ya * dh) if dh > 0 else 0
262 self.set_off((ox, oy))
263gobject.type_register(pageview)
264
c9e2eced
FT
265class pagefetch(object):
266 def __init__(self, fpage):
267 self.pg = fpage
268
269 def attach(self, reader):
270 self.rd = reader
271 self.msg = gtk.Alignment(0, 0.5, 0, 0)
272 self.hlay = gtk.HBox()
273 self.lbl = gtk.Label("Fetching page...")
274 self.hlay.pack_start(self.lbl)
275 self.lbl.show()
276 self.msg.add(self.hlay)
277 self.hlay.show()
278 self.rd.sbar.pack_start(self.msg)
279 self.msg.show()
280
281 self.pg.notify(self.haspage)
282
283 def haspage(self):
284 if self.rd.pagefetch.cur != self: return False
285 if not self.pg.done:
286 return True
287 if self.pg.val is not None:
288 self.rd.setpage(self.pg.val)
289 self.rd.pagefetch.set(None)
290
291 def abort(self):
292 self.rd.sbar.remove(self.msg)
293
294class imgfetch(object):
295 def __init__(self, fimage):
296 self.img = fimage
297 self.upd = False
298 self.error = None
299
300 def attach(self, reader):
301 self.rd = reader
302 self.msg = gtk.Alignment(0, 0.5, 0, 0)
303 self.hlay = gtk.HBox()
304 self.lbl = gtk.Label("Fetching image...")
305 self.hlay.pack_start(self.lbl)
306 self.lbl.show()
307 self.prog = gtk.ProgressBar()
308 self.prog.set_fraction(0.0)
309 self.hlay.pack_start(self.prog)
310 self.prog.show()
311 self.msg.add(self.hlay)
312 self.hlay.show()
313 self.rd.sbar.pack_start(self.msg)
314 self.msg.show()
315
316 self.img.notify(self.imgprog)
317
318 def imgprog(self):
319 if self.rd.imgfetch.cur != self: return False
320 if self.img.done:
321 try:
322 img = self.img.val
323 except Exception as e:
324 self.error = str(e)
325 else:
326 self.rd.setimg(img)
327 self.upd = True
328 self.rd.imgfetch.set(None)
329 else:
330 p = self.img.prog
331 if p: self.prog.set_fraction(p)
332 return True
333
334 def abort(self):
335 self.rd.sbar.remove(self.msg)
336 if not self.upd:
337 self.rd.setimg(None)
338 if self.error is not None:
339 self.rd.pagelbl.set_text("Error fetching image: " + self.error)
340
341class procslot(object):
342 __slots__ = ["cur", "p"]
343 def __init__(self, p):
344 self.cur = None
345 self.p = p
346
347 def set(self, proc):
348 if self.cur is not None:
349 self.cur.abort()
350 self.cur = None
351 if proc is not None:
352 self.cur = proc
353 try:
354 proc.attach(self.p)
355 except:
356 self.cur = None
357 raise
358
43a12498 359class reader(gtk.Window):
03feea07 360 def __init__(self, manga):
43a12498
FT
361 super(reader, self).__init__(gtk.WINDOW_TOPLEVEL)
362 self.connect("delete_event", lambda wdg, ev, data=None: False)
363 self.connect("destroy", lambda wdg, data=None: self.quit())
364 self.connect("key_press_event", self.key)
c9e2eced
FT
365 self.cache = pagecache()
366 self.pagefetch = procslot(self)
367 self.imgfetch = procslot(self)
368
369 vlay = gtk.VBox()
43a12498
FT
370 self.pfr = gtk.Frame(None)
371 self.pfr.set_shadow_type(gtk.SHADOW_NONE)
c9e2eced 372 vlay.pack_start(self.pfr)
43a12498 373 self.pfr.show()
c9e2eced
FT
374 self.sbar = gtk.HBox()
375 self.pagelbl = gtk.Label("")
376 algn = gtk.Alignment(0, 0.5, 0, 0)
377 algn.add(self.pagelbl)
378 self.pagelbl.show()
379 self.sbar.pack_start(algn)
380 algn.show()
381 vlay.pack_end(self.sbar, False)
382 self.sbar.show()
383 self.add(vlay)
384 vlay.show()
43a12498
FT
385
386 self.manga = manga
03feea07 387 self.page = None
c9e2eced 388 self.fetchpage(pageget(self.manga))
43a12498 389 self.updtitle()
c9e2eced 390 self.point = None
43a12498 391
c9e2eced
FT
392 def updpagelbl(self):
393 if self.page is None:
394 self.pagelbl.set_text("")
395 else:
396 w, h = self.page.get_osize()
397 self.pagelbl.set_text(u"%s\u00d7%s (%d%%)" % (w, h, int(self.page.zoom * 100)))
398
399 def setimg(self, img):
43a12498
FT
400 if self.page is not None:
401 self.pfr.remove(self.page)
402 self.page = None
c9e2eced
FT
403 if img is not None:
404 self.page = pageview(img)
43a12498
FT
405 self.pfr.add(self.page)
406 self.page.show()
c9e2eced
FT
407 self.updpagelbl()
408
409 def setpage(self, page):
410 if self.point is not None:
411 self.point = None
412 if page is not None:
413 self.point = ccursor(page, self.cache)
414 self.imgfetch.set(imgfetch(self.cache[page]))
415 else:
416 self.setimg(None)
417
418 def fetchpage(self, fpage):
419 self.imgfetch.set(None)
420 self.pagefetch.set(pagefetch(fpage))
43a12498
FT
421
422 def updtitle(self):
423 self.set_title(u"Automanga \u2013 " + self.manga.name)
03feea07
FT
424
425 @property
426 def zoom(self):
427 return self.page.zoom
428 @zoom.setter
429 def zoom(self, zoom):
430 self.page.set_zoom(zoom)
c9e2eced 431 self.updpagelbl()
03feea07
FT
432
433 def pan(self, off):
434 ox, oy = self.page.off
435 px, py = off
436 self.page.set_off((ox + px, oy + py))
437
03feea07 438 def key(self, wdg, ev, data=None):
c9e2eced 439 if ev.keyval in [ord('Q'), ord('q')]:
03feea07 440 self.quit()
c9e2eced
FT
441 elif ev.keyval in [65307]:
442 if self.page is not None:
443 self.pagefetch.set(None)
444 self.imgfetch.set(None)
445 if self.page is not None:
446 if ev.keyval in [ord('O'), ord('o')]:
447 self.zoom = 1.0
448 elif ev.keyval in [ord('P'), ord('p')]:
449 self.zoom = None
450 elif ev.keyval in [ord('[')]:
451 self.zoom = min(self.zoom * 1.25, 3)
452 elif ev.keyval in [ord(']')]:
453 self.zoom /= 1.25
454 elif ev.keyval in [ord('h')]:
455 self.pan((-100, 0))
456 elif ev.keyval in [ord('j')]:
457 self.pan((0, 100))
458 elif ev.keyval in [ord('k')]:
459 self.pan((0, -100))
460 elif ev.keyval in [ord('l')]:
461 self.pan((100, 0))
462 elif ev.keyval in [ord('H')]:
463 self.page.set_off((0, self.page.off[1]))
464 elif ev.keyval in [ord('J')]:
465 self.page.set_off((self.page.off[0], self.page.get_asize()[1]))
466 elif ev.keyval in [ord('K')]:
467 self.page.set_off((self.page.off[1], 0))
468 elif ev.keyval in [ord('L')]:
469 self.page.set_off((self.page.get_asize()[0], self.page.off[1]))
470 if self.point is not None:
471 if ev.keyval in [ord(' ')]:
472 self.fetchpage(self.point.next)
473 elif ev.keyval in [65288]:
474 self.fetchpage(self.point.prev)
03feea07
FT
475
476 def quit(self):
477 self.hide()
d9bf4bdb 478 gtk.main_quit()
43a12498 479gobject.type_register(reader)