prompt.py 20 KB

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