prompt.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. """!
  2. @package prompt.py
  3. @brief GRASS prompt
  4. Classes:
  5. - GPrompt
  6. - PromptListCtrl
  7. - TextCtrlAutoComplete
  8. @todo: fix TextCtrlAutoComplete to work also on Macs (missing
  9. wx.PopupWindow())
  10. (C) 2009 by 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 Martin Landa <landa.martin gmail.com>
  15. """
  16. import os
  17. import sys
  18. import shlex
  19. import wx
  20. import wx.lib.mixins.listctrl as listmix
  21. from grass.script import core as grass
  22. import globalvar
  23. import utils
  24. import menuform
  25. import menudata
  26. class GPrompt:
  27. """!Interactive GRASS prompt"""
  28. def __init__(self, parent):
  29. self.parent = parent # GMFrame
  30. # dictionary of modules (description, keywords, ...)
  31. self.modules = self.parent.menudata.GetModules()
  32. self.panel, self.input = self.__create()
  33. def __create(self):
  34. """!Create widget"""
  35. cmdprompt = wx.Panel(self.parent)
  36. #
  37. # search
  38. #
  39. searchTxt = wx.StaticText(parent = cmdprompt, id = wx.ID_ANY,
  40. label = _("Search:"))
  41. self.searchBy = wx.Choice(parent = cmdprompt, id = wx.ID_ANY,
  42. choices = [_("description"),
  43. _("keywords")])
  44. winHeight = self.searchBy.GetSize()[1]
  45. self.search = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
  46. value = "", size = (-1, 25))
  47. label = wx.Button(parent = cmdprompt, id = wx.ID_ANY,
  48. label = _("Cmd >"), size = (-1, winHeight))
  49. label.SetToolTipString(_("Click for erasing command prompt"))
  50. ### todo: fix TextCtrlAutoComplete to work also on Macs
  51. ### reason: missing wx.PopupWindow()
  52. try:
  53. cmdinput = TextCtrlAutoComplete(parent = cmdprompt, id = wx.ID_ANY,
  54. value = "",
  55. style = wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  56. size = (-1, winHeight),
  57. statusbar = self.parent.statusbar)
  58. except NotImplementedError:
  59. # wx.PopupWindow may be not available in wxMac
  60. # see http://trac.wxwidgets.org/ticket/9377
  61. cmdinput = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
  62. value = "",
  63. style=wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  64. size = (-1, 25))
  65. self.searchBy.Enable(False)
  66. self.search.Enable(False)
  67. cmdinput.SetFont(wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL, 0, ''))
  68. wx.CallAfter(cmdinput.SetInsertionPoint, 0)
  69. # bidnings
  70. label.Bind(wx.EVT_BUTTON, self.OnCmdErase)
  71. cmdinput.Bind(wx.EVT_TEXT_ENTER, self.OnRunCmd)
  72. cmdinput.Bind(wx.EVT_TEXT, self.OnUpdateStatusBar)
  73. self.search.Bind(wx.EVT_TEXT, self.OnSearchModule)
  74. # layout
  75. sizer = wx.GridBagSizer(hgap=5, vgap=5)
  76. sizer.AddGrowableCol(2)
  77. sizer.Add(item = searchTxt,
  78. flag = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL,
  79. pos = (0, 0))
  80. sizer.Add(item = self.searchBy,
  81. flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  82. pos = (0, 1))
  83. sizer.Add(item = self.search,
  84. flag = wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  85. border = 5,
  86. pos = (0, 2))
  87. sizer.Add(item = label,
  88. flag = wx.LEFT | wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  89. border = 5,
  90. pos = (1, 0))
  91. sizer.Add(item = cmdinput,
  92. flag = wx.EXPAND | wx.RIGHT,
  93. border = 5,
  94. pos = (1, 1), span = (1, 2))
  95. cmdprompt.SetSizer(sizer)
  96. sizer.Fit(cmdprompt)
  97. cmdprompt.Layout()
  98. return cmdprompt, cmdinput
  99. def __checkKey(self, text, keywords):
  100. """!Check if text is in keywords"""
  101. found = 0
  102. keys = text.split(',')
  103. if len(keys) > 1: # -> multiple keys
  104. for k in keys[:-1]:
  105. k = k.strip()
  106. for key in keywords:
  107. if k == key: # full match
  108. found += 1
  109. break
  110. k = keys[-1].strip()
  111. for key in keywords:
  112. if k in key: # partial match
  113. found +=1
  114. break
  115. else:
  116. for key in keywords:
  117. if text in key: # partial match
  118. found +=1
  119. break
  120. if found == len(keys):
  121. return True
  122. return False
  123. def GetPanel(self):
  124. """!Get main widget panel"""
  125. return self.panel
  126. def GetInput(self):
  127. """!Get main prompt widget"""
  128. return self.input
  129. def OnCmdErase(self, event):
  130. """!Erase command prompt"""
  131. self.input.SetValue('')
  132. def OnRunCmd(self, event):
  133. """!Run command"""
  134. cmdString = event.GetString()
  135. if self.parent.GetName() != "LayerManager":
  136. return
  137. if cmdString[:2] == 'd.' and not self.parent.curr_page:
  138. self.parent.NewDisplay(show=True)
  139. cmd = shlex.split(str(cmdString))
  140. if len(cmd) > 1:
  141. self.parent.goutput.RunCmd(cmd, switchPage = True)
  142. else:
  143. self.parent.goutput.RunCmd(cmd, switchPage = False)
  144. self.OnUpdateStatusBar(None)
  145. def OnUpdateStatusBar(self, event):
  146. """!Update Layer Manager status bar"""
  147. if self.parent.GetName() != "LayerManager":
  148. return
  149. if event is None:
  150. self.parent.statusbar.SetStatusText("")
  151. else:
  152. self.parent.statusbar.SetStatusText(_("Type GRASS command and run by pressing ENTER"))
  153. event.Skip()
  154. def OnSearchModule(self, event):
  155. """!Search module by metadata"""
  156. text = event.GetString()
  157. if not text:
  158. self.input.SetChoices(globalvar.grassCmd['all'])
  159. return
  160. modules = []
  161. for module, data in self.modules.iteritems():
  162. if self.searchBy.GetSelection() == 0: # -> description
  163. if text in data['desc']:
  164. modules.append(module)
  165. else: # -> keywords
  166. if self.__checkKey(text, data['keywords']):
  167. modules.append(module)
  168. self.parent.statusbar.SetStatusText(_("%d modules found") % len(modules))
  169. self.input.SetChoices(modules)
  170. class PromptListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  171. def __init__(self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition,
  172. size = wx.DefaultSize, style = 0):
  173. wx.ListCtrl.__init__(self, parent, id, pos, size, style)
  174. listmix.ListCtrlAutoWidthMixin.__init__(self)
  175. class TextCtrlAutoComplete(wx.ComboBox, listmix.ColumnSorterMixin):
  176. def __init__ (self, parent, statusbar,
  177. id = wx.ID_ANY, choices = [], **kwargs):
  178. """!Constructor works just like wx.TextCtrl except you can pass in a
  179. list of choices. You can also change the choice list at any time
  180. by calling setChoices.
  181. Inspired by http://wiki.wxpython.org/TextCtrlAutoComplete
  182. """
  183. self.statusbar = statusbar
  184. if kwargs.has_key('style'):
  185. kwargs['style'] = wx.TE_PROCESS_ENTER | kwargs['style']
  186. else:
  187. kwargs['style'] = wx.TE_PROCESS_ENTER
  188. wx.ComboBox.__init__(self, parent, id, **kwargs)
  189. # some variables
  190. self._choices = choices
  191. self._hideOnNoMatch = True
  192. self._module = None # currently selected module
  193. self._choiceType = None # type of choice (module, params, flags, raster, vector ...)
  194. self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
  195. # sort variable needed by listmix
  196. self.itemDataMap = dict()
  197. # widgets
  198. try:
  199. self.dropdown = wx.PopupWindow(self)
  200. except NotImplementedError:
  201. self.Destroy()
  202. raise NotImplementedError
  203. # create the list and bind the events
  204. self.dropdownlistbox = PromptListCtrl(parent = self.dropdown,
  205. style = wx.LC_REPORT | wx.LC_SINGLE_SEL | \
  206. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER,
  207. pos = wx.Point(0, 0))
  208. listmix.ColumnSorterMixin.__init__(self, 1)
  209. # set choices (list of GRASS modules)
  210. self._choicesCmd = globalvar.grassCmd['all']
  211. self._choicesMap = dict()
  212. for type in ('raster', 'vector'):
  213. self._choicesMap[type] = grass.list_strings(type = type[:4])
  214. # first search for GRASS module
  215. self.SetChoices(self._choicesCmd)
  216. self.SetMinSize(self.GetSize())
  217. # read history
  218. self.SetHistoryItems()
  219. # bindings...
  220. self.Bind(wx.EVT_KILL_FOCUS, self.OnControlChanged)
  221. self.Bind(wx.EVT_TEXT, self.OnEnteredText)
  222. self.Bind(wx.EVT_KEY_DOWN , self.OnKeyDown)
  223. # if need drop down on left click
  224. self.dropdown.Bind(wx.EVT_LISTBOX , self.OnListItemSelected, self.dropdownlistbox)
  225. self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.OnListClick)
  226. self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.OnListDClick)
  227. self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.OnListColClick)
  228. self.Bind(wx.EVT_COMBOBOX, self.OnCommandSelect)
  229. def _updateDataList(self, choices):
  230. """!Update data list"""
  231. # delete, if need, all the previous data
  232. if self.dropdownlistbox.GetColumnCount() != 0:
  233. self.dropdownlistbox.DeleteAllColumns()
  234. self.dropdownlistbox.DeleteAllItems()
  235. # and update the dict
  236. if choices:
  237. for numVal, data in enumerate(choices):
  238. self.itemDataMap[numVal] = data
  239. else:
  240. numVal = 0
  241. self.SetColumnCount(numVal)
  242. def _setListSize(self):
  243. """!Set list size"""
  244. choices = self._choices
  245. longest = 0
  246. for choice in choices:
  247. longest = max(len(choice), longest)
  248. longest += 3
  249. itemcount = min(len( choices ), 7) + 2
  250. charheight = self.dropdownlistbox.GetCharHeight()
  251. charwidth = self.dropdownlistbox.GetCharWidth()
  252. self.popupsize = wx.Size(charwidth*longest, charheight*itemcount)
  253. self.dropdownlistbox.SetSize(self.popupsize)
  254. self.dropdown.SetClientSize(self.popupsize)
  255. def _showDropDown(self, show = True):
  256. """!Either display the drop down list (show = True) or hide it
  257. (show = False).
  258. """
  259. if show:
  260. size = self.dropdown.GetSize()
  261. width, height = self.GetSizeTuple()
  262. x, y = self.ClientToScreenXY(0, height)
  263. if size.GetWidth() != width:
  264. size.SetWidth(width)
  265. self.dropdown.SetSize(size)
  266. self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
  267. if (y + size.GetHeight()) < self._screenheight:
  268. self.dropdown.SetPosition(wx.Point(x, y))
  269. else:
  270. self.dropdown.SetPosition(wx.Point(x, y - height - size.GetHeight()))
  271. self.dropdown.Show(show)
  272. def _listItemVisible(self):
  273. """!Moves the selected item to the top of the list ensuring it is
  274. always visible.
  275. """
  276. toSel = self.dropdownlistbox.GetFirstSelected()
  277. if toSel == -1:
  278. return
  279. self.dropdownlistbox.EnsureVisible(toSel)
  280. def _setValueFromSelected(self):
  281. """!Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
  282. Will do nothing if no item is selected in the wx.ListCtrl.
  283. """
  284. sel = self.dropdownlistbox.GetFirstSelected()
  285. if sel > -1:
  286. if self._colFetch != -1:
  287. col = self._colFetch
  288. else:
  289. col = self._colSearch
  290. itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
  291. cmd = shlex.split(str(self.GetValue()))
  292. if len(cmd) > 1:
  293. # -> append text (skip last item)
  294. if self._choiceType == 'param':
  295. self.SetValue(' '.join(cmd[:-1]) + ' ' + itemtext + '=')
  296. optType = self._module.get_param(itemtext)['prompt']
  297. if optType in ('raster', 'vector'):
  298. # -> raster/vector map
  299. self.SetChoices(self._choicesMap[optType], optType)
  300. elif self._choiceType == 'flag':
  301. if len(itemtext) > 1:
  302. prefix = '--'
  303. else:
  304. prefix = '-'
  305. self.SetValue(' '.join(cmd[:-1]) + ' ' + prefix + itemtext)
  306. elif self._choiceType in ('raster', 'vector'):
  307. self.SetValue(' '.join(cmd[:-1]) + ' ' + cmd[-1].split('=', 1)[0] + '=' + itemtext)
  308. else:
  309. # -> reset text
  310. self.SetValue(itemtext + ' ')
  311. self.SetInsertionPointEnd()
  312. self._showDropDown(False)
  313. def GetListCtrl(self):
  314. """!Method required by listmix.ColumnSorterMixin"""
  315. return self.dropdownlistbox
  316. def SetHistoryItems(self):
  317. """!Read history file and update combobox items"""
  318. env = grass.gisenv()
  319. fileHistory = open(os.path.join(env['GISDBASE'], env['LOCATION_NAME'], env['MAPSET'],
  320. '.bash_history'), 'r')
  321. try:
  322. hist = []
  323. for line in fileHistory.readlines():
  324. hist.append(line.replace('\n', ''))
  325. self.SetItems(hist)
  326. finally:
  327. fileHistory.close()
  328. return
  329. self.SetItems([])
  330. def SetChoices(self, choices, type = 'module'):
  331. """!Sets the choices available in the popup wx.ListBox.
  332. The items will be sorted case insensitively.
  333. """
  334. self._choices = choices
  335. self._choiceType = type
  336. self.dropdownlistbox.SetWindowStyleFlag(wx.LC_REPORT | wx.LC_SINGLE_SEL | \
  337. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER)
  338. if not isinstance(choices, list):
  339. self._choices = [ x for x in choices ]
  340. if self._choiceType not in ('raster', 'vector'):
  341. # do not sort raster/vector maps
  342. utils.ListSortLower(self._choices)
  343. self._updateDataList(self._choices)
  344. self.dropdownlistbox.InsertColumn(0, "")
  345. for num, colVal in enumerate(self._choices):
  346. index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
  347. self.dropdownlistbox.SetStringItem(index, 0, colVal)
  348. self.dropdownlistbox.SetItemData(index, num)
  349. self._setListSize()
  350. # there is only one choice for both search and fetch if setting a single column:
  351. self._colSearch = 0
  352. self._colFetch = -1
  353. def OnCommandSelect(self, event):
  354. """!Command selected from history"""
  355. self.SetFocus()
  356. def OnListClick(self, evt):
  357. """!Left mouse button pressed"""
  358. toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
  359. #no values on poition, return
  360. if toSel == -1: return
  361. self.dropdownlistbox.Select(toSel)
  362. def OnListDClick(self, evt):
  363. """!Mouse button double click"""
  364. self._setValueFromSelected()
  365. def OnListColClick(self, evt):
  366. """!Left mouse button pressed on column"""
  367. col = evt.GetColumn()
  368. # reverse the sort
  369. if col == self._colSearch:
  370. self._ascending = not self._ascending
  371. self.SortListItems( evt.GetColumn(), ascending=self._ascending )
  372. self._colSearch = evt.GetColumn()
  373. evt.Skip()
  374. def OnListItemSelected(self, event):
  375. """!Item selected"""
  376. self._setValueFromSelected()
  377. event.Skip()
  378. def OnEnteredText(self, event):
  379. """!Text entered"""
  380. text = event.GetString()
  381. if not text:
  382. # control is empty; hide dropdown if shown:
  383. if self.dropdown.IsShown():
  384. self._showDropDown(False)
  385. event.Skip()
  386. return
  387. cmd = shlex.split(str(text))
  388. pattern = str(text)
  389. if len(cmd) > 1:
  390. # search for module's options
  391. if cmd[0] in self._choicesCmd and not self._module:
  392. self._module = menuform.GUI().ParseInterface(cmd = cmd)
  393. if self._module:
  394. if len(cmd[-1].split('=', 1)) == 1:
  395. # new option
  396. if cmd[-1][0] == '-':
  397. # -> flags
  398. self.SetChoices(self._module.get_list_flags(), type = 'flag')
  399. pattern = cmd[-1].lstrip('-')
  400. else:
  401. # -> options
  402. self.SetChoices(self._module.get_list_params(), type = 'param')
  403. pattern = cmd[-1]
  404. else:
  405. # value
  406. pattern = cmd[-1].split('=', 1)[1]
  407. else:
  408. # search for GRASS modules
  409. if self._module:
  410. # -> switch back to GRASS modules list
  411. self.SetChoices(self._choicesCmd)
  412. self._module = None
  413. self._choiceType = None
  414. found = False
  415. choices = self._choices
  416. for numCh, choice in enumerate(choices):
  417. if choice.lower().startswith(pattern):
  418. found = True
  419. if found:
  420. self._showDropDown(True)
  421. item = self.dropdownlistbox.GetItem(numCh)
  422. toSel = item.GetId()
  423. self.dropdownlistbox.Select(toSel)
  424. break
  425. if not found:
  426. self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
  427. if self._hideOnNoMatch:
  428. self._showDropDown(False)
  429. if self._module and '=' not in cmd[-1]:
  430. message = ''
  431. if cmd[-1][0] == '-': # flag
  432. message = "Warning: flag <%s> not found in '%s'" % \
  433. (cmd[-1][1:], self._module.name)
  434. else: # option
  435. message = "Warning: option <%s> not found in '%s'" % \
  436. (cmd[-1], self._module.name)
  437. self.statusbar.SetStatusText(message)
  438. if self._module and len(cmd[-1]) == 2 and cmd[-1][-2] == '=':
  439. optType = self._module.get_param(cmd[-1][:-2])['prompt']
  440. if optType in ('raster', 'vector'):
  441. # -> raster/vector map
  442. self.SetChoices(self._choicesMap[optType], optType)
  443. self._listItemVisible()
  444. event.Skip()
  445. def OnKeyDown (self, event):
  446. """
  447. Do some work when the user press on the keys: up and down:
  448. move the cursor left and right: move the search
  449. """
  450. skip = True
  451. sel = self.dropdownlistbox.GetFirstSelected()
  452. visible = self.dropdown.IsShown()
  453. KC = event.GetKeyCode()
  454. if KC == wx.WXK_DOWN:
  455. if sel < (self.dropdownlistbox.GetItemCount() - 1):
  456. self.dropdownlistbox.Select(sel + 1)
  457. self._listItemVisible()
  458. self._showDropDown()
  459. skip = False
  460. elif KC == wx.WXK_UP:
  461. if sel > 0:
  462. self.dropdownlistbox.Select(sel - 1)
  463. self._listItemVisible()
  464. self._showDropDown ()
  465. skip = False
  466. if visible:
  467. if event.GetKeyCode() == wx.WXK_RETURN:
  468. self._setValueFromSelected()
  469. skip = False
  470. if event.GetKeyCode() == wx.WXK_ESCAPE:
  471. self._showDropDown(False)
  472. skip = False
  473. if skip:
  474. event.Skip()
  475. def OnControlChanged(self, event):
  476. """!Control changed"""
  477. if self.IsShown():
  478. self._showDropDown(False)
  479. event.Skip()