fixed another mem leak
[swftools.git] / wx / 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 wx.lib.wordwrap import wordwrap
32 from wx.lib.pubsub import Publisher
33
34 from document import Document
35 from gui import (PdfFrame, ProgressDialog, OptionsDialog, AboutDialog,
36                  ID_INVERT_SELECTION, ID_SELECT_ODD, ID_SELECT_EVEN)
37
38
39 def GetDataDir():
40     """
41     Return the standard location on this platform for application data
42     """
43     sp = wx.StandardPaths.Get()
44     return sp.GetUserDataDir()
45
46 def GetConfig():
47     if not os.path.exists(GetDataDir()):
48         os.makedirs(GetDataDir())
49
50     config = wx.FileConfig(
51         localFilename=os.path.join(GetDataDir(), "options"))
52     return config
53
54
55 class Pdf2Swf:
56     def __init__(self):
57         self.__doc = Document()
58
59         self.__threads = {}
60
61         self.__busy = None
62         self.__progress = None
63
64         self.__can_save = False
65
66         self.view = PdfFrame()
67         wx.GetApp().SetTopWindow(self.view)
68         # Call Show after the current and pending event
69         # handlers have been completed. Otherwise on MSW
70         # we see the frame been draw and after that we saw
71         # the menubar appear
72         wx.CallAfter(self.view.Show)
73
74         self.options = OptionsDialog(self.view)
75         self.__ReadConfigurationFile()
76
77         Publisher.subscribe(self.OnPageChanged, "PAGE_CHANGED")
78         Publisher.subscribe(self.OnFileLoaded, "FILE_LOADED")
79         Publisher.subscribe(self.OnDiffSizes, "DIFF_SIZES")
80         Publisher.subscribe(self.OnThumbnailAdded, "THUMBNAIL_ADDED")
81         Publisher.subscribe(self.OnThumbnailDone, "THUMBNAIL_DONE")
82         Publisher.subscribe(self.OnProgressBegin, "SWF_BEGIN_SAVE")
83         Publisher.subscribe(self.OnProgressUpdate, "SWF_PAGE_SAVED")
84         Publisher.subscribe(self.OnProgressDone, "SWF_FILE_SAVED")
85         Publisher.subscribe(self.OnCombineError, "SWF_COMBINE_ERROR")
86         Publisher.subscribe(self.OnFileDroped, "FILE_DROPED")
87         Publisher.subscribe(self.OnFilesDroped, "FILES_DROPED")
88
89         self.view.Bind(wx.EVT_MENU, self.OnMenuOpen, id=wx.ID_OPEN)
90         self.view.Bind(wx.EVT_MENU, self.OnMenuSave, id=wx.ID_SAVE)
91         self.view.Bind(wx.EVT_MENU, self.OnMenuSaveSelected, id=wx.ID_SAVEAS)
92         self.view.Bind(wx.EVT_MENU, self.OnMenuExit, id=wx.ID_EXIT)
93         self.view.Bind(wx.EVT_MENU_RANGE, self.OnFileHistory,
94                        id=wx.ID_FILE1, id2=wx.ID_FILE9)
95
96         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SAVE)
97         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SAVEAS)
98
99         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectAll, id=wx.ID_SELECTALL)
100         self.view.Bind(wx.EVT_MENU,
101                        self.OnMenuInvertSelection, id=ID_INVERT_SELECTION)
102         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectOdd, id=ID_SELECT_ODD)
103         self.view.Bind(wx.EVT_MENU, self.OnMenuSelectEven, id=ID_SELECT_EVEN)
104         self.view.Bind(wx.EVT_MENU, self.OnMenuOptions, id=wx.ID_PREFERENCES)
105
106         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_SELECTALL)
107         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_INVERT_SELECTION)
108         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_SELECT_ODD)
109         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=ID_SELECT_EVEN)
110
111         self.view.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)
112
113         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_IN)
114         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_OUT)
115         self.view.Bind(wx.EVT_MENU, self.OnZoom, id=wx.ID_ZOOM_100)
116         self.view.Bind(wx.EVT_MENU, self.OnFit, id=wx.ID_ZOOM_FIT)
117
118         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_IN)
119         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_OUT)
120         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_100)
121         self.view.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI, id=wx.ID_ZOOM_FIT)
122
123         self.view.page_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectItem)
124         self.view.Bind(wx.EVT_CLOSE, self.OnMenuExit)
125
126         # statusbar cancel thumbanails generation button
127         self.view.statusbar.btn_cancel.Bind(wx.EVT_BUTTON,
128                                             self.OnThumbnailCancel)
129
130     def OnFilesDroped(self, evt):
131         dlg = wx.MessageDialog(self.view,
132                       u"You must drop only one file.",
133                       u"Notice",
134                       style=wx.OK, pos=wx.DefaultPosition)
135         dlg.ShowModal()
136         dlg.Destroy()
137
138     def OnFileDroped(self, message):
139         self.__Load(message.data["filename"])
140
141     def OnFileHistory(self, evt):
142         # get the file based on the menu ID
143         fileNum = evt.GetId() - wx.ID_FILE1
144         filename = self.view.filehistory.GetHistoryFile(fileNum)
145
146         self.__Load(filename)
147
148     def OnProgressBegin(self, message):
149         if self.__progress:
150             self.__progress.Destroy()
151         pages = message.data["pages"]
152         style = (
153                  wx.PD_APP_MODAL|wx.PD_ELAPSED_TIME|
154                  wx.PD_REMAINING_TIME|wx.PD_CAN_ABORT|
155                  wx.PD_AUTO_HIDE
156                 )
157         self.__progress = ProgressDialog(u"Saving...",
158                                        u"Start saving SWF pages",
159                                        maximum=pages-1,
160                                        parent=self.view, style=style)
161         self.__progress.Show()
162         self.view.SetStatusText(u"Saving document...")
163
164     def OnProgressUpdate(self, message):
165         pagenr = message.data["pagenr"]
166         pagenr0 = pagenr - 1 # 0 based
167         pages = message.data["pages"]
168
169         keep_running, skip = self.__progress.Update(
170                                  pagenr0,
171                                  u"Saving SWF page %d of %d" % (pagenr, pages)
172                              )
173         if not keep_running and self.__threads.has_key("progress"):
174             self.view.SetStatusText(u"Cancelling...")
175             self.__threads.pop("progress").Stop()
176
177         # Send size events to resize the progress dialog,
178         # this will allow the progress message label to resize accordingly.
179         # Here we minimize that events every 10 times.
180         if pagenr0 % 10 == 0:
181             self.__progress.SendSizeEvent()
182
183     def OnProgressDone(self, message):
184         self.__progress.Hide()
185         if self.__threads.has_key("progress"): # it goes all the way?
186             self.__threads.pop("progress")
187             self.view.SetStatusText(u"SWF document saved successfully.")
188         else:
189             self.view.SetStatusText(u"")
190
191     def OnCombineError(self, message):
192         from wx.lib.dialogs import ScrolledMessageDialog
193         ScrolledMessageDialog(self.view, message.data, u"Notice").ShowModal()
194
195
196     def OnThumbnailAdded(self, message):
197         self.view.statusbar.SetGaugeValue(message.data['pagenr'])
198         tot = self.view.page_list.GetItemCount()
199         self.view.SetStatusText(u"Generating thumbnails %s/%d" %
200                                 (message.data['pagenr'], tot), 0)
201
202     def OnThumbnailDone(self, message):
203         self.view.statusbar.SetGaugeValue(0)
204         self.view.SetStatusText(u"", 0)
205         if self.__threads.has_key("thumbnails"):
206             self.__threads.pop("thumbnails")
207
208     def OnThumbnailCancel(self, event):
209         if self.__threads.has_key("thumbnails"):
210             self.__threads["thumbnails"].Stop()
211
212     def OnSelectItem(self, event):
213         self.__doc.ChangePage(event.GetIndex() + 1)
214
215     def OnPageChanged(self, message):
216         # ignore if we have more than one item selected
217         if self.view.page_list.GetSelectedItemCount() > 1:
218             return
219
220         self.view.page_preview.DisplayPage(message.data)
221
222     def OnFileLoaded(self, message):
223         if self.__progress:
224             self.__progress.Destroy()
225             self.__progress = None
226
227         self.view.SetStatusText(u"Document loaded successfully.")
228         self.view.page_list.DisplayEmptyThumbnails(message.data["pages"])
229         thumbs = self.__doc.GetThumbnails()
230         t = self.view.page_list.DisplayThumbnails(thumbs)
231         self.__threads["thumbnails"] = t
232         self.view.statusbar.SetGaugeRange(message.data["pages"])
233         del self.__busy
234
235     def OnDiffSizes(self, message):
236         # just let the user know- for now, we can't handle this properly
237         self.Message(
238                     u"In this PDF, width or height are not the same for "
239                     u"each page. This might cause problems if you export "
240                     u"pages of different dimensions into the same SWF file."
241                     )
242
243     def OnMenuOpen(self, event):
244         dlg = wx.FileDialog(self.view, u"Choose PDF File:",
245                      style = wx.FD_CHANGE_DIR|wx.FD_OPEN,
246                      wildcard = u"PDF files (*.pdf)|*.pdf|all files (*.*)|*.*")
247
248         if dlg.ShowModal() == wx.ID_OK:
249             filename = dlg.GetPath()
250             self.__Load(filename)
251
252     def OnMenuSave(self, event, pages=None):
253         defaultFile = self.__doc.lastsavefile
254         allFiles = "*.*" if "wxMSW" in wx.PlatformInfo else "*"
255         self.view.SetStatusText(u"")
256         dlg = wx.FileDialog(self.view, u"Choose Save Filename:",
257                        style = wx.SAVE | wx.OVERWRITE_PROMPT,
258                        defaultFile=defaultFile,
259                        wildcard=u"SWF files (*.swf)|*.swf"
260                                  "|all files (%s)|%s" % (allFiles, allFiles))
261
262         if dlg.ShowModal() == wx.ID_OK:
263             self.__threads["progress"] = self.__doc.SaveSWF(dlg.GetPath(),
264                                                          pages, self.options)
265
266     def OnUpdateUI(self, event):
267         menubar = self.view.GetMenuBar()
268         menubar.Enable(event.GetId(), self.__can_save)
269
270         self.view.GetToolBar().EnableTool(event.GetId(), self.__can_save)
271
272     def OnMenuSaveSelected(self, event):
273         pages = []
274         page = self.view.page_list.GetFirstSelected()
275         pages.append(page+1)
276
277         while True:
278             page = self.view.page_list.GetNextSelected(page)
279             if page == -1:
280                 break
281             pages.append(page+1)
282
283         self.OnMenuSave(event, pages)
284
285     def OnMenuExit(self, event):
286         self.view.SetStatusText(u"Cleaning up...")
287
288         # Stop any running thread
289         self.__StopThreads()
290
291         config = GetConfig()
292         self.view.filehistory.Save(config)
293         config.Flush()
294         # A little extra cleanup is required for the FileHistory control
295         del self.view.filehistory
296
297         # Save quality options
298         dirpath = GetDataDir()
299         data = self.options.quality_panel.pickle()
300         try:
301             f = file(os.path.join(dirpath, 'quality.pkl'), 'wb')
302             pickle.dump(data, f)
303             f.close()
304         except IOError:
305             pass
306
307         # Save viewer options
308         try:
309             f = file(os.path.join(dirpath, 'viewers.pkl'), 'wb')
310             data = self.options.viewers_panel.pickle()
311             pickle.dump(data, f)
312             f.close()
313         except Exception, e:
314             pass
315
316         self.view.Destroy()
317
318     def OnMenuSelectAll(self, event):
319         for i in range(0, self.view.page_list.GetItemCount()):
320             self.view.page_list.Select(i, True)
321
322     def OnMenuInvertSelection(self, event):
323         for i in range(0, self.view.page_list.GetItemCount()):
324             self.view.page_list.Select(i, not self.view.page_list.IsSelected(i))
325
326     def OnMenuSelectOdd(self, event):
327         for i in range(0, self.view.page_list.GetItemCount()):
328             self.view.page_list.Select(i, not bool(i%2))
329
330     def OnMenuSelectEven(self, event):
331         for i in range(0, self.view.page_list.GetItemCount()):
332             self.view.page_list.Select(i, bool(i%2))
333
334     def OnMenuOptions(self, event):
335         self.options.ShowModal()
336
337     def OnFit(self, event):
338         self.__doc.Fit(self.view.page_preview.GetClientSize())
339
340     def OnZoom(self, event):
341         zoom = {
342               wx.ID_ZOOM_IN: .1,
343               wx.ID_ZOOM_OUT: -.1,
344               wx.ID_ZOOM_100: 1,
345         }
346         self.__doc.Zoom(zoom[event.GetId()])
347
348     def OnAbout(self, evt):
349         AboutDialog(self.view)
350
351     def __Load(self, filename):
352         self.__can_save = True
353         self.__StopThreads()
354         self.view.SetStatusText(u"Loading document...")
355         self.__busy = wx.BusyInfo(u"One moment please, "
356                                   u"opening pdf document...")
357
358         self.view.filehistory.AddFileToHistory(filename)
359
360         # Need to delay the file load a little bit
361         # for the BusyInfo get a change to repaint itself
362         wx.FutureCall(150, self.__doc.Load, filename)
363
364     def __StopThreads(self):
365         for n, t in self.__threads.items():
366             t.Stop()
367
368         running = True
369         while running:
370             running = False
371             for n, t in self.__threads.items():
372                 running = running + t.IsRunning()
373             time.sleep(0.1)
374
375     def __ReadConfigurationFile(self):
376         config = GetConfig()
377         self.view.filehistory.Load(config)
378
379         dirpath = GetDataDir()
380         try:
381             f = file(os.path.join(dirpath, 'quality.pkl'), 'rb')
382             try:
383                 data = pickle.load(f)
384                 self.options.quality_panel.unpickle(data)
385             except:
386                 self.Message(
387                       u"Error loading quality settings. "
388                       u"They will be reset to defaults. ")
389             f.close()
390         except IOError:
391             pass
392
393         try:
394             f = file(os.path.join(dirpath, 'viewers.pkl'), 'rb')
395             try:
396                 data = pickle.load(f)
397                 self.options.viewers_panel.unpickle(data)
398             except:
399                 self.Message(
400                       u"Error loading viewers settings. "
401                       u"They will be reset to defaults. ")
402             f.close()
403         except IOError:
404             pass
405         #d = pickle.load(f)
406
407     def Message(self, message):
408         dlg = wx.MessageDialog(self.view,
409                       message,
410                       style=wx.OK, pos=wx.DefaultPosition)
411         dlg.ShowModal()
412         dlg.Destroy()
413