menu.py 13 KB

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