-
Notifications
You must be signed in to change notification settings - Fork 8
/
mieru.py
executable file
·475 lines (401 loc) · 14 KB
/
mieru.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement # for python 2.5
import gs
import timer
import time
from threading import RLock
# Mieru modules import
startTs = timer.start()
import manga
import options
import startup
import stats
timer.elapsed(startTs, "All modules combined")
# set current directory to the directory
# of this file
# like this, Mieru can be run from absolute path
# eq.: ./opt/mieru/mieru.py -p harmattan -u harmattan
import os
originalCWD = os.getcwd()
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
# append the platform modules folder to path
import sys
sys.path.append('platforms')
class Mieru:
def destroy(self):
# log elapsed time
sessionTime = time.time() - self.startupTimeStamp
self.stats.updateUsageTime(sessionTime)
self.saveActiveMangaState()
self.options.save()
print("Mieru quiting")
self.gui.stopMainLoop()
def __init__(self):
# log start
initTs = time.clock()
self.startupTimeStamp = time.time()
# parse startup arguments
start = startup.Startup()
args = start.args
self.args = args
self.originalCWD = originalCWD
# restore the persistent options dictionary
self.d = {}
self.options = options.Options(self)
# options value watching
self.maxWatchId = 0
self.watches = {}
# history lock
self.historyLock = RLock() # NOTE: not yet used
# enable stats
self.stats = stats.Stats(self)
self.continuousReading = True
# get the platform module
self.platform = None
# get the platform ID string
platformId = "pc" # safe fallback
if args.p is None:
import platform_detection
# platform detection
result = platform_detection.getBestPlatformModuleId()
if result:
platformId = result
else: # use the CLI provided value
platformId = args.p
if platformId:
if platformId == "maemo5":
import maemo5
if args.u == "hildon": # enable app menu with Hildon gui
self.platform = maemo5.Maemo5(self, GTK=True)
else:
self.platform = maemo5.Maemo5(self, GTK=False)
elif platformId == "harmattan":
import harmattan
self.platform = harmattan.Harmattan(self)
else:
import pc
self.platform = pc.PC(self)
else:
# no platform provided, decide based on selected GUI
if args.u == "hildon":
import maemo5
self.platform = maemo5.Maemo5(self, GTK=True)
elif args.u == "harmattan":
import harmattan
self.platform = harmattan.Harmattan(self)
else:
import pc
self.platform = pc.PC(self)
# create the GUI
startTs1 = timer.start()
# use CLI provided GUI module ID
if args.u:
self._loadGUIModule(args.u)
else: # get GUI module id from the platform module
ids = self.platform.getSupportedGUIModuleIds()
if ids:
guiModuleId = ids[0]
print('preferred GUI ID from platform module: %s' % guiModuleId)
self._loadGUIModule(guiModuleId)
else:
print("platform module error: list of supported GUI IDs is empty")
timer.elapsed(startTs1, "GUI module import")
# # resize the viewport when window size changes
# self.gui.resizeNotify(self._resizeViewport)
self.activeManga = None
# check if a path was specified in the startup arguments
if args.o is not None:
try:
print("loading manga from: %s" % args.o)
self.setActiveManga(self.openManga(args.o, checkHistory=True))
print('manga loaded')
except Exception, e:
print("loading manga from path: %s failed" % args.o)
print(e)
""" restore previously saved state (if available and no manga was
sucessfully loaded from a path provided by startup arguments"""
if self.activeManga is None:
self._restoreState()
timer.elapsed(initTs, "Init")
timer.elapsed(startTs, "Complete startup")
# start the main loop
self.gui.startMainLoop()
# print("loaded modules")
# print(list(sys.modules.keys()))
def _loadGUIModule(self, id):
# report GUI string
import gui
initialSize = self.platform.getScreenWH()
if id in ("QML", "harmattan"):
self.gui = gui.getGui(self, 'QML', accel=True, size=initialSize)
elif id == "hildon":
self.gui = gui.getGui(self, 'hildon', accel=True, size=initialSize)
elif id == "GTK":
self.gui = gui.getGui(self, 'GTK', accel=True, size=initialSize)
# notify the platform module
self.platform.guiModuleLoaded()
def getDict(self):
return self.d
def setDict(self, d):
self.d = d
def getViewport(self):
return self.viewport
def getWindow(self):
return self.window
def getVbox(self):
return self.vbox
def keyPressed(self, keyName):
if keyName == 'f':
self.gui.toggleFullscreen()
elif keyName == 'o':
self.notify('fit to <b>original size</b>')
self.set('fitMode', "original")
elif keyName == 'i':
self.notify('fit to <b>width</b>')
self.set('fitMode', "width")
elif keyName == 'u':
self.notify('fit to <b>height</b>')
self.set('fitMode', "height")
elif keyName == 'z':
self.notify('fit to <b>screen</b>')
self.set('fitMode', "screen")
elif keyName == 'n':
"""launch file chooser"""
self.platform.startChooser("file")
elif keyName == 'b':
"""launch folder chooser"""
self.platform.startChooser("folder")
elif keyName == 'k':
"""toggle kinetic scrolling"""
kinetic = self.get('kineticScrolling', True)
if kinetic:
self.set('kineticScrolling', False)
self.notify('kinetic scrolling <b>disabled</b>')
else:
self.set('kineticScrolling', True)
self.notify('kinetic scrolling <b>enabled</b>')
elif keyName == 'p':
"""show paging dialog"""
self.platform.showPagingDialog()
elif keyName == 'c':
"""show options window"""
self.platform.showOptions()
elif keyName == 'a':
"""show info window"""
self.platform.showInfo()
elif keyName == 'm':
"""minimize the main window"""
self.platform.minimize()
elif keyName == 'q':
self.destroy(self.window)
elif keyName == 'F8' or keyName == 'Page_Up':
if self.activeManga:
self.activeManga.previous()
elif keyName == 'F7' or keyName == 'Page_Down':
if self.activeManga:
self.activeManga.next()
elif not self.platform.handleKeyPress(keyName):
print("key: %s" % keyName)
def on_button_press_event(actor, event):
print("button press event")
def notify(self, message, icon=""):
print("notification: %s" % message)
self.platform.notify(message, icon)
def openManga(self, path, startOnPage=0, replaceCurrent=True, loadNotify=True, checkHistory=True):
if replaceCurrent:
mangaState = None
if checkHistory: # check for saved state for the path
mangaState = self.getMangaStateFromHistory(path)
if mangaState:
print('manga path found in history')
self.openMangaFromState(mangaState)
else:
print("opening %s on page %d" % (path, startOnPage))
mangaInstance = manga.Manga(self, path, startOnPage, loadNotify=loadNotify)
# close and replace any current active manga
self.setActiveManga(mangaInstance)
# increment manga count
self.stats.incrementUnitCount()
# return the newly created manga instance
return mangaInstance
else:
return manga.Manga(self, path, startOnPage, loadNotify=loadNotify)
def openMangaFromState(self, state):
print("opening manga from state")
mangaInstance = manga.Manga(self, load=False)
mangaInstance.setState(state)
if mangaInstance.container is None:
print("container creation failed")
return False
else:
# close and replace any current active manga
self.setActiveManga(mangaInstance)
return True
def setActiveManga(self, mangaInstance):
"""set the given instance as the active manga
eq. it has focus, receives page turn events, etc."""
# is there an already active manga
if self.activeManga:
# add it to history
self.addMangaToHistory(self.activeManga)
# close it
self.activeManga.close()
# replace it with the new one
self.activeManga = mangaInstance
# notify the GUI there is a new active manga instance
self.gui.newActiveManga(self.activeManga)
def getActiveManga(self):
return self.activeManga
def getActiveMangaPath(self):
if self.activeManga:
return self.activeManga.getPath()
else:
print("mieru: can't return manga path - there is no active manga")
return None
def addToHistory(self, mangaState):
"""add a saved manga state to the history"""
if self.get('historyEnabled', True):
openMangasHistory = self.getHistory()
status = None
try:
if mangaState['path'] is not None:
path = mangaState['path']
print("adding to history: %s, on page %d" % (path, mangaState.get('pageNumber', 0)))
openMangasHistory[path] = {"state": mangaState, "timestamp": time.time()}
"""the states are saved under their path to store only unique mangas,
when the same manga is opened again, its state is replaced by the new one
the timestamp is used for chronological sorting of the list
"""
# save the history back to the persistent store
# TODO: limit the size of the history + clearing of history
status = True
except Exception, e:
print("saving manga to history failed with exception:\n", e)
print("manga state was:", mangaState)
self.set('openMangasHistory', openMangasHistory)
self.options.save()
return status
else:
print('history: not added -> history is disabled')
return False
def addMangaToHistory(self, manga):
"""add a manga instance to history"""
state = manga.getState()
if state:
self.addToHistory(state)
def getMangaStateFromHistory(self, path):
history = self.getHistory()
entry = history.get(path, None)
if entry:
return entry['state'] # manga path was stored in history
else:
return None # this manga path has no known history
def removeMangasFromHistory(self, paths):
"""a function for batch removing mangas from history"""
print("removing %d mangas from history" % len(paths))
openMangasHistory = self.getHistory()
if openMangasHistory:
for path in paths:
if path in openMangasHistory:
print("deleting %s" % path)
del openMangasHistory[path]
self.set('openMangasHistory', openMangasHistory)
print("removing done")
def removeMangaFromHistory(self, path):
"""delete manga described by path from history"""
openMangasHistory = self.getHistory()
if openMangasHistory:
if path in openMangasHistory:
del openMangasHistory[path]
self.set('openMangasHistory', openMangasHistory)
def getHistory(self):
"""return history of open mangas, without sorting it"""
history = self.get('openMangasHistory', {})
# check if the data retrieved from history is really a list
if isinstance(history, dict):
return history
else:
return {}
# looks like some other object type than a dict got stored in the history,
# so we return an empty list (no dict -> no valid history -> empty history)
def getSortedHistory(self):
openMangasHistory = self.getHistory()
if openMangasHistory:
sortedList = []
for path in sorted(openMangasHistory, key=lambda path: openMangasHistory[path]['timestamp'], reverse=True):
sortedList.append(openMangasHistory[path])
return sortedList
else:
return []
def clearHistory(self):
"""clear the history of opened mangas"""
self.set('openMangasHistory', {})
def watch(self, key, callback, *args):
"""add a callback on an options key"""
id = self.maxWatchId + 1 # TODO remove watch based on id
self.maxWatchId = id # TODO: recycle ids ? (alla PID)
if key not in self.watches:
self.watches[key] = [] # create the initial list
self.watches[key].append((id, callback, args))
return id
def _notifyWatcher(self, key, value):
"""run callbacks registered on an options key"""
callbacks = self.watches.get(key, None)
if callbacks:
for item in callbacks:
(id, callback, args) = item
oldValue = self.get(key, None)
if callback:
callback(key, value, oldValue, *args)
else:
print("invalid watcher callback :", callback)
def get(self, key, default):
"""
get a value from the persistent dictionary
"""
try:
return self.d.get(key, default)
except Exception, e:
print("options: exception while working with persistent dictionary:\n%s" % e)
return default
def set(self, key, value):
"""
set a value in the persistent dictionary
"""
self.d[key] = value
self.options.save()
if key in self.watches.keys():
self._notifyWatcher(key, value)
def saveActiveMangaState(self):
print("saving active manga state")
if self.activeManga: # is some manga actually loaded ?
state = self.activeManga.getState()
if self.addToHistory(state):
print('state saved')
def _restoreState(self):
openMangasHistory = self.getSortedHistory()
if openMangasHistory:
print("restoring last open manga")
lastOpenMangaState = openMangasHistory[0]['state']
if self.openMangaFromState(lastOpenMangaState):
print("last open manga restored")
else:
print("restoring last open manga failed")
else:
print("no history found")
def _resizeViewport(self, allocation):
self.viewport = allocation
def getFittingModes(self):
"""return list of fitting mode with key and description"""
modes = [
("original", "fit to original size"),
("width", "fit to width"),
("height", "fit to height"),
("screen", "fit to screen")
]
return modes
if __name__ == "__main__":
mieru = Mieru()