datacatalog.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. """
  2. @package lmgr::datacatalog
  3. @brief Data catalog
  4. Classes:
  5. - datacatalog::DataCatalog
  6. - datacatalog::LocationMapTree
  7. - datacatalog::DataCatalogTree
  8. @todo:
  9. - use gui_core/treeview.py
  10. (C) 2014 by Tereza Fiedlerova, and the GRASS Development Team
  11. This program is free software under the GNU General Public
  12. License (>=v2). Read the file COPYING that comes with GRASS
  13. for details.
  14. @author Tereza Fiedlerova
  15. """
  16. import os
  17. import sys
  18. import wx
  19. import wx.gizmos as gizmos
  20. from core.gcmd import RunCommand, GError, GMessage
  21. from core.utils import GetListOfLocations, ListOfMapsets
  22. from core.gthread import gThread
  23. from core.debug import Debug
  24. from gui_core.dialogs import TextEntryDialog
  25. from grass.pydispatch.signal import Signal
  26. import grass.script as grass
  27. class DataCatalog(wx.Panel):
  28. """Data catalog panel"""
  29. def __init__(self, parent, giface=None, id = wx.ID_ANY, title=_("Data catalog"),
  30. name='catalog', **kwargs):
  31. """Panel constructor """
  32. self.showNotification = Signal('DataCatalog.showNotification')
  33. self.parent = parent
  34. self.baseTitle = title
  35. wx.Panel.__init__(self, parent = parent, id = id, **kwargs)
  36. self.SetName("DataCatalog")
  37. Debug.msg(1, "DataCatalog.__init__()")
  38. # tree with layers
  39. self.tree = DataCatalogTree(self)
  40. self.thread = gThread()
  41. self._loaded = False
  42. self.tree.showNotification.connect(self.showNotification)
  43. # some layout
  44. self._layout()
  45. def _layout(self):
  46. """Do layout"""
  47. sizer = wx.BoxSizer(wx.VERTICAL)
  48. sizer.Add(item = self.tree.GetControl(), proportion = 1,
  49. flag = wx.EXPAND)
  50. self.SetAutoLayout(True)
  51. self.SetSizer(sizer)
  52. self.Layout()
  53. def LoadItems(self):
  54. if self._loaded:
  55. return
  56. self.thread.Run(callable=self.tree.InitTreeItems,
  57. ondone=lambda event: self.LoadItemsDone())
  58. def LoadItemsDone(self):
  59. self._loaded = True
  60. self.tree.ExpandCurrentLocation()
  61. class LocationMapTree(wx.TreeCtrl):
  62. def __init__(self, parent, style=wx.TR_HIDE_ROOT | wx.TR_EDIT_LABELS |
  63. wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_COLUMN_LINES | wx.TR_SINGLE):
  64. """Location Map Tree constructor."""
  65. super(LocationMapTree, self).__init__(parent, id=wx.ID_ANY, style=style)
  66. self.showNotification = Signal('Tree.showNotification')
  67. self.parent = parent
  68. self.root = self.AddRoot('Catalog') # will not be displayed when we use TR_HIDE_ROOT flag
  69. self._initVariables()
  70. self.MakeBackup()
  71. wx.EVT_TREE_ITEM_RIGHT_CLICK(self, wx.ID_ANY, self.OnRightClick)
  72. self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)
  73. self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
  74. self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
  75. def _initTreeItems(self, locations = [], mapsets = []):
  76. """Add locations, mapsets and layers to the tree."""
  77. if not locations:
  78. locations = GetListOfLocations(self.gisdbase)
  79. if not mapsets:
  80. mapsets = ['*']
  81. first = True
  82. for loc in locations:
  83. location = loc
  84. if first:
  85. self.ChangeEnvironment(location, 'PERMANENT')
  86. first = False
  87. else:
  88. self.ChangeEnvironment(location)
  89. varloc = self.AppendItem(self.root, loc)
  90. # add all mapsets
  91. for mapset in ListOfMapsets():
  92. self.AppendItem(varloc, mapset)
  93. # get list of all maps in location
  94. maplist = RunCommand('g.list', flags='mt', type='rast,rast3d,vect', mapset=','.join(mapsets),
  95. quiet=True, read=True)
  96. maplist = maplist.splitlines()
  97. for ml in maplist:
  98. # parse
  99. parts1 = ml.split('/')
  100. parts2 = parts1[1].split('@')
  101. mapset = parts2[1]
  102. mlayer = parts2[0]
  103. ltype = parts1[0]
  104. # add mapset
  105. if self.itemExists(mapset, varloc) == False:
  106. varmapset = self.AppendItem(varloc, mapset)
  107. else:
  108. varmapset = self.getItemByName(mapset, varloc)
  109. # add type node if not exists
  110. if self.itemExists(ltype, varmapset) == False:
  111. vartype = self.AppendItem(varmapset, ltype)
  112. self.AppendItem(vartype, mlayer)
  113. self.RestoreBackup()
  114. Debug.msg(1, "Tree filled")
  115. def InitTreeItems(self):
  116. """Create popup menu for layers"""
  117. raise NotImplementedError()
  118. def _popupMenuLayer(self):
  119. """Create popup menu for layers"""
  120. raise NotImplementedError()
  121. def _popupMenuMapset(self):
  122. """Create popup menu for mapsets"""
  123. raise NotImplementedError()
  124. def _initVariables(self):
  125. """Init variables."""
  126. self.selected_layer = None
  127. self.selected_type = None
  128. self.selected_mapset = None
  129. self.selected_location = None
  130. self.gisdbase = grass.gisenv()['GISDBASE']
  131. self.ctrldown = False
  132. def GetControl(self):
  133. """Returns control itself."""
  134. return self
  135. def DefineItems(self, item0):
  136. """Set selected items."""
  137. self.selected_layer = None
  138. self.selected_type = None
  139. self.selected_mapset = None
  140. self.selected_location = None
  141. items = []
  142. item = item0
  143. while (self.GetItemParent(item)):
  144. items.insert(0,item)
  145. item = self.GetItemParent(item)
  146. self.selected_location = items[0]
  147. length = len(items)
  148. if (length > 1):
  149. self.selected_mapset = items[1]
  150. if (length > 2):
  151. self.selected_type = items[2]
  152. if (length > 3):
  153. self.selected_layer = items[3]
  154. def getItemByName(self, match, root):
  155. """Return match item from the root."""
  156. item, cookie = self.GetFirstChild(root)
  157. while item.IsOk():
  158. if self.GetItemText(item) == match:
  159. return item
  160. item, cookie = self.GetNextChild(root, cookie)
  161. return None
  162. def itemExists(self, match, root):
  163. """Return true if match item exists in the root item."""
  164. item, cookie = self.GetFirstChild(root)
  165. while item.IsOk():
  166. if self.GetItemText(item) == match:
  167. return True
  168. item, cookie = self.GetNextChild(root, cookie)
  169. return False
  170. def UpdateTree(self):
  171. """Update whole tree."""
  172. self.DeleteAllItems()
  173. self.root = self.AddRoot('Tree')
  174. self.AddTreeItems()
  175. label = "Tree updated."
  176. self.showNotification.emit(message=label)
  177. def OnSelChanged(self, event):
  178. self.selected_layer = None
  179. def OnRightClick(self, event):
  180. """Display popup menu."""
  181. self.DefineItems(event.GetItem())
  182. if(self.selected_layer):
  183. self._popupMenuLayer()
  184. elif(self.selected_mapset and self.selected_type==None):
  185. self._popupMenuMapset()
  186. def OnDoubleClick(self, event):
  187. """Double click"""
  188. Debug.msg(1, "Double CLICK")
  189. def OnKeyDown(self, event):
  190. """Set key event and check if control key is down"""
  191. keycode = event.GetKeyCode()
  192. if keycode == wx.WXK_CONTROL:
  193. self.ctrldown = True
  194. Debug.msg(1,"CONTROL ON")
  195. def OnKeyUp(self, event):
  196. """Check if control key is up"""
  197. keycode = event.GetKeyCode()
  198. if keycode == wx.WXK_CONTROL:
  199. self.ctrldown = False
  200. Debug.msg(1,"CONTROL OFF")
  201. def MakeBackup(self):
  202. """Make backup for case of change"""
  203. gisenv = grass.gisenv()
  204. self.glocation = gisenv['LOCATION_NAME']
  205. self.gmapset = gisenv['MAPSET']
  206. def RestoreBackup(self):
  207. """Restore backup"""
  208. stringl = 'LOCATION_NAME='+self.glocation
  209. RunCommand('g.gisenv', set=stringl)
  210. stringm = 'MAPSET='+self.gmapset
  211. RunCommand('g.gisenv', set=stringm)
  212. def ChangeEnvironment(self, location, mapset=None):
  213. """Change gisenv variables -> location, mapset"""
  214. stringl = 'LOCATION_NAME='+location
  215. RunCommand('g.gisenv', set=stringl)
  216. if mapset:
  217. stringm = 'MAPSET='+mapset
  218. RunCommand('g.gisenv', set=stringm)
  219. def ExpandCurrentLocation(self):
  220. """Expand current location"""
  221. location = grass.gisenv()['LOCATION_NAME']
  222. item = self.getItemByName(location, self.root)
  223. if item is not None:
  224. self.SelectItem(item)
  225. self.ExpandAllChildren(item)
  226. self.EnsureVisible(item)
  227. else:
  228. Debug.msg(1, "Location <%s> not found" % location)
  229. class DataCatalogTree(LocationMapTree):
  230. def __init__(self, parent):
  231. """Data Catalog Tree constructor."""
  232. super(DataCatalogTree, self).__init__(parent)
  233. self._initVariablesCatalog()
  234. wx.EVT_TREE_BEGIN_DRAG(self, wx.ID_ANY, self.OnBeginDrag)
  235. wx.EVT_TREE_END_DRAG(self, wx.ID_ANY, self.OnEndDrag)
  236. wx.EVT_TREE_END_LABEL_EDIT(self, wx.ID_ANY, self.OnEditLabel)
  237. wx.EVT_TREE_BEGIN_LABEL_EDIT(self, wx.ID_ANY, self.OnStartEditLabel)
  238. def _initVariablesCatalog(self):
  239. """Init variables."""
  240. self.copy_layer = None
  241. self.copy_type = None
  242. self.copy_mapset = None
  243. self.copy_location = None
  244. def InitTreeItems(self):
  245. """Add locations, mapsets and layers to the tree."""
  246. self._initTreeItems()
  247. def OnCopy(self, event):
  248. """Copy layer or mapset (just save it temporarily, copying is done by paste)"""
  249. self.copy_layer = self.selected_layer
  250. self.copy_type = self.selected_type
  251. self.copy_mapset = self.selected_mapset
  252. self.copy_location = self.selected_location
  253. label = "Layer "+self.GetItemText(self.copy_layer)+" copied to clipboard. You can paste it to selected mapset."
  254. self.showNotification.emit(message=label)
  255. def OnRename(self, event):
  256. """Rename levent with dialog"""
  257. if (self.selected_layer):
  258. self.old_name = self.GetItemText(self.selected_layer)
  259. self.new_name = self._getUserEntry(_('New name'), _('Rename map'), self.old_name)
  260. self.rename()
  261. def OnStartEditLabel(self, event):
  262. """Start label editing"""
  263. item = event.GetItem()
  264. self.DefineItems(item)
  265. Debug.msg(1, "Start label edit "+self.GetItemText(item))
  266. label = _("Editing") + " " + self.GetItemText(item)
  267. self.showNotification.emit(message=label)
  268. if (self.selected_layer == None):
  269. event.Veto()
  270. def OnEditLabel(self, event):
  271. """End label editing"""
  272. if (self.selected_layer):
  273. item = event.GetItem()
  274. self.old_name = self.GetItemText(item)
  275. Debug.msg(1, "End label edit "+self.old_name)
  276. wx.CallAfter(self.afterEdit, self, item)
  277. def afterEdit(pro, self, item):
  278. self.new_name = self.GetItemText(item)
  279. self.rename()
  280. def rename(self):
  281. """Rename layer"""
  282. if self.selected_layer and self.new_name:
  283. string = self.old_name+','+self.new_name
  284. self.ChangeEnvironment(self.GetItemText(self.selected_location), self.GetItemText(self.selected_mapset))
  285. renamed = 0
  286. label = _("Renaming") + " " + string + " ..."
  287. self.showNotification.emit(message=label)
  288. if (self.GetItemText(self.selected_type)=='vector'):
  289. renamed = RunCommand('g.rename', vect=string)
  290. elif (self.GetItemText(self.selected_type)=='raster'):
  291. renamed = RunCommand('g.rename', rast=string)
  292. else:
  293. renamed = RunCommand('g.rename', rast3d=string)
  294. if (renamed==0):
  295. self.SetItemText(self.selected_layer,self.new_name)
  296. label = "g.rename "+self.GetItemText(self.selected_type)+"="+string+" -- completed"
  297. self.showNotification.emit(message=label)
  298. Debug.msg(1,"LAYER RENAMED TO: "+self.new_name)
  299. self.RestoreBackup()
  300. def OnPaste(self, event):
  301. """Paste layer or mapset"""
  302. # copying between mapsets of one location
  303. if (self.copy_layer == None):
  304. return
  305. if (self.selected_location == self.copy_location and self.selected_mapset):
  306. if (self.selected_type != None):
  307. if (self.GetItemText(self.copy_type) != self.GetItemText(self.selected_type)): # copy raster to vector or vice versa
  308. GError(_("Failed to copy layer: invalid type."), parent = self)
  309. return
  310. self.new_name = self._getUserEntry(_('New name'), _('Copy map'),
  311. self.GetItemText(self.copy_layer) + '_copy')
  312. if not self.new_name:
  313. return
  314. if (self.GetItemText(self.copy_layer) == self.new_name):
  315. GMessage(_("Layer was not copied: new layer has the same name"), parent=self)
  316. return
  317. string = self.GetItemText(self.copy_layer)+'@'+self.GetItemText(self.copy_mapset)+','+self.new_name
  318. self.ChangeEnvironment(self.GetItemText(self.selected_location), self.GetItemText(self.selected_mapset))
  319. pasted = 0
  320. type = None
  321. label = _("Copying") + " " + string + " ..."
  322. self.showNotification.emit(message=label)
  323. if (self.GetItemText(self.copy_type)=='vector'):
  324. pasted = RunCommand('g.copy', vect=string)
  325. node = 'vector'
  326. elif (self.GetItemText(self.copy_type)=='raster'):
  327. pasted = RunCommand('g.copy', rast=string)
  328. node = 'raster'
  329. else:
  330. pasted = RunCommand('g.copy', rast3d=string)
  331. node = '3draster'
  332. if pasted == 0:
  333. if self.selected_type == None:
  334. self.selected_type = self.getItemByName(node, self.selected_mapset)
  335. if self.selected_type == None:
  336. # add type node if not exists
  337. self.selected_type = self.AppendItem(self.selected_mapset, node)
  338. self.AppendItem(self.selected_type,self.new_name)
  339. self.SortChildren(self.selected_type)
  340. Debug.msg(1,"COPIED TO: "+self.new_name)
  341. label = "g.copy "+self.GetItemText(self.copy_type)+"="+string+" -- completed" # generate this message (command) automatically?
  342. self.showNotification.emit(message=label)
  343. else:
  344. GError(_("Failed to copy layer: action is allowed only within the same location."),
  345. parent=self)
  346. # expand selected mapset
  347. self.ExpandAllChildren(self.selected_mapset)
  348. self.EnsureVisible(self.selected_mapset)
  349. self.RestoreBackup()
  350. def OnDelete(self, event):
  351. """Delete layer or mapset"""
  352. if (self.selected_layer):
  353. string = self.GetItemText(self.selected_layer)
  354. self.ChangeEnvironment(self.GetItemText(self.selected_location), self.GetItemText(self.selected_mapset))
  355. removed = 0
  356. # TODO: rewrite this that it will tell map type in the dialog
  357. if (self._confirmDialog(question=_('Do you really want to delete map <{m}>?').format(m=string),
  358. title=_('Delete map')) == wx.ID_YES):
  359. label = _("Deleting") + " " + string + " ..."
  360. self.showNotification.emit(message=label)
  361. if (self.GetItemText(self.selected_type)=='vector'):
  362. removed = RunCommand('g.remove', flags='f', type='vector',
  363. name=string)
  364. elif (self.GetItemText(self.selected_type)=='raster'):
  365. removed = RunCommand('g.remove', flags='f', type='raster',
  366. name=string)
  367. else:
  368. removed = RunCommand('g.remove', flags='f', type='3draster',
  369. name=string)
  370. if (removed==0):
  371. self.Delete(self.selected_layer)
  372. Debug.msg(1,"LAYER "+string+" DELETED")
  373. label = "g.remove -f type="+self.GetItemText(self.selected_type)+" name="+string+" -- completed" # generate this message (command) automatically?
  374. self.showNotification.emit(message=label)
  375. self.RestoreBackup()
  376. def OnDisplayLayer(self, event):
  377. """Display layer in current graphics view"""
  378. layerName = []
  379. if (self.GetItemText(self.selected_location) == self.glocation and self.selected_mapset):
  380. string = self.GetItemText(self.selected_layer)+'@'+self.GetItemText(self.selected_mapset)
  381. layerName.append(string)
  382. label = _("Displaying") + " " + string + " ..."
  383. self.showNotification.emit(message=label)
  384. label = "d."+self.GetItemText(self.selected_type)+" --q map="+string+" -- completed. Go to Map layers for further operations."
  385. if (self.GetItemText(self.selected_type)=='vector'):
  386. self.parent.parent.AddMaps(layerName, 'vector', True)
  387. elif (self.GetItemText(self.selected_type)=='raster'):
  388. self.parent.parent.AddMaps(layerName, 'raster', True)
  389. else:
  390. self.parent.parent.AddMaps(layerName, '3draster', True)
  391. label = "d.rast --q map="+string+" -- completed. Go to 'Map layers' for further operations." # generate this message (command) automatically?
  392. self.showNotification.emit(message=label)
  393. Debug.msg(1,"LAYER "+self.GetItemText(self.selected_layer)+" DISPLAYED")
  394. else:
  395. GError(_("Failed to display layer: not in current mapset or invalid layer"),
  396. parent = self)
  397. def OnBeginDrag(self, event):
  398. """Just copy necessary data"""
  399. if (self.ctrldown):
  400. #cursor = wx.StockCursor(wx.CURSOR_HAND)
  401. #self.SetCursor(cursor)
  402. event.Allow()
  403. self.DefineItems(event.GetItem())
  404. self.OnCopy(event)
  405. Debug.msg(1,"DRAG")
  406. else:
  407. event.Veto()
  408. Debug.msg(1,"DRAGGING without ctrl key does nothing")
  409. def OnEndDrag(self, event):
  410. """Copy layer into target"""
  411. #cursor = wx.StockCursor(wx.CURSOR_ARROW)
  412. #self.SetCursor(cursor)
  413. if (event.GetItem()):
  414. self.DefineItems(event.GetItem())
  415. if (self.selected_location == self.copy_location and self.selected_mapset):
  416. event.Allow()
  417. self.OnPaste(event)
  418. self.ctrldown = False
  419. #cursor = wx.StockCursor(wx.CURSOR_DEFAULT)
  420. #self.SetCursor(cursor) # TODO: change cursor while dragging and then back, this is not working
  421. Debug.msg(1,"DROP DONE")
  422. else:
  423. event.Veto()
  424. def _getUserEntry(self, message, title, value):
  425. """Dialog for simple text entry"""
  426. dlg = TextEntryDialog(self, message, title)
  427. dlg.SetValue(value)
  428. if dlg.ShowModal() == wx.ID_OK:
  429. name = dlg.GetValue()
  430. else:
  431. name = None
  432. dlg.Destroy()
  433. return name
  434. def _confirmDialog(self, question, title):
  435. """Confirm dialog"""
  436. dlg = wx.MessageDialog(self, question, title, wx.YES_NO)
  437. res = dlg.ShowModal()
  438. dlg.Destroy()
  439. return res
  440. def _popupMenuLayer(self):
  441. """Create popup menu for layers"""
  442. menu = wx.Menu()
  443. item = wx.MenuItem(menu, wx.NewId(), _("&Copy"))
  444. menu.AppendItem(item)
  445. self.Bind(wx.EVT_MENU, self.OnCopy, item)
  446. item = wx.MenuItem(menu, wx.NewId(), _("&Paste"))
  447. menu.AppendItem(item)
  448. self.Bind(wx.EVT_MENU, self.OnPaste, item)
  449. item = wx.MenuItem(menu, wx.NewId(), _("&Delete"))
  450. menu.AppendItem(item)
  451. self.Bind(wx.EVT_MENU, self.OnDelete, item)
  452. item = wx.MenuItem(menu, wx.NewId(), _("&Rename"))
  453. menu.AppendItem(item)
  454. self.Bind(wx.EVT_MENU, self.OnRename, item)
  455. item = wx.MenuItem(menu, wx.NewId(), _("&Display layer"))
  456. menu.AppendItem(item)
  457. self.Bind(wx.EVT_MENU, self.OnDisplayLayer, item)
  458. self.PopupMenu(menu)
  459. menu.Destroy()
  460. def _popupMenuMapset(self):
  461. """Create popup menu for mapsets"""
  462. menu = wx.Menu()
  463. item = wx.MenuItem(menu, wx.NewId(), _("&Paste"))
  464. menu.AppendItem(item)
  465. self.Bind(wx.EVT_MENU, self.OnPaste, item)
  466. self.PopupMenu(menu)
  467. menu.Destroy()
  468. # testing...
  469. if __name__ == "__main__":
  470. class TestTree(LocationMapTree):
  471. def __init__(self, parent):
  472. """Test Tree constructor."""
  473. super(TestTree, self).__init__(parent, style=wx.TR_HIDE_ROOT | wx.TR_EDIT_LABELS |
  474. wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_COLUMN_LINES |
  475. wx.TR_MULTIPLE)
  476. def InitTreeItems(self):
  477. """Add locations, mapsets and layers to the tree."""
  478. gisenv = grass.gisenv()
  479. location = gisenv['LOCATION_NAME']
  480. mapset = gisenv['MAPSET']
  481. self._initTreeItems(locations=[location],
  482. mapsets=[mapset])
  483. self.ExpandAll()
  484. def _popupMenuLayer(self):
  485. """Create popup menu for layers"""
  486. pass
  487. def _popupMenuMapset(self):
  488. """Create popup menu for mapsets"""
  489. pass
  490. class TestFrame(wx.Frame):
  491. """Frame for testing purposes only."""
  492. def __init__(self, model=None):
  493. wx.Frame.__init__(self, None, title='Test tree')
  494. panel = wx.Panel(self)
  495. self.tree = TestTree(parent=self)
  496. self.tree.SetMinSize((300, 500))
  497. self.tree.InitTreeItems()
  498. szr = wx.BoxSizer(wx.VERTICAL)
  499. szr.Add(self.tree, 1, wx.ALIGN_CENTER)
  500. panel.SetSizerAndFit(szr)
  501. szr.SetSizeHints(self)
  502. def main():
  503. app = wx.App()
  504. frame = TestFrame()
  505. frame.Show()
  506. app.MainLoop()
  507. main()