controller.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. """
  2. @package animation.controller
  3. @brief Animations management
  4. Classes:
  5. - controller::AnimationController
  6. (C) 2013 by the GRASS Development Team
  7. This program is free software under the GNU General Public License
  8. (>=v2). Read the file COPYING that comes with GRASS for details.
  9. @author Anna Petrasova <kratochanna gmail.com>
  10. """
  11. import os
  12. import wx
  13. from core.gcmd import GException, GError, GMessage
  14. from grass.imaging import writeAvi, writeGif, writeIms, writeSwf
  15. from core.gthread import gThread
  16. from core.settings import UserSettings
  17. from gui_core.wrap import EmptyImage, ImageFromBitmap
  18. from animation.temporal_manager import TemporalManager
  19. from animation.dialogs import InputDialog, EditDialog, ExportDialog
  20. from animation.utils import TemporalMode, TemporalType, Orientation, RenderText, WxImageToPil, \
  21. sampleCmdMatrixAndCreateNames, layerListToCmdsMatrix, HashCmds, getCpuCount
  22. from animation.data import AnimationData
  23. class AnimationController(wx.EvtHandler):
  24. def __init__(self, frame, sliders, animations, mapwindows,
  25. provider, bitmapPool, mapFilesPool):
  26. wx.EvtHandler.__init__(self)
  27. self.mapwindows = mapwindows
  28. self.frame = frame
  29. self.sliders = sliders
  30. self.slider = self.sliders['temporal']
  31. self.animationToolbar = None
  32. self.temporalMode = None
  33. self.animationData = []
  34. self.timer = wx.Timer(self, id=wx.ID_ANY)
  35. self.animations = animations
  36. self.bitmapPool = bitmapPool
  37. self.mapFilesPool = mapFilesPool
  38. self.bitmapProvider = provider
  39. for anim, win in zip(self.animations, self.mapwindows):
  40. anim.SetCallbackUpdateFrame(
  41. lambda index, dataId, win=win: self.UpdateFrame(
  42. index, win, dataId))
  43. anim.SetCallbackEndAnimation(
  44. lambda index, dataId, win=win: self.UpdateFrameEnd(
  45. index, win, dataId))
  46. anim.SetCallbackOrientationChanged(
  47. self.OrientationChangedInReverseMode)
  48. for slider in self.sliders.values():
  49. slider.SetCallbackSliderChanging(self.SliderChanging)
  50. slider.SetCallbackSliderChanged(self.SliderChanged)
  51. slider.SetCallbackFrameIndexChanged(self.ChangeFrame)
  52. self.runAfterReleasingSlider = None
  53. self.temporalManager = TemporalManager()
  54. self.Bind(wx.EVT_TIMER, self.OnTimerTick, self.timer)
  55. self.timeTick = 200
  56. self._dialogs = {}
  57. def SetAnimationToolbar(self, toolbar):
  58. self.animationToolbar = toolbar
  59. def GetTimeTick(self):
  60. return self._timeTick
  61. def SetTimeTick(self, value):
  62. self._timeTick = value
  63. if self.timer.IsRunning():
  64. self.timer.Stop()
  65. self.timer.Start(self._timeTick)
  66. self.DisableSliderIfNeeded()
  67. timeTick = property(fget=GetTimeTick, fset=SetTimeTick)
  68. def OnTimerTick(self, event):
  69. for anim in self.animations:
  70. anim.Update()
  71. def StartAnimation(self):
  72. # if self.timer.IsRunning():
  73. # self.timer.Stop()
  74. for anim in self.animations:
  75. if self.timer.IsRunning():
  76. anim.NextFrameIndex()
  77. anim.Start()
  78. if not self.timer.IsRunning():
  79. self.timer.Start(self.timeTick)
  80. self.DisableSliderIfNeeded()
  81. def PauseAnimation(self, paused):
  82. if paused:
  83. if self.timer.IsRunning():
  84. self.timer.Stop()
  85. self.DisableSliderIfNeeded()
  86. else:
  87. if not self.timer.IsRunning():
  88. self.timer.Start(self.timeTick)
  89. self.DisableSliderIfNeeded()
  90. for anim in self.animations:
  91. anim.Pause(paused)
  92. def EndAnimation(self):
  93. if self.timer.IsRunning():
  94. self.timer.Stop()
  95. self.DisableSliderIfNeeded()
  96. for anim in self.animations:
  97. anim.Stop()
  98. def UpdateFrameEnd(self, index, win, dataId):
  99. if self.timer.IsRunning():
  100. self.timer.Stop()
  101. self.DisableSliderIfNeeded()
  102. self.animationToolbar.Stop()
  103. self.UpdateFrame(index, win, dataId)
  104. def UpdateFrame(self, index, win, dataId):
  105. bitmap = self.bitmapProvider.GetBitmap(dataId)
  106. if not UserSettings.Get(group='animation', key='temporal',
  107. subkey=['nodata', 'enable']):
  108. if dataId is not None:
  109. win.DrawBitmap(bitmap)
  110. else:
  111. win.DrawBitmap(bitmap)
  112. self.slider.UpdateFrame(index)
  113. def SliderChanging(self, index):
  114. if self.runAfterReleasingSlider is None:
  115. self.runAfterReleasingSlider = self.timer.IsRunning()
  116. self.PauseAnimation(True)
  117. self.ChangeFrame(index)
  118. def SliderChanged(self):
  119. if self.runAfterReleasingSlider:
  120. self.PauseAnimation(False)
  121. self.runAfterReleasingSlider = None
  122. def ChangeFrame(self, index):
  123. for anim in self.animations:
  124. anim.FrameChangedFromOutside(index)
  125. def DisableSliderIfNeeded(self):
  126. if self.timer.IsRunning() and self._timeTick < 100:
  127. self.slider.EnableSlider(False)
  128. else:
  129. self.slider.EnableSlider(True)
  130. def OrientationChangedInReverseMode(self, mode):
  131. if mode == Orientation.FORWARD:
  132. self.animationToolbar.PlayForward()
  133. elif mode == Orientation.BACKWARD:
  134. self.animationToolbar.PlayBack()
  135. def SetReplayMode(self, mode):
  136. for anim in self.animations:
  137. anim.replayMode = mode
  138. def SetOrientation(self, mode):
  139. for anim in self.animations:
  140. anim.orientation = mode
  141. def SetTemporalMode(self, mode):
  142. self._temporalMode = mode
  143. def GetTemporalMode(self):
  144. return self._temporalMode
  145. temporalMode = property(fget=GetTemporalMode, fset=SetTemporalMode)
  146. def GetTimeGranularity(self):
  147. if self.temporalMode == TemporalMode.TEMPORAL:
  148. return self.temporalManager.GetGranularity()
  149. return None
  150. def UpdateAnimations(self):
  151. """Used sofar for updating slider time labels
  152. after change of format"""
  153. self._setAnimations()
  154. def EditAnimations(self):
  155. # running = False
  156. # if self.timer.IsRunning():
  157. # running = True
  158. self.EndAnimation()
  159. dlg = EditDialog(
  160. parent=self.frame,
  161. evalFunction=self.EvaluateInput,
  162. animationData=self.animationData,
  163. maxAnimations=len(
  164. self.animations))
  165. dlg.CenterOnParent()
  166. if dlg.ShowModal() == wx.ID_CANCEL:
  167. dlg.Destroy()
  168. return
  169. self.animationData, self.temporalMode, self.temporalManager = dlg.GetResult()
  170. dlg.Destroy()
  171. self._setAnimations()
  172. def AddAnimation(self):
  173. # check if we can add more animations
  174. found = False
  175. indices = [anim.windowIndex for anim in self.animationData]
  176. for windowIndex in range(len(self.animations)):
  177. if windowIndex not in indices:
  178. found = True
  179. break
  180. if not found:
  181. GMessage(
  182. parent=self.frame,
  183. message=_("Maximum number of animations is %s.") %
  184. len(self.animations))
  185. return
  186. # running = False
  187. # if self.timer.IsRunning():
  188. # running = True
  189. self.EndAnimation()
  190. # self.PauseAnimation(True)
  191. animData = AnimationData()
  192. # number of active animations
  193. animationIndex = len(
  194. [anim for anim in self.animations if anim.IsActive()])
  195. animData.SetDefaultValues(windowIndex, animationIndex)
  196. dlg = InputDialog(
  197. parent=self.frame,
  198. mode='add',
  199. animationData=animData)
  200. dlg.CenterOnParent()
  201. if dlg.ShowModal() == wx.ID_CANCEL:
  202. dlg.UnInit()
  203. dlg.Destroy()
  204. return
  205. dlg.Destroy()
  206. # check compatibility
  207. if animData.windowIndex in indices:
  208. GMessage(
  209. parent=self.frame, message=_(
  210. "More animations are using one window."
  211. " Please select different window for each animation."))
  212. return
  213. try:
  214. temporalMode, tempManager = self.EvaluateInput(
  215. self.animationData + [animData])
  216. except GException as e:
  217. GError(parent=self.frame, message=e.value, showTraceback=False)
  218. return
  219. # if ok, set temporal mode
  220. self.temporalMode = temporalMode
  221. self.temporalManager = tempManager
  222. # add data
  223. windowIndex = animData.windowIndex
  224. self.animationData.append(animData)
  225. self._setAnimations()
  226. def SetAnimations(self, layerLists):
  227. """Set animation data directly.
  228. :param layerLists: list of layerLists
  229. """
  230. try:
  231. animationData = []
  232. for i in range(len(self.animations)):
  233. if layerLists[i]:
  234. anim = AnimationData()
  235. anim.SetDefaultValues(i, i)
  236. anim.SetLayerList(layerLists[i])
  237. animationData.append(anim)
  238. except (GException, ValueError, IOError) as e:
  239. GError(parent=self.frame, message=str(e),
  240. showTraceback=False, caption=_("Invalid input"))
  241. return
  242. try:
  243. temporalMode, tempManager = self.EvaluateInput(animationData)
  244. except GException as e:
  245. GError(parent=self.frame, message=e.value, showTraceback=False)
  246. return
  247. self.animationData = animationData
  248. self.temporalManager = tempManager
  249. self.temporalMode = temporalMode
  250. self._setAnimations()
  251. def _setAnimations(self):
  252. indices = [anim.windowIndex for anim in self.animationData]
  253. self._updateWindows(activeIndices=indices)
  254. if self.temporalMode == TemporalMode.TEMPORAL:
  255. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  256. else:
  257. timeLabels, mapNamesDict = None, None
  258. for anim in self.animationData:
  259. if anim.viewMode == '2d':
  260. anim.cmdMatrix = layerListToCmdsMatrix(anim.layerList)
  261. else:
  262. anim.cmdMatrix = [(cmd,)
  263. for cmd in anim.GetNvizCommands()
  264. ['commands']]
  265. self._updateSlider(timeLabels=timeLabels)
  266. self._updateAnimations(
  267. activeIndices=indices,
  268. mapNamesDict=mapNamesDict)
  269. self._updateBitmapData()
  270. # if running:
  271. # self.PauseAnimation(False)
  272. # # self.StartAnimation()
  273. # else:
  274. self.EndAnimation()
  275. def _updateSlider(self, timeLabels=None):
  276. if self.temporalMode == TemporalMode.NONTEMPORAL:
  277. self.frame.SetSlider('nontemporal')
  278. self.slider = self.sliders['nontemporal']
  279. frameCount = self.animationData[0].mapCount
  280. self.slider.SetFrames(frameCount)
  281. elif self.temporalMode == TemporalMode.TEMPORAL:
  282. self.frame.SetSlider('temporal')
  283. self.slider = self.sliders['temporal']
  284. self.slider.SetTemporalType(self.temporalManager.temporalType)
  285. self.slider.SetFrames(timeLabels)
  286. else:
  287. self.frame.SetSlider(None)
  288. self.slider = None
  289. def _updateAnimations(self, activeIndices, mapNamesDict=None):
  290. if self.temporalMode == TemporalMode.NONTEMPORAL:
  291. for i in range(len(self.animations)):
  292. if i not in activeIndices:
  293. self.animations[i].SetActive(False)
  294. continue
  295. anim = [anim for anim in self.animationData
  296. if anim.windowIndex == i][0]
  297. w, h = self.mapwindows[i].GetClientSize()
  298. regions = anim.GetRegions(w, h)
  299. self.animations[i].SetFrames(
  300. [HashCmds(cmdList, region) for cmdList,
  301. region in zip(anim.cmdMatrix, regions)])
  302. self.animations[i].SetActive(True)
  303. else:
  304. for i in range(len(self.animations)):
  305. if i not in activeIndices:
  306. self.animations[i].SetActive(False)
  307. continue
  308. anim = [anim for anim in self.animationData
  309. if anim.windowIndex == i][0]
  310. w, h = self.mapwindows[i].GetClientSize()
  311. regions = anim.GetRegions(w, h)
  312. identifiers = sampleCmdMatrixAndCreateNames(
  313. anim.cmdMatrix, mapNamesDict[
  314. anim.firstStdsNameType[0]], regions)
  315. self.animations[i].SetFrames(identifiers)
  316. self.animations[i].SetActive(True)
  317. def _updateWindows(self, activeIndices):
  318. # add or remove window
  319. for windowIndex in range(len(self.animations)):
  320. if not self.frame.IsWindowShown(
  321. windowIndex) and windowIndex in activeIndices:
  322. self.frame.AddWindow(windowIndex)
  323. elif self.frame.IsWindowShown(windowIndex) and windowIndex not in activeIndices:
  324. self.frame.RemoveWindow(windowIndex)
  325. def _updateBitmapData(self):
  326. # unload previous data
  327. self.bitmapProvider.Unload()
  328. # load new data
  329. for animData in self.animationData:
  330. if animData.viewMode == '2d':
  331. self._set2DData(animData)
  332. else:
  333. self._load3DData(animData)
  334. self._loadLegend(animData)
  335. color = UserSettings.Get(
  336. group='animation',
  337. key='bgcolor',
  338. subkey='color')
  339. cpus = UserSettings.Get(
  340. group='animation',
  341. key='nprocs',
  342. subkey='value')
  343. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color)
  344. # clear pools
  345. self.bitmapPool.Clear()
  346. self.mapFilesPool.Clear()
  347. def _set2DData(self, animationData):
  348. opacities = [
  349. layer.opacity for layer in animationData.layerList
  350. if layer.active]
  351. w, h = self.mapwindows[animationData.GetWindowIndex()].GetClientSize()
  352. regions = animationData.GetRegions(w, h)
  353. self.bitmapProvider.SetCmds(
  354. animationData.cmdMatrix, opacities, regions)
  355. def _load3DData(self, animationData):
  356. nviz = animationData.GetNvizCommands()
  357. self.bitmapProvider.SetCmds3D(nviz['commands'], nviz['region'])
  358. def _loadLegend(self, animationData):
  359. if animationData.legendCmd:
  360. try:
  361. bitmap = self.bitmapProvider.LoadOverlay(
  362. animationData.legendCmd)
  363. try:
  364. from PIL import Image
  365. for param in animationData.legendCmd:
  366. if param.startswith('at'):
  367. b, t, l, r = param.split('=')[1].split(',')
  368. x, y = float(l) / 100., 1 - float(t) / 100.
  369. break
  370. except ImportError:
  371. x, y = 0, 0
  372. self.mapwindows[
  373. animationData.windowIndex].SetOverlay(
  374. bitmap, x, y)
  375. except GException:
  376. GError(message=_("Failed to display legend."))
  377. else:
  378. self.mapwindows[animationData.windowIndex].ClearOverlay()
  379. def EvaluateInput(self, animationData):
  380. stds = 0
  381. maps = 0
  382. mapCount = set()
  383. tempManager = None
  384. windowIndex = []
  385. for anim in animationData:
  386. for layer in anim.layerList:
  387. if layer.active and hasattr(layer, 'maps'):
  388. if layer.mapType in ('strds', 'stvds', 'str3ds'):
  389. stds += 1
  390. else:
  391. maps += 1
  392. mapCount.add(len(layer.maps))
  393. windowIndex.append(anim.windowIndex)
  394. if maps and stds:
  395. temporalMode = TemporalMode.NONTEMPORAL
  396. elif maps:
  397. temporalMode = TemporalMode.NONTEMPORAL
  398. elif stds:
  399. temporalMode = TemporalMode.TEMPORAL
  400. else:
  401. temporalMode = None
  402. if temporalMode == TemporalMode.NONTEMPORAL:
  403. if len(mapCount) > 1:
  404. raise GException(
  405. _("Inconsistent number of maps, please check input data."))
  406. elif temporalMode == TemporalMode.TEMPORAL:
  407. tempManager = TemporalManager()
  408. # these raise GException:
  409. for anim in animationData:
  410. tempManager.AddTimeSeries(*anim.firstStdsNameType)
  411. message = tempManager.EvaluateInputData()
  412. if message:
  413. GMessage(parent=self.frame, message=message)
  414. return temporalMode, tempManager
  415. def Reload(self):
  416. self.EndAnimation()
  417. color = UserSettings.Get(
  418. group='animation',
  419. key='bgcolor',
  420. subkey='color')
  421. cpus = UserSettings.Get(
  422. group='animation',
  423. key='nprocs',
  424. subkey='value')
  425. self.bitmapProvider.Load(nprocs=cpus, bgcolor=color, force=True)
  426. self.EndAnimation()
  427. def Export(self):
  428. if not self.animationData:
  429. GMessage(parent=self.frame, message=_("No animation to export."))
  430. return
  431. if 'export' in self._dialogs:
  432. self._dialogs['export'].Show()
  433. self._dialogs['export'].Raise()
  434. else:
  435. dlg = ExportDialog(self.frame, temporal=self.temporalMode,
  436. timeTick=self.timeTick)
  437. dlg.CenterOnParent()
  438. dlg.doExport.connect(self._export)
  439. self._dialogs['export'] = dlg
  440. dlg.Show()
  441. def _export(self, exportInfo, decorations):
  442. size = self.frame.animationPanel.GetSize()
  443. if self.temporalMode == TemporalMode.TEMPORAL:
  444. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  445. frameCount = len(timeLabels)
  446. else:
  447. frameCount = self.animationData[
  448. 0].mapCount # should be the same for all
  449. animWinSize = []
  450. animWinPos = []
  451. animWinIndex = []
  452. legends = [anim.legendCmd for anim in self.animationData]
  453. # determine position and sizes of bitmaps
  454. for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
  455. if anim.IsActive():
  456. pos = win.GetPosition()
  457. animWinPos.append(pos)
  458. animWinSize.append(win.GetSize())
  459. animWinIndex.append(i)
  460. images = []
  461. busy = wx.BusyInfo(
  462. _("Preparing export, please wait..."),
  463. parent=self.frame)
  464. wx.GetApp().Yield()
  465. lastBitmaps = {}
  466. fgcolor = UserSettings.Get(
  467. group='animation',
  468. key='font',
  469. subkey='fgcolor')
  470. bgcolor = UserSettings.Get(
  471. group='animation',
  472. key='font',
  473. subkey='bgcolor')
  474. for frameIndex in range(frameCount):
  475. image = EmptyImage(*size)
  476. image.Replace(0, 0, 0, 255, 255, 255)
  477. # collect bitmaps of all windows and paste them into the one
  478. for i in animWinIndex:
  479. frameId = self.animations[i].GetFrame(frameIndex)
  480. if not UserSettings.Get(group='animation', key='temporal',
  481. subkey=['nodata', 'enable']):
  482. if frameId is not None:
  483. bitmap = self.bitmapProvider.GetBitmap(frameId)
  484. lastBitmaps[i] = bitmap
  485. else:
  486. if i not in lastBitmaps:
  487. lastBitmaps[i] = wx.NullBitmap()
  488. else:
  489. bitmap = self.bitmapProvider.GetBitmap(frameId)
  490. lastBitmaps[i] = bitmap
  491. im = ImageFromBitmap(lastBitmaps[i])
  492. # add legend if used
  493. legend = legends[i]
  494. if legend:
  495. legendBitmap = self.bitmapProvider.LoadOverlay(legend)
  496. x, y = self.mapwindows[i].GetOverlayPos()
  497. legImage = ImageFromBitmap(legendBitmap)
  498. # not so nice result, can we handle the transparency
  499. # otherwise?
  500. legImage.ConvertAlphaToMask()
  501. im.Paste(legImage, x, y)
  502. if im.GetSize() != animWinSize[i]:
  503. im.Rescale(*animWinSize[i])
  504. image.Paste(im, *animWinPos[i])
  505. # paste decorations
  506. for decoration in decorations:
  507. # add image
  508. x = decoration['pos'][0] / 100. * size[0]
  509. y = decoration['pos'][1] / 100. * size[1]
  510. if decoration['name'] == 'image':
  511. decImage = wx.Image(decoration['file'])
  512. elif decoration['name'] == 'time':
  513. timeLabel = timeLabels[frameIndex]
  514. if timeLabel[1]: # interval
  515. text = _("%(from)s %(dash)s %(to)s") % {
  516. 'from': timeLabel[0],
  517. 'dash': u"\u2013", 'to': timeLabel[1]}
  518. else:
  519. if self.temporalManager.GetTemporalType() == TemporalType.ABSOLUTE:
  520. text = timeLabel[0]
  521. else:
  522. text = _("%(start)s %(unit)s") % \
  523. {'start': timeLabel[0], 'unit': timeLabel[2]}
  524. decImage = RenderText(
  525. text, decoration['font'],
  526. bgcolor, fgcolor).ConvertToImage()
  527. elif decoration['name'] == 'text':
  528. text = decoration['text']
  529. decImage = RenderText(
  530. text, decoration['font'],
  531. bgcolor, fgcolor).ConvertToImage()
  532. image.Paste(decImage, x, y)
  533. images.append(image)
  534. del busy
  535. # export
  536. pilImages = [WxImageToPil(image) for image in images]
  537. self.busy = wx.BusyInfo(_("Exporting animation, please wait..."),
  538. parent=self.frame)
  539. wx.GetApp().Yield()
  540. try:
  541. def export_avi_callback(event):
  542. error = event.ret
  543. del self.busy
  544. if error:
  545. GError(parent=self.frame, message=error)
  546. return
  547. if exportInfo['method'] == 'sequence':
  548. filename = os.path.join(
  549. exportInfo['directory'],
  550. exportInfo['prefix'] +
  551. '.' +
  552. exportInfo['format'].lower())
  553. writeIms(filename=filename, images=pilImages)
  554. elif exportInfo['method'] == 'gif':
  555. writeGif(filename=exportInfo['file'], images=pilImages,
  556. duration=self.timeTick / float(1000), repeat=True)
  557. elif exportInfo['method'] == 'swf':
  558. writeSwf(filename=exportInfo['file'], images=pilImages,
  559. duration=self.timeTick / float(1000), repeat=True)
  560. elif exportInfo['method'] == 'avi':
  561. thread = gThread()
  562. thread.Run(callable=writeAvi,
  563. filename=exportInfo['file'],
  564. images=pilImages,
  565. duration=self.timeTick / float(1000),
  566. encoding=exportInfo['encoding'],
  567. inputOptions=exportInfo['options'],
  568. bg_task=True,
  569. ondone=export_avi_callback,
  570. )
  571. except Exception as e:
  572. del self.busy
  573. GError(parent=self.frame, message=str(e))
  574. return
  575. if exportInfo['method'] in ('sequence', 'gif', 'swf'):
  576. del self.busy