extensions.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. """
  2. @package modules.extensions
  3. @brief GRASS Addons extensions management classes
  4. Classes:
  5. - extensions::InstallExtensionWindow
  6. - extensions::ExtensionTreeModelBuilder
  7. - extensions::ManageExtensionWindow
  8. - extensions::CheckListExtension
  9. (C) 2008-2016 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Martin Landa <landa.martin gmail.com>
  13. @author Anna Petrasova <kratochanna gmail.com>
  14. """
  15. import os
  16. import sys
  17. import wx
  18. from grass.script import task as gtask
  19. from core import globalvar
  20. from core.gcmd import GError, RunCommand, GException, GMessage
  21. from core.utils import SetAddOnPath
  22. from core.gthread import gThread
  23. from core.menutree import TreeModel, ModuleNode
  24. from gui_core.widgets import GListCtrl
  25. from gui_core.treeview import CTreeView
  26. from core.toolboxes import toolboxesOutdated
  27. from gui_core.wrap import Button, StaticBox, Menu, NewId, SearchCtrl
  28. class InstallExtensionWindow(wx.Frame):
  29. def __init__(
  30. self,
  31. parent,
  32. giface,
  33. id=wx.ID_ANY,
  34. title=_("Fetch & install extension from GRASS Addons"),
  35. **kwargs,
  36. ):
  37. self.parent = parent
  38. self._giface = giface
  39. self.options = dict() # list of options
  40. wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs)
  41. self.SetIcon(
  42. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  43. )
  44. self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
  45. # self.repoBox = StaticBox(
  46. # parent=self.panel, id=wx.ID_ANY, label=" %s " %
  47. # _("Repository (leave empty to use the official one)"))
  48. self.treeBox = StaticBox(
  49. parent=self.panel,
  50. id=wx.ID_ANY,
  51. label=" %s " % _("List of extensions - double-click to install"),
  52. )
  53. # self.repo = TextCtrl(parent=self.panel, id=wx.ID_ANY)
  54. # modelBuilder loads data into tree model
  55. self.modelBuilder = ExtensionTreeModelBuilder()
  56. # tree view displays model data
  57. self.tree = CTreeView(parent=self.panel, model=self.modelBuilder.GetModel())
  58. self.search = SearchCtrl(self.panel)
  59. self.search.SetDescriptiveText(_("Search"))
  60. self.search.ShowCancelButton(True)
  61. # load data in different thread
  62. self.thread = gThread()
  63. self.optionBox = StaticBox(
  64. parent=self.panel, id=wx.ID_ANY, label=" %s " % _("Options")
  65. )
  66. task = gtask.parse_interface("g.extension")
  67. ignoreFlags = ["l", "c", "g", "a", "f", "t", "help", "quiet"]
  68. if sys.platform == "win32":
  69. ignoreFlags.append("d")
  70. ignoreFlags.append("i")
  71. for f in task.get_options()["flags"]:
  72. name = f.get("name", "")
  73. desc = f.get("label", "")
  74. if not desc:
  75. desc = f.get("description", "")
  76. if not name and not desc:
  77. continue
  78. if name in ignoreFlags:
  79. continue
  80. self.options[name] = wx.CheckBox(
  81. parent=self.panel, id=wx.ID_ANY, label=desc
  82. )
  83. # defaultUrl = '' # default/official one will be used when option empty
  84. # self.repo.SetValue(
  85. # task.get_param(
  86. # value='url').get(
  87. # 'default',
  88. # defaultUrl))
  89. self.statusbar = self.CreateStatusBar(number=1)
  90. # self.btnFetch = Button(parent=self.panel, id=wx.ID_ANY,
  91. # label=_("&Fetch"))
  92. # self.btnFetch.SetToolTip(_("Fetch list of available tools "
  93. # "from GRASS Addons repository"))
  94. self.btnClose = Button(parent=self.panel, id=wx.ID_CLOSE)
  95. self.btnInstall = Button(parent=self.panel, id=wx.ID_ANY, label=_("&Install"))
  96. self.btnInstall.SetToolTip(_("Install selected add-ons GRASS tool"))
  97. self.btnInstall.Enable(False)
  98. self.btnHelp = Button(parent=self.panel, id=wx.ID_HELP)
  99. self.btnHelp.SetToolTip(_("Show g.extension manual page"))
  100. self.btnClose.Bind(wx.EVT_BUTTON, lambda evt: self.Close())
  101. # self.btnFetch.Bind(wx.EVT_BUTTON, self.OnFetch)
  102. self.btnInstall.Bind(wx.EVT_BUTTON, self.OnInstall)
  103. self.btnHelp.Bind(wx.EVT_BUTTON, self.OnHelp)
  104. self.search.Bind(wx.EVT_TEXT, lambda evt: self.Filter(evt.GetString()))
  105. self.search.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, lambda evt: self.Filter(""))
  106. self.tree.selectionChanged.connect(self.OnItemSelected)
  107. self.tree.itemActivated.connect(self.OnItemActivated)
  108. self.tree.contextMenu.connect(self.OnContextMenu)
  109. wx.CallAfter(self._fetch)
  110. self._layout()
  111. def _layout(self):
  112. """Do layout"""
  113. sizer = wx.BoxSizer(wx.VERTICAL)
  114. # repoSizer = wx.StaticBoxSizer(self.repoBox, wx.VERTICAL)
  115. # repo1Sizer = wx.BoxSizer(wx.HORIZONTAL)
  116. # repo1Sizer.Add(self.repo, proportion=1,
  117. # flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=1)
  118. # repo1Sizer.Add(self.btnFetch, proportion=0,
  119. # flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=1)
  120. # repoSizer.Add(repo1Sizer,
  121. # flag=wx.EXPAND)
  122. sizer.Add(self.search, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
  123. treeSizer = wx.StaticBoxSizer(self.treeBox, wx.HORIZONTAL)
  124. treeSizer.Add(self.tree, proportion=1, flag=wx.ALL | wx.EXPAND, border=1)
  125. # options
  126. optionSizer = wx.StaticBoxSizer(self.optionBox, wx.VERTICAL)
  127. for key in self.options.keys():
  128. optionSizer.Add(self.options[key], proportion=0)
  129. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  130. btnSizer.Add(self.btnHelp, proportion=0)
  131. btnSizer.AddStretchSpacer()
  132. btnSizer.Add(self.btnClose, proportion=0, flag=wx.RIGHT, border=5)
  133. btnSizer.Add(self.btnInstall, proportion=0)
  134. # sizer.Add(repoSizer, proportion=0,
  135. # flag=wx.ALL | wx.EXPAND, border=3)
  136. sizer.Add(
  137. treeSizer,
  138. proportion=1,
  139. flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND,
  140. border=3,
  141. )
  142. sizer.Add(
  143. optionSizer,
  144. proportion=0,
  145. flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND,
  146. border=3,
  147. )
  148. sizer.Add(btnSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
  149. self.panel.SetSizer(sizer)
  150. sizer.Fit(self.panel)
  151. self.Layout()
  152. def _getCmd(self):
  153. item = self.tree.GetSelected()
  154. if not item or "command" not in item[0].data:
  155. GError(_("Extension not defined"), parent=self)
  156. return
  157. name = item[0].data["command"]
  158. flags = list()
  159. for key in self.options.keys():
  160. if self.options[key].IsChecked():
  161. if len(key) == 1:
  162. flags.append("-%s" % key)
  163. else:
  164. flags.append("--%s" % key)
  165. # 'url=' + self.repo.GetValue().strip()]
  166. return ["g.extension"] + flags + ["extension={}".format(name)]
  167. def OnFetch(self, event):
  168. """Fetch list of available extensions"""
  169. self._fetch()
  170. def _fetch(self):
  171. """Fetch list of available extensions"""
  172. wx.BeginBusyCursor()
  173. self.SetStatusText(
  174. _("Fetching list of tools from GRASS-Addons (be patient)..."), 0
  175. )
  176. try:
  177. self.thread.Run(
  178. callable=self.modelBuilder.Load,
  179. url="", # self.repo.GetValue().strip(),
  180. ondone=lambda event: self._fetchDone(),
  181. )
  182. except GException as error:
  183. self._fetchDone()
  184. GError(str(error), parent=self, showTraceback=False)
  185. def _fetchDone(self):
  186. self.tree.RefreshItems()
  187. nitems = len(self.modelBuilder.GetModel().SearchNodes(key="command", value="*"))
  188. self.SetStatusText(_("%d extensions loaded") % nitems, 0)
  189. wx.EndBusyCursor()
  190. def Filter(self, text):
  191. model = self.modelBuilder.GetModel()
  192. if text:
  193. model = model.Filtered(
  194. key=["command", "keywords", "description"], value=text
  195. )
  196. self.tree.SetModel(model)
  197. self.tree.ExpandAll()
  198. else:
  199. self.tree.SetModel(model)
  200. def OnContextMenu(self, node):
  201. if not hasattr(self, "popupID"):
  202. self.popupID = dict()
  203. for key in ("install", "help"):
  204. self.popupID[key] = NewId()
  205. data = node.data
  206. if data and "command" in data:
  207. self.popupMenu = Menu()
  208. self.popupMenu.Append(self.popupID["install"], _("Install"))
  209. self.Bind(wx.EVT_MENU, self.OnInstall, id=self.popupID["install"])
  210. self.popupMenu.AppendSeparator()
  211. self.popupMenu.Append(self.popupID["help"], _("Show manual page"))
  212. self.Bind(wx.EVT_MENU, self.OnItemHelp, id=self.popupID["help"])
  213. self.PopupMenu(self.popupMenu)
  214. self.popupMenu.Destroy()
  215. def OnItemActivated(self, node):
  216. data = node.data
  217. if data and "command" in data:
  218. self.OnInstall(event=None)
  219. def OnInstall(self, event):
  220. """Install selected extension"""
  221. log = self.parent.GetLogWindow()
  222. cmd = self._getCmd()
  223. if cmd:
  224. log.RunCmd(cmd, onDone=self.OnDone)
  225. def OnDone(self, event):
  226. if event.returncode == 0:
  227. if not os.getenv("GRASS_ADDON_BASE"):
  228. SetAddOnPath(key="BASE")
  229. globalvar.UpdateGRASSAddOnCommands()
  230. toolboxesOutdated()
  231. def OnItemHelp(self, event):
  232. item = self.tree.GetSelected()
  233. if not item or "command" not in item[0].data:
  234. return
  235. self._giface.Help(entry=item[0].data["command"], online=True)
  236. def OnHelp(self, event):
  237. self._giface.Help(entry="g.extension")
  238. def OnItemSelected(self, node):
  239. """Item selected"""
  240. data = node.data
  241. if data is None:
  242. self.SetStatusText("", 0)
  243. self.btnInstall.Enable(False)
  244. else:
  245. self.SetStatusText(data.get("description", ""), 0)
  246. self.btnInstall.Enable(True)
  247. class ExtensionTreeModelBuilder:
  248. """Tree model of available extensions."""
  249. def __init__(self):
  250. self.mainNodes = dict()
  251. self.model = TreeModel(ModuleNode)
  252. for prefix in (
  253. "display",
  254. "database",
  255. "general",
  256. "imagery",
  257. "misc",
  258. "postscript",
  259. "paint",
  260. "raster",
  261. "raster3D",
  262. "sites",
  263. "temporal",
  264. "vector",
  265. "wxGUI",
  266. "other",
  267. ):
  268. node = self.model.AppendNode(parent=self.model.root, label=prefix)
  269. self.mainNodes[prefix] = node
  270. def GetModel(self):
  271. return self.model
  272. def _emptyTree(self):
  273. """Remove modules from tree keeping the main structure"""
  274. for node in self.mainNodes.values():
  275. for child in reversed(node.children):
  276. self.model.RemoveNode(child)
  277. def _expandPrefix(self, c):
  278. name = {
  279. "d": "display",
  280. "db": "database",
  281. "g": "general",
  282. "i": "imagery",
  283. "m": "misc",
  284. "ps": "postscript",
  285. "p": "paint",
  286. "r": "raster",
  287. "r3": "raster3D",
  288. "s": "sites",
  289. "t": "temporal",
  290. "v": "vector",
  291. "wx": "wxGUI",
  292. "": "other",
  293. }
  294. if c in name:
  295. return name[c]
  296. return c
  297. def Load(self, url, full=True):
  298. """Load list of extensions"""
  299. self._emptyTree()
  300. if full:
  301. flags = "g"
  302. else:
  303. flags = "l"
  304. retcode, ret, msg = RunCommand(
  305. "g.extension", read=True, getErrorMsg=True, url=url, flags=flags, quiet=True
  306. )
  307. if retcode != 0:
  308. raise GException(_("Unable to load extensions. %s") % msg)
  309. currentNode = None
  310. for line in ret.splitlines():
  311. if full:
  312. try:
  313. key, value = line.split("=", 1)
  314. except ValueError:
  315. key = "name"
  316. value = line
  317. if key == "name":
  318. try:
  319. prefix, name = value.split(".", 1)
  320. except ValueError:
  321. prefix = ""
  322. name = value
  323. mainNode = self.mainNodes[self._expandPrefix(prefix)]
  324. currentNode = self.model.AppendNode(parent=mainNode, label=value)
  325. currentNode.data = {"command": value}
  326. else:
  327. if currentNode is not None:
  328. currentNode.data[key] = value
  329. else:
  330. try:
  331. prefix, name = line.strip().split(".", 1)
  332. except ValueError:
  333. prefix = ""
  334. name = line.strip()
  335. if self._expandPrefix(prefix) == prefix:
  336. prefix = ""
  337. module = prefix + "." + name
  338. mainNode = self.mainNodes[self._expandPrefix(prefix)]
  339. currentNode = self.model.AppendNode(parent=mainNode, label=module)
  340. currentNode.data = {
  341. "command": module,
  342. "keywords": "",
  343. "description": "",
  344. }
  345. class ManageExtensionWindow(wx.Frame):
  346. def __init__(
  347. self,
  348. parent,
  349. id=wx.ID_ANY,
  350. title=_("Manage installed GRASS Addons extensions"),
  351. **kwargs,
  352. ):
  353. self.parent = parent
  354. wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs)
  355. self.SetIcon(
  356. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  357. )
  358. self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
  359. self.extBox = StaticBox(
  360. parent=self.panel,
  361. id=wx.ID_ANY,
  362. label=" %s " % _("List of installed extensions"),
  363. )
  364. self.extList = CheckListExtension(parent=self.panel)
  365. # buttons
  366. self.btnUninstall = Button(
  367. parent=self.panel, id=wx.ID_REMOVE, label=_("Uninstall")
  368. )
  369. self.btnUninstall.SetToolTip(_("Uninstall selected Addons extensions"))
  370. self.btnUpdate = Button(
  371. parent=self.panel, id=wx.ID_REFRESH, label=_("Reinstall")
  372. )
  373. self.btnUpdate.SetToolTip(_("Reinstall selected Addons extensions"))
  374. self.btnClose = Button(parent=self.panel, id=wx.ID_CLOSE)
  375. self.btnUninstall.Bind(wx.EVT_BUTTON, self.OnUninstall)
  376. self.btnUpdate.Bind(wx.EVT_BUTTON, self.OnUpdate)
  377. self.btnClose.Bind(wx.EVT_BUTTON, lambda evt: self.Close())
  378. self._layout()
  379. def _layout(self):
  380. """Do layout"""
  381. sizer = wx.BoxSizer(wx.VERTICAL)
  382. extSizer = wx.StaticBoxSizer(self.extBox, wx.HORIZONTAL)
  383. extSizer.Add(self.extList, proportion=1, flag=wx.ALL | wx.EXPAND, border=1)
  384. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  385. btnSizer.Add(self.btnClose, proportion=0, flag=wx.RIGHT, border=5)
  386. btnSizer.Add(self.btnUpdate, proportion=0, flag=wx.RIGHT, border=5)
  387. btnSizer.Add(self.btnUninstall, proportion=0)
  388. sizer.Add(extSizer, proportion=1, flag=wx.ALL | wx.EXPAND, border=3)
  389. sizer.Add(btnSizer, proportion=0, flag=wx.ALIGN_RIGHT | wx.ALL, border=5)
  390. self.panel.SetSizer(sizer)
  391. sizer.Fit(self.panel)
  392. self.Layout()
  393. def _getSelectedExtensions(self):
  394. eList = self.extList.GetExtensions()
  395. if not eList:
  396. GMessage(_("No extension selected. " "Operation canceled."), parent=self)
  397. return []
  398. return eList
  399. def OnUninstall(self, event):
  400. """Uninstall selected extensions"""
  401. eList = self._getSelectedExtensions()
  402. if not eList:
  403. return
  404. for ext in eList:
  405. files = RunCommand(
  406. "g.extension",
  407. parent=self,
  408. read=True,
  409. quiet=True,
  410. extension=ext,
  411. operation="remove",
  412. ).splitlines()
  413. if len(files) > 10:
  414. files = files[:10]
  415. files.append("...")
  416. dlg = wx.MessageDialog(
  417. parent=self,
  418. message=_(
  419. "List of files to be removed:\n%(files)s\n\n"
  420. "Do you want really to remove <%(ext)s> extension?"
  421. )
  422. % {"files": os.linesep.join(files), "ext": ext},
  423. caption=_("Remove extension"),
  424. style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
  425. )
  426. if dlg.ShowModal() == wx.ID_YES:
  427. RunCommand(
  428. "g.extension",
  429. flags="f",
  430. parent=self,
  431. quiet=True,
  432. extension=ext,
  433. operation="remove",
  434. )
  435. self.extList.LoadData()
  436. # update prompt
  437. globalvar.UpdateGRASSAddOnCommands(eList)
  438. toolboxesOutdated()
  439. def OnUpdate(self, event):
  440. """Update selected extensions"""
  441. eList = self._getSelectedExtensions()
  442. if not eList:
  443. return
  444. log = self.parent.GetLogWindow()
  445. for ext in eList:
  446. log.RunCmd(["g.extension", "extension=%s" % ext, "operation=add"])
  447. class CheckListExtension(GListCtrl):
  448. """List of mapset/owner/group"""
  449. def __init__(self, parent):
  450. GListCtrl.__init__(self, parent)
  451. # load extensions
  452. self.InsertColumn(0, _("Extension"))
  453. self.LoadData()
  454. def LoadData(self):
  455. """Load data into list"""
  456. self.DeleteAllItems()
  457. for ext in RunCommand(
  458. "g.extension", quiet=True, parent=self, read=True, flags="a"
  459. ).splitlines():
  460. if ext:
  461. self.InsertItem(self.GetItemCount(), ext)
  462. def GetExtensions(self):
  463. """Get extensions to be un-installed"""
  464. extList = list()
  465. for i in range(self.GetItemCount()):
  466. if self.IsItemChecked(i):
  467. name = self.GetItemText(i)
  468. if name:
  469. extList.append(name)
  470. return extList