Merge branch 'horizontals'
[swftools.git] / wx / lib / app.py
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 #
4 # gpdf2swf.py
5 # graphical user interface for pdf2swf
6 #
7 # Part of the swftools package.
8
9 # Copyright (c) 2008,2009 Matthias Kramm <kramm@quiss.org> 
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */
24
25 from __future__ import division
26 import os
27 import wx
28 import time
29 import pickle
30
31 from lib.wordwrap import wordwrap
32 from wx.lib.pubsub import Publisher
33
34 from document import Document
35 from gui.dialogs import (ProgressDialog, OptionsDialog, AboutDialog, InfoDialog)
36 from gui.gmain import (PdfFrame,
37                       ID_INVERT_SELECTION, ID_SELECT_ODD,
38                       ID_ONE_PAGE_PER_FILE,
39                       ID_SELECT_EVEN, ID_DOC_INFO,
40                      )
41
42
43 def GetDataDir():
44     """
45     Return the standard location on this platform for application data
46     """
47     sp = wx.StandardPaths.Get()
48     return sp.GetUserDataDir()
49
50 def GetConfig():
51     if not os.path.exists(GetDataDir()):
52         os.makedirs(GetDataDir())
53
54     config = wx.FileConfig(
55         localFilename=os.path.join(GetDataDir(), "options"))
56     return config
57
58
59 class Pdf2Swf:
60     def __init__(self):
61         self.__doc = Document()
62
63         self.__threads = {}
64
65         self.__busy = None
66         self.__progress = None
67
68         self.__can_save = False
69         self.__can_viewinfo = False
70
71         self.view = PdfFrame()
72         wx.GetApp().SetTopWindow(self.view)
73         # Call Show after the current and pending event
74         # handlers have been completed. Otherwise on MSW
75         # we see the frame been draw and after that we saw
76         # the menubar appear
77         wx.CallAfter(self.view.Show)
78
79         self.options = OptionsDialog(self.view)
80         self.__ReadConfigurationFile()
81
82         self.view.toolbar_preview_type.SetSelection(0)
83
84         Publisher.subscribe(self.OnPageChanged, "PAGE_CHANGED")
85         Publisher.subscribe(self.OnFileLoaded, "FILE_LOADED")
86         Publisher.subscribe(self.OnFileNotLoaded, "FILE_NOT_LOADED")
87         Publisher.subscribe(self.OnDiffSizes, "DIFF_SIZES")
88         Publisher.subscribe(self.OnThumbnailAdded, "THUMBNAIL_ADDED")
89         Publisher.subscribe(self.OnThumbnailDone, "THUMBNAIL_DONE")
90         Publisher.subscribe(self.OnProgressBegin, "SWF_BEGIN_SAVE")
91         Publisher.subscribe(self.OnProgressUpdate, "SWF_PAGE_SAVED")
92         Publisher.subscribe(self.OnProgressDone, "SWF_FILE_SAVED")
93         Publisher.subscribe(self.OnCombineError, "SWF_COMBINE_ERROR")
94         Publisher.subscribe(self.OnFileDroped, "FILE_DROPED")
95         Publisher.subscribe(self.OnFilesDroped, "FILES_DROPED")
96         Publisher.subscribe(self.OnPluginOnePagePerFileNotSupported,
97                                     "PLUGIN_ONE_PAGE_PER_FILE_NOT_SUPPORTED")
98         Publisher.subscribe(self.OnPluginError, "PLUGIN_ERROR")
99
100         self.view.Bind(wx.EVT_MENU, self.OnMenuOpen, id=wx.ID_OPEN)
101         self.view.Bind(wx.EVT_MENU, self.OnMenuSave, id=wx.ID_SAVE)
102         self.view.Bind(wx.EVT_MENU, self.OnMenuSaveSelected, id=wx.ID_SAVEAS)
103         self.view.Bind(wx.EVT_MENU, self.OnMenuExit, id=wx.ID_EXIT)
104         self.view.Bind(wx.EVT_MENU_RANGE, self.OnFileHistory,
105                        id=wx.ID_FILE1, id2=wx.ID_FILE9)
106
107         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SAVE)
108         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SAVEAS)
109
110         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectAll, id=wx.ID_SELECTALL)
111         self.view.Bind(wx.EVT_MENU,
112                        self.OnMenuInvertSelection, id=ID_INVERT_SELECTION)
113         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectOdd, id=ID_SELECT_ODD)
114         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectEven, id=ID_SELECT_EVEN)
115         self.view.Bind(wx.EVT_MENU, self.OnMenuOptions, id=wx.ID_PREFERENCES)
116
117         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SELECTALL)
118         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_INVERT_SELECTION)
119         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_SELECT_ODD)
120         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_SELECT_EVEN)
121
122         self.view.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)
123
124         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_IN)
125         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_OUT)
126         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_100)
127         self.view.Bind(wx.EVT_MENU, self.OnFit, id=wx.ID_ZOOM_FIT)
128         self.view.Bind(wx.EVT_MENU, self.OnShowDocInfo, id=ID_DOC_INFO)
129
130         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_IN)
131         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_OUT)
132         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_100)
133         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_FIT)
134         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUIInfo, id=ID_DOC_INFO)
135
136         self.view.page_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectItem)
137         self.view.Bind(wx.EVT_CLOSE, self.OnMenuExit)
138
139         self.view.toolbar_preview_type.Bind(wx.EVT_CHOICE,
140                                             self.OnPreviewType)
141
142         # statusbar cancel thumbanails generation button
143         self.view.statusbar.btn_cancel.Bind(wx.EVT_BUTTON,
144                                             self.OnThumbnailCancel)
145
146         # Don't know where the problem is (python/xpython or wxwidgets/wxpython)
147         # but I found that this hack was necessary to avoid the app enter in a
148         # idle state. We must, for example, move the mouse inside the app
149         # for threads continue their job.
150         # There is no need for this when freezing with other utils, like
151         # py2exe, pyinstaller, cxfreeze
152         if "wxMSW" in wx.PlatformInfo:
153             self.timer = wx.Timer(self.view)
154             self.view.Bind(wx.EVT_TIMER, lambda evt: None)
155             self.timer.Start(50)
156
157     def OnFilesDroped(self, evt):
158         dlg = wx.MessageDialog(self.view,
159                       u"You must drop only one file.",
160                       u"Notice",
161                       style=wx.OK, pos=wx.DefaultPosition)
162         dlg.ShowModal()
163         dlg.Destroy()
164
165     def OnFileDroped(self, message):
166         self.__Load(message.data["filename"])
167
168     def OnPluginOnePagePerFileNotSupported(self, message):
169         self.Message(u"Selected viewer does not support "
170                      u"one page per file. ")
171
172     def OnPluginError(self, message):
173         self.Message(u"Error applying selected viewer")
174
175     def OnFileHistory(self, evt):
176         # get the file based on the menu ID
177         fileNum = evt.GetId() - wx.ID_FILE1
178         filename = self.view.filehistory.GetHistoryFile(fileNum)
179
180         self.__Load(filename)
181
182     def OnProgressBegin(self, message):
183         pages = message.data["pages"]
184         style = (
185                  wx.PD_APP_MODAL|wx.PD_ELAPSED_TIME|
186                  wx.PD_REMAINING_TIME|wx.PD_CAN_ABORT|
187                  wx.PD_AUTO_HIDE
188                 )
189         self.__progress = ProgressDialog(u"Saving...",
190                                        u"Start saving SWF pages",
191                                        maximum=pages,
192                                        parent=self.view, style=style)
193         self.__progress.Show()
194         self.view.SetStatusText(u"Saving document...")
195
196     def OnProgressUpdate(self, message):
197         pagenr = message.data["pagenr"]
198         pages = message.data["pages"]
199
200         keep_running = self.__progress.Update(
201                                  pagenr,
202                                  u"Saving SWF page %d of %d" % (pagenr, pages)
203                              )
204
205         if not keep_running and self.__threads.has_key("progress"):
206             self.view.SetStatusText(u"Cancelling...")
207             self.__threads.pop("progress").Stop()
208
209
210     def OnProgressDone(self, message):
211         if self.__threads.has_key("progress"): # it goes all the way?
212             self.__threads.pop("progress")
213             self.view.SetStatusText(u"SWF document saved successfully.")
214         else:
215             self.view.SetStatusText(u"")
216
217         self.__progress.Destroy()
218         self.__progress = None
219
220     def OnCombineError(self, message):
221         from wx.lib.dialogs import ScrolledMessageDialog
222         ScrolledMessageDialog(self.view, message.data, u"Notice").ShowModal()
223
224
225     def OnThumbnailAdded(self, message):
226         self.view.statusbar.SetGaugeValue(message.data['pagenr'])
227         tot = self.view.page_list.GetItemCount()
228         self.view.SetStatusText(u"Generating thumbnails %s/%d" %
229                                 (message.data['pagenr'], tot), 0)
230
231     def OnThumbnailDone(self, message):
232         self.view.statusbar.SetGaugeValue(0)
233         self.view.SetStatusText(u"", 0)
234         if self.__threads.has_key("thumbnails"):
235             self.__threads.pop("thumbnails")
236         self.view.SendSizeEvent()
237
238     def OnThumbnailCancel(self, event):
239         if self.__threads.has_key("thumbnails"):
240             self.__threads["thumbnails"].Stop()
241
242     def OnSelectItem(self, event):
243         self.__doc.ChangePage(event.GetIndex() + 1)
244
245     def OnPreviewType(self, event):
246         filename = self.__doc.filename
247         if filename:
248             self.__Load(filename)
249
250     def OnPageChanged(self, message):
251         # ignore if we have more than one item selected
252         if self.view.page_list.GetSelectedItemCount() > 1:
253             return
254
255         self.view.page_preview.DisplayPage(message.data)
256
257     def SetTitle(self):
258         name = wx.GetApp().GetAppName()
259         filename = os.path.basename(self.__doc.filename)
260         if self.__doc.title != "n/a":
261             t = "%s - %s (%s)" % (name, filename, self.__doc.title)
262         else:
263             t = "%s - %s" % (name, filename)
264         self.view.SetTitle(t)
265
266     def OnFileLoaded(self, message):
267         if self.__progress:
268             self.__progress.Destroy()
269             self.__progress = None
270
271         self.__can_viewinfo = True
272         del self.__busy
273
274         self.SetTitle()
275
276         if self.__doc.oktocopy == 'no':
277             self.__can_save = False
278             self.view.page_list.DisplayEmptyThumbnails(0)
279             self.view.page_preview.Clear()
280             self.view.SetStatusText(u"")
281             self.Message(
282                     u"This PDF disallows copying, cannot be converted."
283                     )
284             return
285
286         #if not self.__doc.oktoprint:
287         self.view.SetStatusText(u"Document loaded successfully.")
288
289         self.view.page_list.DisplayEmptyThumbnails(message.data["pages"])
290         thumbs = self.__doc.GetThumbnails()
291         t = self.view.page_list.DisplayThumbnails(thumbs)
292         self.__threads["thumbnails"] = t
293         self.view.statusbar.SetGaugeRange(message.data["pages"])
294         #del self.__busy
295
296     def OnFileNotLoaded(self, message):
297         self.__can_save = False
298         self.__can_viewinfo = False
299         del self.__busy
300         self.view.SetStatusText(u"")
301         self.Message(
302                     u"Could not open file %s" % message.data['filename']
303                     )
304         
305     def OnDiffSizes(self, message):
306         # just let the user know- for now, we can't handle this properly
307         self.Message(
308                     u"In this PDF, width or height are not the same for "
309                     u"each page. This might cause problems if you export "
310                     u"pages of different dimensions into the same SWF file."
311                     )
312
313     def OnMenuOpen(self, event):
314         dlg = wx.FileDialog(self.view, u"Choose PDF File:",
315                      style=wx.OPEN|wx.CHANGE_DIR,
316                      wildcard = u"PDF files (*.pdf)|*.pdf|all files (*.*)|*.*")
317
318         if dlg.ShowModal() == wx.ID_OK:
319             filename = dlg.GetPath()
320             self.__Load(filename)
321
322     def OnMenuSave(self, event, pages=None):
323         defaultFile = self.__doc.lastsavefile
324         if "wxMSW" in wx.PlatformInfo:
325             allFiles = "*.*"
326         else:
327             allFiles = "*"
328         self.view.SetStatusText(u"")
329         dlg = wx.FileDialog(self.view, u"Choose Save Filename:",
330                        style = wx.SAVE | wx.OVERWRITE_PROMPT,
331                        defaultFile=os.path.basename(defaultFile),
332                        wildcard=u"SWF files (*.swf)|*.swf"
333                                  "|all files (%s)|%s" % (allFiles, allFiles))
334
335
336         if dlg.ShowModal() == wx.ID_OK:
337             menubar = self.view.GetMenuBar()
338             one_file_per_page = menubar.IsChecked(ID_ONE_PAGE_PER_FILE)
339
340             self.__threads["progress"] = self.__doc.SaveSWF(dlg.GetPath(),
341                                                          one_file_per_page,
342                                                          pages, self.options)
343
344     def OnUpdateUI(self, event):
345         menubar = self.view.GetMenuBar()
346         menubar.Enable(event.GetId(), self.__can_save)
347
348         self.view.GetToolBar().EnableTool(event.GetId(), self.__can_save)
349
350     def OnUpdateUIInfo(self, event):
351         menubar = self.view.GetMenuBar()
352         menubar.Enable(event.GetId(), self.__can_viewinfo)
353
354         self.view.GetToolBar().EnableTool(event.GetId(), self.__can_viewinfo)
355
356     def OnMenuSaveSelected(self, event):
357         pages = []
358         page = self.view.page_list.GetFirstSelected()
359         pages.append(page+1)
360
361         while True:
362             page = self.view.page_list.GetNextSelected(page)
363             if page == -1:
364                 break
365             pages.append(page+1)
366
367         self.OnMenuSave(event, pages)
368
369     def OnMenuExit(self, event):
370         self.view.SetStatusText(u"Cleaning up...")
371
372         # Stop any running thread
373         self.__StopThreads()
374
375         config = GetConfig()
376         self.view.filehistory.Save(config)
377         config.Flush()
378         # A little extra cleanup is required for the FileHistory control
379         del self.view.filehistory
380
381         # Save quality options
382         dirpath = GetDataDir()
383         data = self.options.quality_panel.pickle()
384         try:
385             f = file(os.path.join(dirpath, 'quality.pkl'), 'wb')
386             pickle.dump(data, f)
387             f.close()
388         except Exception:
389             pass
390
391         # Save viewer options
392         try:
393             f = file(os.path.join(dirpath, 'viewers.pkl'), 'wb')
394             data = self.options.viewers_panel.pickle()
395             pickle.dump(data, f)
396             f.close()
397         except Exception:
398             pass
399
400         self.view.Destroy()
401
402     def OnMenuSelectAll(self, event):
403         for i in range(0, self.view.page_list.GetItemCount()):
404             self.view.page_list.Select(i, True)
405
406     def OnMenuInvertSelection(self, event):
407         for i in range(0, self.view.page_list.GetItemCount()):
408             self.view.page_list.Select(i, not self.view.page_list.IsSelected(i))
409
410     def OnMenuSelectOdd(self, event):
411         for i in range(0, self.view.page_list.GetItemCount()):
412             self.view.page_list.Select(i, not bool(i%2))
413
414     def OnMenuSelectEven(self, event):
415         for i in range(0, self.view.page_list.GetItemCount()):
416             self.view.page_list.Select(i, bool(i%2))
417
418     def OnMenuOptions(self, event):
419         self.options.ShowModal()
420
421     def OnFit(self, event):
422         self.__doc.Fit(self.view.page_preview.GetClientSize())
423
424     def OnZoom(self, event):
425         zoom = {
426               wx.ID_ZOOM_IN: .1,
427               wx.ID_ZOOM_OUT: -.1,
428               wx.ID_ZOOM_100: 1,
429         }
430         self.__doc.Zoom(zoom[event.GetId()])
431
432     def OnShowDocInfo(self, event):
433         info = InfoDialog(self.view)
434         info.info.display(self.__doc)
435         info.Show()
436
437     def OnAbout(self, evt):
438         AboutDialog(self.view)
439
440     def __Load(self, filename):
441         self.__can_save = True
442         self.__StopThreads()
443         self.view.SetStatusText(u"Loading document...")
444         self.__busy = wx.BusyInfo(u"One moment please, "
445                                   u"opening pdf document...")
446
447         self.view.filehistory.AddFileToHistory(filename)
448         try:
449             # I dont care if this, for some reason,
450             # give some error. I just swallow it
451             os.chdir(os.path.dirname(filename))
452         except:
453             pass
454
455         # Need to delay the file load a little bit
456         # for the BusyInfo get a change to repaint itself
457         #wx.FutureCall(150, self.__doc.Load, filename)
458         sel = self.view.toolbar_preview_type.GetSelection()
459         #print sel
460         PREV_TYPE = {
461             0 : [('bitmap', '1'), ('poly2bitmap', '0'), ('bitmapfonts', '1'),],
462             1 : [('bitmap', '0'), ('poly2bitmap', '1'), ('bitmapfonts', '0'),],
463             2 : [('bitmap', '0'), ('poly2bitmap', '0'), ('bitmapfonts', '0'),],
464         }
465         self.__doc.preview_parameters = PREV_TYPE[sel]
466         wx.CallAfter(self.__doc.Load, filename)
467
468     def __StopThreads(self):
469         for n, t in self.__threads.items():
470             t.Stop()
471
472         running = True
473         while running:
474             running = False
475             for n, t in self.__threads.items():
476                 running = running + t.IsRunning()
477             time.sleep(0.1)
478
479     def __ReadConfigurationFile(self):
480         config = GetConfig()
481         self.view.filehistory.Load(config)
482
483         dirpath = GetDataDir()
484         try:
485             f = file(os.path.join(dirpath, 'quality.pkl'), 'rb')
486             #try:
487             if 1:
488                 data = pickle.load(f)
489                 self.options.quality_panel.unpickle(data)
490             #except:
491             #    self.Message(
492             #          u"Error loading quality settings. "
493             #          u"They will be reset to defaults. ")
494             f.close()
495         except Exception:
496             pass
497
498         try:
499             f = file(os.path.join(dirpath, 'viewers.pkl'), 'rb')
500             #try:
501             if 1:
502                 data = pickle.load(f)
503                 self.options.viewers_panel.unpickle(data)
504             #except:
505             #    self.Message(
506             #          u"Error loading viewers settings. "
507             #          u"They will be reset to defaults. ")
508             f.close()
509         except Exception:
510             pass
511         #d = pickle.load(f)
512
513     def Message(self, message):
514         dlg = wx.MessageDialog(self.view,
515                       message,
516                       style=wx.OK, pos=wx.DefaultPosition)
517         dlg.ShowModal()
518         dlg.Destroy()
519