controller.py 24 KB

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