menu.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. """
  2. @package gui_core.menu
  3. @brief Menu classes for wxGUI
  4. Classes:
  5. - menu::Menu
  6. - menu::SearchModuleWindow
  7. (C) 2010-2013 by the GRASS Development Team
  8. This program is free software under the GNU General Public License
  9. (>=v2). Read the file COPYING that comes with GRASS for details.
  10. @author Martin Landa <landa.martin gmail.com>
  11. @author Pawel Netzel (menu customization)
  12. @author Milena Nowotarska (menu customization)
  13. @author Robert Szczepanek (menu customization)
  14. @author Vaclav Petras <wenzeslaus gmail.com> (menu customization)
  15. @author Tomas Zigo <tomas.zigo slovanet.sk> RecentFilesMenu
  16. """
  17. import re
  18. import os
  19. import wx
  20. from core import globalvar
  21. from core import utils
  22. from core.gcmd import EncodeString
  23. from gui_core.treeview import CTreeView
  24. from gui_core.wrap import Button, SearchCtrl
  25. from gui_core.wrap import Menu as MenuWidget
  26. from icons.icon import MetaIcon
  27. from grass.pydispatch.signal import Signal
  28. class Menu(wx.MenuBar):
  29. def __init__(self, parent, model):
  30. """Creates menubar"""
  31. wx.MenuBar.__init__(self)
  32. self.parent = parent
  33. self.model = model
  34. self.menucmd = dict()
  35. self.bmpsize = (16, 16)
  36. for child in self.model.root.children:
  37. self.Append(self._createMenu(child), child.label)
  38. def _createMenu(self, node):
  39. """Creates menu"""
  40. menu = MenuWidget()
  41. for child in node.children:
  42. if child.children:
  43. label = child.label
  44. subMenu = self._createMenu(child)
  45. menu.AppendMenu(wx.ID_ANY, label, subMenu)
  46. else:
  47. data = child.data.copy()
  48. data.pop("label")
  49. self._createMenuItem(menu, label=child.label, **data)
  50. self.parent.Bind(wx.EVT_MENU_HIGHLIGHT_ALL, self.OnMenuHighlight)
  51. return menu
  52. def _createMenuItem(
  53. self,
  54. menu,
  55. label,
  56. description,
  57. handler,
  58. command,
  59. keywords,
  60. shortcut="",
  61. icon="",
  62. wxId=wx.ID_ANY,
  63. kind=wx.ITEM_NORMAL,
  64. ):
  65. """Creates menu items
  66. There are three menu styles (menu item text styles).
  67. 1 -- label only, 2 -- label and cmd name, 3 -- cmd name only
  68. """
  69. if not label:
  70. menu.AppendSeparator()
  71. return
  72. if command:
  73. helpString = command + " -- " + description
  74. else:
  75. helpString = description
  76. if shortcut:
  77. label += "\t" + shortcut
  78. menuItem = wx.MenuItem(menu, wxId, label, helpString, kind)
  79. if icon:
  80. menuItem.SetBitmap(MetaIcon(img=icon).GetBitmap(self.bmpsize))
  81. menu.AppendItem(menuItem)
  82. self.menucmd[menuItem.GetId()] = command
  83. if command:
  84. try:
  85. cmd = utils.split(str(command))
  86. except UnicodeError:
  87. cmd = utils.split(EncodeString((command)))
  88. # disable only grass commands which are not present (e.g.
  89. # r.in.lidar)
  90. if (
  91. cmd
  92. and cmd[0] not in globalvar.grassCmd
  93. and re.match("[rvdipmgt][3bs]?\.([a-z0-9\.])+", cmd[0])
  94. ):
  95. menuItem.Enable(False)
  96. rhandler = eval("self.parent." + handler)
  97. self.parent.Bind(wx.EVT_MENU, rhandler, menuItem)
  98. def GetData(self):
  99. """Get menu data"""
  100. return self.model
  101. def GetCmd(self):
  102. """Get dictionary of commands (key is id)
  103. :return: dictionary of commands
  104. """
  105. return self.menucmd
  106. def OnMenuHighlight(self, event):
  107. """
  108. Default menu help handler
  109. """
  110. # Show how to get menu item info from this event handler
  111. id = event.GetMenuId()
  112. item = self.FindItemById(id)
  113. if item:
  114. help = item.GetHelp()
  115. # but in this case just call Skip so the default is done
  116. event.Skip()
  117. class SearchModuleWindow(wx.Panel):
  118. """Menu tree and search widget for searching modules.
  119. Signal:
  120. showNotification - attribute 'message'
  121. """
  122. def __init__(self, parent, handlerObj, giface, model, id=wx.ID_ANY, **kwargs):
  123. self.parent = parent
  124. self._handlerObj = handlerObj
  125. self._giface = giface
  126. self._model = model
  127. self.showNotification = Signal("SearchModuleWindow.showNotification")
  128. wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
  129. # search widget
  130. self._search = SearchCtrl(self)
  131. self._search.SetDescriptiveText(_("Search"))
  132. self._search.ShowCancelButton(True)
  133. self._btnAdvancedSearch = Button(
  134. self, id=wx.ID_ANY, label=_("Adva&nced search...")
  135. )
  136. self._btnAdvancedSearch.SetToolTip(
  137. _("Do advanced search using %s module") % "g.search.module"
  138. )
  139. # tree
  140. self._tree = CTreeView(model=model, parent=self)
  141. self._tree.SetToolTip(_("Double-click to run selected module"))
  142. # buttons
  143. self._btnRun = Button(self, id=wx.ID_OK, label=_("&Run..."))
  144. self._btnRun.SetToolTip(_("Run selected module from the tree"))
  145. self._btnHelp = Button(self, id=wx.ID_ANY, label=_("H&elp"))
  146. self._btnHelp.SetToolTip(_("Show manual for selected module from the tree"))
  147. # bindings
  148. self._search.Bind(wx.EVT_TEXT, lambda evt: self.Filter(evt.GetString()))
  149. self._search.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, lambda evt: self.Filter(""))
  150. self._btnRun.Bind(wx.EVT_BUTTON, lambda evt: self.Run())
  151. self._btnHelp.Bind(wx.EVT_BUTTON, lambda evt: self.Help())
  152. self._btnAdvancedSearch.Bind(wx.EVT_BUTTON, lambda evt: self.AdvancedSearch())
  153. self._tree.selectionChanged.connect(self.OnItemSelected)
  154. self._tree.itemActivated.connect(lambda node: self.Run(node))
  155. self._layout()
  156. self._search.SetFocus()
  157. def _layout(self):
  158. """Do dialog layout"""
  159. sizer = wx.BoxSizer(wx.VERTICAL)
  160. # search
  161. searchSizer = wx.BoxSizer(wx.HORIZONTAL)
  162. searchSizer.Add(self._search, proportion=1, flag=wx.EXPAND | wx.RIGHT, border=5)
  163. searchSizer.Add(self._btnAdvancedSearch, proportion=0, flag=wx.EXPAND)
  164. sizer.Add(searchSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
  165. # body
  166. sizer.Add(
  167. self._tree, proportion=1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5
  168. )
  169. # buttons
  170. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  171. btnSizer.AddStretchSpacer()
  172. btnSizer.Add(self._btnHelp, proportion=0, flag=wx.EXPAND | wx.RIGHT, border=5)
  173. btnSizer.Add(self._btnRun, proportion=0)
  174. sizer.Add(btnSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=5)
  175. self.SetSizerAndFit(sizer)
  176. self.SetAutoLayout(True)
  177. self.Layout()
  178. def Filter(self, text):
  179. if text:
  180. model = self._model.Filtered(
  181. key=["command", "keywords", "description"], value=text
  182. )
  183. self._tree.SetModel(model)
  184. self._tree.ExpandAll()
  185. else:
  186. self._tree.SetModel(self._model)
  187. def _GetSelectedNode(self):
  188. selection = self._tree.GetSelected()
  189. if not selection:
  190. return None
  191. return selection[0]
  192. def Run(self, node=None):
  193. """Run selected command.
  194. :param node: a tree node associated with the module or other item
  195. """
  196. if not node:
  197. node = self._GetSelectedNode()
  198. # nothing selected
  199. if not node:
  200. return
  201. data = node.data
  202. # non-leaf nodes
  203. if not data:
  204. # expand/collapse location/mapset...
  205. if self._tree.IsNodeExpanded(node):
  206. self._tree.CollapseNode(node, recursive=False)
  207. else:
  208. self._tree.ExpandNode(node, recursive=False)
  209. return
  210. # extract name of the handler and create a new call
  211. handler = "self._handlerObj." + data["handler"].lstrip("self.")
  212. if data["command"]:
  213. eval(handler)(event=None, cmd=data["command"].split())
  214. else:
  215. eval(handler)(event=None)
  216. def Help(self, node=None):
  217. """Show documentation for a module"""
  218. if not node:
  219. node = self._GetSelectedNode()
  220. # nothing selected
  221. if not node:
  222. return
  223. data = node.data
  224. # non-leaf nodes
  225. if not data:
  226. return
  227. if not data["command"]:
  228. # showing nothing for non-modules
  229. return
  230. # strip parameters from command if present
  231. name = data["command"].split()[0]
  232. self._giface.Help(name)
  233. self.showNotification.emit(
  234. message=_("Documentation for %s is now open in the web browser") % name
  235. )
  236. def AdvancedSearch(self):
  237. """Show advanced search window"""
  238. self._handlerObj.RunMenuCmd(cmd=["g.search.modules"])
  239. def OnItemSelected(self, node):
  240. """Item selected"""
  241. data = node.data
  242. if not data or "command" not in data:
  243. return
  244. if data["command"]:
  245. label = data["command"]
  246. if data["description"]:
  247. label += " -- " + data["description"]
  248. else:
  249. label = data["description"]
  250. self.showNotification.emit(message=label)
  251. class RecentFilesMenu:
  252. """Add recent files history menu
  253. Signal FileRequested is emitted if you request file from recent
  254. files menu
  255. :param str path: file path you requested
  256. :param bool file_exists: file path exists or not
  257. :param obj file_history: file history obj instance
  258. :param str app_name: required for group name of recent files path
  259. written into the .recent_files file
  260. :param obj parent_menu: menu widget instance where be inserted
  261. recent files menu on the specified position
  262. :param int pos: position (index) where insert recent files menu in
  263. the parent menu
  264. :param int history_len: the maximum number of file paths written
  265. into the .recent_files file to app name group
  266. """
  267. recent_files = ".recent_files"
  268. def __init__(self, app_name, parent_menu, pos, history_len=10):
  269. self._history_len = history_len
  270. self._parent_menu = parent_menu
  271. self._pos = pos
  272. self.file_requested = Signal("RecentFilesMenu.FileRequested")
  273. self._filehistory = wx.FileHistory(maxFiles=history_len)
  274. # Recent files path stored in GRASS GIS config dir in the
  275. # .recent_files file in the group by application name
  276. self._config = wx.FileConfig(
  277. style=wx.CONFIG_USE_LOCAL_FILE,
  278. localFilename=os.path.join(
  279. utils.GetSettingsPath(),
  280. self.recent_files,
  281. ),
  282. )
  283. self._config.SetPath(strPath=app_name)
  284. self._filehistory.Load(self._config)
  285. self.RemoveNonExistentFiles()
  286. self.recent = wx.Menu()
  287. self._filehistory.UseMenu(self.recent)
  288. self._filehistory.AddFilesToMenu()
  289. # Show recent files menu if count of items in menu > 0
  290. if self._filehistory.GetCount() > 0:
  291. self._insertMenu()
  292. def _insertMenu(self):
  293. """Insert recent files menu into the parent menu on the
  294. specified position if count of menu items > 0"""
  295. self._parent_menu.Insert(
  296. pos=self._pos,
  297. id=wx.ID_ANY,
  298. text=_("&Recent Files"),
  299. submenu=self.recent,
  300. )
  301. self.recent.Bind(
  302. wx.EVT_MENU_RANGE,
  303. self._onFileHistory,
  304. id=wx.ID_FILE1,
  305. id2=wx.ID_FILE + self._history_len,
  306. )
  307. def _onFileHistory(self, event):
  308. """Choose recent file from menu event"""
  309. file_exists = True
  310. file_index = event.GetId() - wx.ID_FILE1
  311. path = self._filehistory.GetHistoryFile(file_index)
  312. if not os.path.exists(path):
  313. self.RemoveFileFromHistory(file_index)
  314. file_exists = False
  315. self.file_requested.emit(
  316. path=path,
  317. file_exists=file_exists,
  318. file_history=self._filehistory,
  319. )
  320. def AddFileToHistory(self, filename):
  321. """Add file to history, and save history into '.recent_files'
  322. file
  323. :param str filename: file path
  324. :return None
  325. """
  326. if self._filehistory.GetCount() == 0:
  327. self._insertMenu()
  328. if filename:
  329. self._filehistory.AddFileToHistory(filename)
  330. self._filehistory.Save(self._config)
  331. self._config.Flush()
  332. def RemoveFileFromHistory(self, file_index):
  333. """Remove file from the history.
  334. :param int file_index: filed index
  335. :return: None
  336. """
  337. self._filehistory.RemoveFileFromHistory(i=file_index)
  338. self._filehistory.Save(self._config)
  339. self._config.Flush()
  340. def RemoveNonExistentFiles(self):
  341. """Remove non existent files from the history"""
  342. for i in reversed(range(0, self._filehistory.GetCount())):
  343. file = self._filehistory.GetHistoryFile(index=i)
  344. if not os.path.exists(file):
  345. self._filehistory.RemoveFileFromHistory(i=i)
  346. self._filehistory.Save(self._config)
  347. self._config.Flush()