~ubuntu-branches/ubuntu/jaunty/calibre/jaunty-backports

« back to all changes in this revision

Viewing changes to src/calibre/gui2/lrf_renderer/main.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-01-20 17:14:02 UTC
  • Revision ID: james.westby@ubuntu.com-20090120171402-8y3znf6nokwqe80k
Tags: upstream-0.4.125+dfsg
ImportĀ upstreamĀ versionĀ 0.4.125+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
__license__   = 'GPL v3'
 
2
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
 
3
import sys, logging, os, traceback, time
 
4
 
 
5
from PyQt4.QtGui import QKeySequence, QPainter, QDialog, QSpinBox, QSlider, QIcon
 
6
from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread
 
7
 
 
8
from calibre import __appname__, setup_cli_handlers, islinux
 
9
from calibre.ebooks.lrf.lrfparser import LRFDocument
 
10
 
 
11
from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, \
 
12
                         config, choose_files, Application
 
13
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
 
14
from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
 
15
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
 
16
from calibre.gui2.main_window import MainWindow
 
17
from calibre.gui2.lrf_renderer.document import Document
 
18
from calibre.gui2.library import SearchBox
 
19
 
 
20
class RenderWorker(QThread):
 
21
    
 
22
    def __init__(self, parent, lrf_stream, logger, opts):
 
23
        QThread.__init__(self, parent)
 
24
        self.stream, self.logger, self.opts = lrf_stream, logger, opts
 
25
        self.aborted = False
 
26
        self.lrf = None
 
27
        self.document = None
 
28
        self.exception = None
 
29
        
 
30
    def run(self):
 
31
        try:
 
32
            self.lrf = LRFDocument(self.stream)
 
33
            self.lrf.parse()
 
34
            self.stream.close()
 
35
            self.stream = None
 
36
            if self.aborted:
 
37
                self.lrf = None            
 
38
        except Exception, err:
 
39
            self.lrf, self.stream = None, None
 
40
            self.exception = err
 
41
            self.formatted_traceback = traceback.format_exc()
 
42
            
 
43
    def abort(self):
 
44
        if self.lrf is not None:
 
45
            self.aborted = True
 
46
            self.lrf.keep_parsing = False
 
47
        
 
48
class Config(QDialog, Ui_ViewerConfig):
 
49
    
 
50
    def __init__(self, parent, opts):
 
51
        QDialog.__init__(self, parent)
 
52
        Ui_ViewerConfig.__init__(self)
 
53
        self.setupUi(self)
 
54
        self.white_background.setChecked(opts.white_background)
 
55
        self.hyphenate.setChecked(opts.hyphenate)
 
56
        
 
57
class Main(MainWindow, Ui_MainWindow):
 
58
    
 
59
    def __init__(self, logger, opts, parent=None):
 
60
        MainWindow.__init__(self, opts, parent)
 
61
        Ui_MainWindow.__init__(self)        
 
62
        self.setupUi(self)
 
63
        self.setAttribute(Qt.WA_DeleteOnClose)
 
64
        self.setWindowTitle(__appname__ + _(' - LRF Viewer'))
 
65
    
 
66
        self.logger = logger
 
67
        self.opts = opts
 
68
        self.document = None
 
69
        self.document = Document(self.logger, self.opts)
 
70
        self.spin_box_action = self.spin_box = QSpinBox()
 
71
        self.tool_bar.addWidget(self.spin_box)
 
72
        self.tool_bar.addSeparator()
 
73
        self.slider_action = self.slider = QSlider(Qt.Horizontal)
 
74
        self.tool_bar.addWidget(self.slider)
 
75
        self.tool_bar.addSeparator()
 
76
        self.search = SearchBox(self)
 
77
        self.search_action = self.tool_bar.addWidget(self.search)
 
78
        QObject.connect(self.document, SIGNAL('chapter_rendered(int)'), self.chapter_rendered)
 
79
        QObject.connect(self.document, SIGNAL('page_changed(PyQt_PyObject)'), self.page_changed)
 
80
        
 
81
        QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
 
82
        
 
83
        self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage)
 
84
        self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage)
 
85
        self.action_next_match.setShortcuts(QKeySequence.FindNext)
 
86
        self.addAction(self.action_next_match)
 
87
        QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next) 
 
88
        QObject.connect(self.action_previous_page, SIGNAL('triggered(bool)'), self.previous)
 
89
        QObject.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
 
90
        QObject.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
 
91
        QObject.connect(self.action_next_match, SIGNAL('triggered(bool)'), self.next_match)
 
92
        QObject.connect(self.action_open_ebook, SIGNAL('triggered(bool)'), self.open_ebook)
 
93
        QObject.connect(self.action_configure, SIGNAL('triggered(bool)'), self.configure)
 
94
        QObject.connect(self.spin_box, SIGNAL('valueChanged(int)'), self.go_to_page)
 
95
        QObject.connect(self.slider, SIGNAL('valueChanged(int)'), self.go_to_page)
 
96
        
 
97
        
 
98
        self.graphics_view.setRenderHint(QPainter.Antialiasing, True)
 
99
        self.graphics_view.setRenderHint(QPainter.TextAntialiasing, True)
 
100
        self.graphics_view.setRenderHint(QPainter.SmoothPixmapTransform, True)
 
101
        
 
102
        self.closed = False
 
103
        
 
104
        
 
105
    def configure(self, triggered):
 
106
        opts = config['LRF_ebook_viewer_options']
 
107
        if not opts:
 
108
            opts = self.opts
 
109
        d = Config(self, opts)
 
110
        d.exec_()
 
111
        if d.result() == QDialog.Accepted:
 
112
            opts.white_background = bool(d.white_background.isChecked())
 
113
            opts.hyphenate = bool(d.hyphenate.isChecked())
 
114
            config['LRF_ebook_viewer_options'] = opts
 
115
    
 
116
    def set_ebook(self, stream):
 
117
        self.progress_bar.setMinimum(0)
 
118
        self.progress_bar.setMaximum(0)
 
119
        self.progress_bar.setValue(0)
 
120
            
 
121
        if stream is not None:
 
122
            self.file_name = os.path.basename(stream.name) if hasattr(stream, 'name') else ''
 
123
            self.progress_label.setText('Parsing '+ self.file_name)
 
124
            self.renderer = RenderWorker(self, stream, self.logger, self.opts)
 
125
            QObject.connect(self.renderer, SIGNAL('finished()'), self.parsed, Qt.QueuedConnection)
 
126
            self.search.help_text = 'Search'
 
127
            self.search.clear_to_help()
 
128
            self.last_search = None
 
129
        else:
 
130
            self.stack.setCurrentIndex(0)
 
131
            self.renderer = None
 
132
        
 
133
    def open_ebook(self, triggered):
 
134
        files = choose_files(self, 'open ebook dialog', 'Choose ebook', 
 
135
                             [('Ebooks', ['lrf'])], all_files=False, 
 
136
                             select_only_single_file=True)
 
137
        if files:
 
138
            file = files[0]
 
139
            self.set_ebook(open(file, 'rb'))
 
140
            self.render()
 
141
            
 
142
    
 
143
    def page_changed(self, num):
 
144
        self.slider.setValue(num)
 
145
        self.spin_box.setValue(num)
 
146
    
 
147
    def render(self):
 
148
        if self.renderer is not None:
 
149
            self.stack.setCurrentIndex(1)
 
150
            self.renderer.start()
 
151
    
 
152
    def find(self, search, refinement):
 
153
        self.last_search = search
 
154
        try:
 
155
            self.document.search(search)
 
156
        except StopIteration:
 
157
            error_dialog(self, _('No matches found'), _('<b>No matches</b> for the search phrase <i>%s</i> were found.')%(search,)).exec_()
 
158
    
 
159
    def parsed(self):
 
160
        if not self.renderer.aborted and self.renderer.lrf is not None:
 
161
            width, height =  self.renderer.lrf.device_info.width, \
 
162
                                            self.renderer.lrf.device_info.height
 
163
            hdelta = self.tool_bar.height()+3
 
164
            
 
165
            from PyQt4.QtGui import QScrollBar
 
166
            s = QScrollBar(self)
 
167
            scrollbar_adjust = min(s.width(), s.height())
 
168
            self.graphics_view.resize_for(width+scrollbar_adjust, height+scrollbar_adjust)
 
169
            
 
170
            desktop = QCoreApplication.instance().desktop()
 
171
            screen_height = desktop.availableGeometry(self).height() - 25
 
172
            height = min(screen_height, height+hdelta+scrollbar_adjust)
 
173
            self.resize(width+scrollbar_adjust, height) 
 
174
            self.setWindowTitle(self.renderer.lrf.metadata.title + ' - ' + __appname__)
 
175
            self.document_title = self.renderer.lrf.metadata.title
 
176
            if self.opts.profile:
 
177
                import cProfile
 
178
                lrf = self.renderer.lrf
 
179
                cProfile.runctx('self.document.render(lrf)', globals(), locals(), lrf.metadata.title+'.stats')
 
180
                print 'Stats written to', self.renderer.lrf.metadata.title+'.stats'
 
181
            else:
 
182
                start = time.time()
 
183
                self.document.render(self.renderer.lrf)
 
184
                print 'Layout time:', time.time()-start, 'seconds'
 
185
            self.renderer.lrf = None
 
186
            
 
187
            self.graphics_view.setScene(self.document)
 
188
            self.graphics_view.show()
 
189
            self.spin_box.setRange(1, self.document.num_of_pages)
 
190
            self.slider.setRange(1, self.document.num_of_pages)
 
191
            self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,))
 
192
            self.spin_box.updateGeometry()
 
193
            self.stack.setCurrentIndex(0)
 
194
        elif self.renderer.exception is not None:
 
195
            exception = self.renderer.exception
 
196
            print >>sys.stderr, 'Error rendering document'
 
197
            print >>sys.stderr, exception
 
198
            print >>sys.stderr, self.renderer.formatted_traceback
 
199
            msg =  u'<p><b>%s</b>: '%(exception.__class__.__name__,) + unicode(str(exception), 'utf8', 'replace') + u'</p>'
 
200
            msg += u'<p>Failed to render document</p>'
 
201
            msg += u'<p>Detailed <b>traceback</b>:<pre>'
 
202
            msg += self.renderer.formatted_traceback + '</pre>'            
 
203
            d = ConversionErrorDialog(self, 'Error while rendering file', msg)
 
204
            d.exec_()
 
205
            
 
206
    def chapter_rendered(self, num):
 
207
        if num > 0:
 
208
            self.progress_bar.setMinimum(0)
 
209
            self.progress_bar.setMaximum(num)
 
210
            self.progress_bar.setValue(0)
 
211
            self.progress_label.setText('Laying out '+ self.document_title)
 
212
        else:
 
213
            self.progress_bar.setValue(self.progress_bar.value()+1)
 
214
        QCoreApplication.processEvents()
 
215
    
 
216
    def next(self, triggered):
 
217
        self.document.next()
 
218
 
 
219
    def next_match(self, triggered):
 
220
        try:
 
221
            self.document.next_match()
 
222
        except StopIteration:
 
223
            pass
 
224
        
 
225
    def previous(self, triggered):
 
226
        self.document.previous()
 
227
        
 
228
    def go_to_page(self, num):
 
229
        self.document.show_page(num)
 
230
        
 
231
    def forward(self, triggered):
 
232
        self.document.forward()
 
233
    
 
234
    def back(self, triggered):
 
235
        self.document.back()
 
236
        
 
237
    def wheelEvent(self, ev):
 
238
        if ev.delta() >= 0:
 
239
            self.document.previous()
 
240
        else:
 
241
            self.document.next()
 
242
 
 
243
    def closeEvent(self, event):
 
244
        if self.renderer is not None and self.renderer.isRunning():
 
245
            self.renderer.abort()
 
246
            self.renderer.wait()
 
247
        self.emit(SIGNAL('viewer_closed(PyQt_PyObject)'), self)
 
248
        event.accept()
 
249
 
 
250
 
 
251
def file_renderer(stream, opts, parent=None, logger=None):
 
252
    if logger is None:
 
253
        level = logging.DEBUG if opts.verbose else logging.INFO
 
254
        logger = logging.getLogger('lrfviewer')
 
255
        setup_cli_handlers(logger, level)
 
256
    if islinux:
 
257
        try: # Set lrfviewer as the default for LRF files for this user
 
258
            from subprocess import call
 
259
            call('xdg-mime default calibre-lrfviewer.desktop application/lrf', shell=True)
 
260
        except:
 
261
            pass
 
262
    m = Main(logger, opts, parent=parent)
 
263
    m.set_ebook(stream)
 
264
    return m
 
265
    
 
266
 
 
267
def option_parser():
 
268
    from calibre.gui2.main_window import option_parser
 
269
    parser = option_parser('''\
 
270
%prog [options] book.lrf
 
271
 
 
272
Read the LRF ebook book.lrf
 
273
''')
 
274
    parser.add_option('--verbose', default=False, action='store_true', dest='verbose',
 
275
                      help='Print more information about the rendering process')
 
276
    parser.add_option('--visual-debug', help='Turn on visual aids to debugging the rendering engine',
 
277
                      default=False, action='store_true', dest='visual_debug')
 
278
    parser.add_option('--disable-hyphenation', dest='hyphenate', default=True, action='store_false',
 
279
                      help='Disable hyphenation. Should significantly speed up rendering.')
 
280
    parser.add_option('--white-background', dest='white_background', default=False, action='store_true',
 
281
                      help='By default the background is off white as I find this easier on the eyes. Use this option to make the background pure white.')
 
282
    parser.add_option('--profile', dest='profile', default=False, action='store_true',
 
283
                      help='Profile the LRF renderer')
 
284
    return parser
 
285
 
 
286
def normalize_settings(parser, opts):
 
287
    saved_opts = config['LRF_ebook_viewer_options']
 
288
    if not saved_opts:
 
289
        saved_opts = opts
 
290
    for opt in parser.option_list:
 
291
        if not opt.dest:
 
292
            continue
 
293
        if getattr(opts, opt.dest) == opt.default and hasattr(saved_opts, opt.dest):
 
294
            continue
 
295
        setattr(saved_opts, opt.dest, getattr(opts, opt.dest))
 
296
    return saved_opts
 
297
    
 
298
 
 
299
def main(args=sys.argv, logger=None):
 
300
    parser = option_parser()
 
301
    opts, args = parser.parse_args(args)
 
302
    if hasattr(opts, 'help'):
 
303
        parser.print_help()
 
304
        return 1
 
305
    pid = os.fork() if islinux else -1
 
306
    if pid <= 0:
 
307
        app = Application(args)
 
308
        app.setWindowIcon(QIcon(':/images/viewer.svg'))
 
309
        QCoreApplication.setOrganizationName(ORG_NAME)
 
310
        QCoreApplication.setApplicationName(APP_UID)
 
311
        opts = normalize_settings(parser, opts)
 
312
        stream = open(args[1], 'rb') if len(args) > 1 else None        
 
313
        main = file_renderer(stream, opts, logger=logger)
 
314
        sys.excepthook = main.unhandled_exception
 
315
        main.show()
 
316
        main.render()
 
317
        return app.exec_()        
 
318
    return 0
 
319
 
 
320
if __name__ == '__main__':
 
321
    sys.exit(main())
 
322
    
 
323