menu.py 13 KB

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