prompt.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. """!
  2. @package prompt.py
  3. @brief wxGUI prompt
  4. Classes:
  5. - GPromptPopUp
  6. - PromptListCtrl
  7. - TextCtrlAutoComplete
  8. - GPromptSTC
  9. @todo: reduce size of STC prompt to about 3 lines
  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. @author Michael Barton <michael.barton@asu.edu>
  16. """
  17. import os
  18. import sys
  19. import shlex
  20. import wx
  21. import wx.stc
  22. import wx.lib.mixins.listctrl as listmix
  23. from grass.script import core as grass
  24. import globalvar
  25. import menudata
  26. import gcmd
  27. class GPromptPopUp:
  28. """!Interactive GRASS prompt"""
  29. def __init__(self, parent):
  30. self.parent = parent # GMFrame
  31. # dictionary of modules (description, keywords, ...)
  32. self.modules = self.parent.menudata.GetModules()
  33. self.panel, self.input = self.__create()
  34. def __create(self):
  35. """!Create widget"""
  36. cmdprompt = wx.Panel(self.parent)
  37. #
  38. # search
  39. #
  40. searchTxt = wx.StaticText(parent = cmdprompt, id = wx.ID_ANY,
  41. label = _("Find module:"))
  42. self.searchBy = wx.Choice(parent = cmdprompt, id = wx.ID_ANY,
  43. choices = [_("description"),
  44. _("keywords")])
  45. winHeight = self.searchBy.GetSize()[1]
  46. self.search = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
  47. value = "", size = (-1, 25))
  48. label = wx.Button(parent = cmdprompt, id = wx.ID_ANY,
  49. label = _("&Cmd >"), size = (-1, winHeight))
  50. label.SetToolTipString(_("Click for erasing command prompt"))
  51. ### todo: fix TextCtrlAutoComplete to work also on Macs
  52. ### reason: missing wx.PopupWindow()
  53. try:
  54. cmdinput = TextCtrlAutoComplete(parent = cmdprompt, id = wx.ID_ANY,
  55. value = "",
  56. style = wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  57. size = (-1, winHeight),
  58. statusbar = self.parent.statusbar)
  59. except NotImplementedError:
  60. # wx.PopupWindow may be not available in wxMac
  61. # see http://trac.wxwidgets.org/ticket/9377
  62. cmdinput = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
  63. value = "",
  64. style=wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  65. size = (-1, 25))
  66. self.searchBy.Enable(False)
  67. self.search.Enable(False)
  68. cmdinput.SetFont(wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL, 0, ''))
  69. wx.CallAfter(cmdinput.SetInsertionPoint, 0)
  70. # bidnings
  71. label.Bind(wx.EVT_BUTTON, self.OnCmdErase)
  72. cmdinput.Bind(wx.EVT_TEXT_ENTER, self.OnRunCmd)
  73. cmdinput.Bind(wx.EVT_TEXT, self.OnUpdateStatusBar)
  74. self.search.Bind(wx.EVT_TEXT, self.OnSearchModule)
  75. # layout
  76. sizer = wx.GridBagSizer(hgap=5, vgap=5)
  77. sizer.AddGrowableRow(1)
  78. sizer.AddGrowableCol(2)
  79. sizer.Add(item = searchTxt,
  80. flag = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL,
  81. pos = (0, 0))
  82. sizer.Add(item = self.searchBy,
  83. flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  84. pos = (0, 1))
  85. sizer.Add(item = self.search,
  86. flag = wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  87. border = 5,
  88. pos = (0, 2))
  89. sizer.Add(item = label,
  90. flag = wx.LEFT | wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
  91. border = 5,
  92. pos = (1, 0))
  93. sizer.Add(item = cmdinput,
  94. flag = wx.EXPAND | wx.RIGHT,
  95. border = 5,
  96. pos = (1, 1), span = (1, 2))
  97. cmdprompt.SetSizer(sizer)
  98. sizer.Fit(cmdprompt)
  99. cmdprompt.Layout()
  100. return cmdprompt, cmdinput
  101. def __checkKey(self, text, keywords):
  102. """!Check if text is in keywords"""
  103. found = 0
  104. keys = text.split(',')
  105. if len(keys) > 1: # -> multiple keys
  106. for k in keys[:-1]:
  107. k = k.strip()
  108. for key in keywords:
  109. if k == key: # full match
  110. found += 1
  111. break
  112. k = keys[-1].strip()
  113. for key in keywords:
  114. if k in key: # partial match
  115. found +=1
  116. break
  117. else:
  118. for key in keywords:
  119. if text in key: # partial match
  120. found +=1
  121. break
  122. if found == len(keys):
  123. return True
  124. return False
  125. def GetPanel(self):
  126. """!Get main widget panel"""
  127. return self.panel
  128. def GetInput(self):
  129. """!Get main prompt widget"""
  130. return self.input
  131. def OnCmdErase(self, event):
  132. """!Erase command prompt"""
  133. self.input.SetValue('')
  134. def OnRunCmd(self, event):
  135. """!Run command"""
  136. cmdString = event.GetString()
  137. if self.parent.GetName() != "LayerManager":
  138. return
  139. if cmdString[:2] == 'd.' and not self.parent.curr_page:
  140. self.parent.NewDisplay(show=True)
  141. cmd = shlex.split(str(cmdString))
  142. if len(cmd) > 1:
  143. self.parent.goutput.RunCmd(cmd, switchPage = True)
  144. else:
  145. self.parent.goutput.RunCmd(cmd, switchPage = False)
  146. self.OnUpdateStatusBar(None)
  147. def OnUpdateStatusBar(self, event):
  148. """!Update Layer Manager status bar"""
  149. if self.parent.GetName() != "LayerManager":
  150. return
  151. if event is None:
  152. self.parent.statusbar.SetStatusText("")
  153. else:
  154. self.parent.statusbar.SetStatusText(_("Type GRASS command and run by pressing ENTER"))
  155. event.Skip()
  156. def OnSearchModule(self, event):
  157. """!Search module by metadata"""
  158. text = event.GetString()
  159. if not text:
  160. self.input.SetChoices(globalvar.grassCmd['all'])
  161. return
  162. modules = []
  163. for module, data in self.modules.iteritems():
  164. if self.searchBy.GetSelection() == 0: # -> description
  165. if text in data['desc']:
  166. modules.append(module)
  167. else: # -> keywords
  168. if self.__checkKey(text, data['keywords']):
  169. modules.append(module)
  170. self.parent.statusbar.SetStatusText(_("%d modules found") % len(modules))
  171. self.input.SetChoices(modules)
  172. class PromptListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  173. def __init__(self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition,
  174. size = wx.DefaultSize, style = 0):
  175. wx.ListCtrl.__init__(self, parent, id, pos, size, style)
  176. listmix.ListCtrlAutoWidthMixin.__init__(self)
  177. class TextCtrlAutoComplete(wx.ComboBox, listmix.ColumnSorterMixin):
  178. def __init__ (self, parent, statusbar,
  179. id = wx.ID_ANY, choices = [], **kwargs):
  180. """!Constructor works just like wx.TextCtrl except you can pass in a
  181. list of choices. You can also change the choice list at any time
  182. by calling setChoices.
  183. Inspired by http://wiki.wxpython.org/TextCtrlAutoComplete
  184. """
  185. self.statusbar = statusbar
  186. if kwargs.has_key('style'):
  187. kwargs['style'] = wx.TE_PROCESS_ENTER | kwargs['style']
  188. else:
  189. kwargs['style'] = wx.TE_PROCESS_ENTER
  190. wx.ComboBox.__init__(self, parent, id, **kwargs)
  191. # some variables
  192. self._choices = choices
  193. self._hideOnNoMatch = True
  194. self._module = None # currently selected module
  195. self._choiceType = None # type of choice (module, params, flags, raster, vector ...)
  196. self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
  197. self._historyItem = 0 # last item
  198. # sort variable needed by listmix
  199. self.itemDataMap = dict()
  200. # widgets
  201. try:
  202. self.dropdown = wx.PopupWindow(self)
  203. except NotImplementedError:
  204. self.Destroy()
  205. raise NotImplementedError
  206. # create the list and bind the events
  207. self.dropdownlistbox = PromptListCtrl(parent = self.dropdown,
  208. style = wx.LC_REPORT | wx.LC_SINGLE_SEL | \
  209. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER,
  210. pos = wx.Point(0, 0))
  211. listmix.ColumnSorterMixin.__init__(self, 1)
  212. # set choices (list of GRASS modules)
  213. self._choicesCmd = globalvar.grassCmd['all']
  214. self._choicesMap = dict()
  215. for type in ('raster', 'vector'):
  216. self._choicesMap[type] = grass.list_strings(type = type[:4])
  217. # first search for GRASS module
  218. self.SetChoices(self._choicesCmd)
  219. self.SetMinSize(self.GetSize())
  220. # read history
  221. self.SetHistoryItems()
  222. # bindings...
  223. self.Bind(wx.EVT_KILL_FOCUS, self.OnControlChanged)
  224. self.Bind(wx.EVT_TEXT, self.OnEnteredText)
  225. self.Bind(wx.EVT_KEY_DOWN , self.OnKeyDown)
  226. ### self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
  227. # if need drop down on left click
  228. self.dropdown.Bind(wx.EVT_LISTBOX , self.OnListItemSelected, self.dropdownlistbox)
  229. self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.OnListClick)
  230. self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.OnListDClick)
  231. self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.OnListColClick)
  232. self.Bind(wx.EVT_COMBOBOX, self.OnCommandSelect)
  233. def _updateDataList(self, choices):
  234. """!Update data list"""
  235. # delete, if need, all the previous data
  236. if self.dropdownlistbox.GetColumnCount() != 0:
  237. self.dropdownlistbox.DeleteAllColumns()
  238. self.dropdownlistbox.DeleteAllItems()
  239. # and update the dict
  240. if choices:
  241. for numVal, data in enumerate(choices):
  242. self.itemDataMap[numVal] = data
  243. else:
  244. numVal = 0
  245. self.SetColumnCount(numVal)
  246. def _setListSize(self):
  247. """!Set list size"""
  248. choices = self._choices
  249. longest = 0
  250. for choice in choices:
  251. longest = max(len(choice), longest)
  252. longest += 3
  253. itemcount = min(len( choices ), 7) + 2
  254. charheight = self.dropdownlistbox.GetCharHeight()
  255. charwidth = self.dropdownlistbox.GetCharWidth()
  256. self.popupsize = wx.Size(charwidth*longest, charheight*itemcount)
  257. self.dropdownlistbox.SetSize(self.popupsize)
  258. self.dropdown.SetClientSize(self.popupsize)
  259. def _showDropDown(self, show = True):
  260. """!Either display the drop down list (show = True) or hide it
  261. (show = False).
  262. """
  263. if show:
  264. size = self.dropdown.GetSize()
  265. width, height = self.GetSizeTuple()
  266. x, y = self.ClientToScreenXY(0, height)
  267. if size.GetWidth() != width:
  268. size.SetWidth(width)
  269. self.dropdown.SetSize(size)
  270. self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
  271. if (y + size.GetHeight()) < self._screenheight:
  272. self.dropdown.SetPosition(wx.Point(x, y))
  273. else:
  274. self.dropdown.SetPosition(wx.Point(x, y - height - size.GetHeight()))
  275. self.dropdown.Show(show)
  276. def _listItemVisible(self):
  277. """!Moves the selected item to the top of the list ensuring it is
  278. always visible.
  279. """
  280. toSel = self.dropdownlistbox.GetFirstSelected()
  281. if toSel == -1:
  282. return
  283. self.dropdownlistbox.EnsureVisible(toSel)
  284. def _setModule(self, name):
  285. """!Set module's choices (flags, parameters)"""
  286. # get module's description
  287. if name in self._choicesCmd and not self._module:
  288. try:
  289. self._module = menuform.GUI().ParseInterface(cmd = [name])
  290. except IOError:
  291. self._module = None
  292. # set choices (flags)
  293. self._choicesMap['flag'] = self._module.get_list_flags()
  294. for idx in range(len(self._choicesMap['flag'])):
  295. item = self._choicesMap['flag'][idx]
  296. desc = self._module.get_flag(item)['label']
  297. if not desc:
  298. desc = self._module.get_flag(item)['description']
  299. self._choicesMap['flag'][idx] = '%s (%s)' % (item, desc)
  300. # set choices (parameters)
  301. self._choicesMap['param'] = self._module.get_list_params()
  302. for idx in range(len(self._choicesMap['param'])):
  303. item = self._choicesMap['param'][idx]
  304. desc = self._module.get_param(item)['label']
  305. if not desc:
  306. desc = self._module.get_param(item)['description']
  307. self._choicesMap['param'][idx] = '%s (%s)' % (item, desc)
  308. def _setValueFromSelected(self):
  309. """!Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
  310. Will do nothing if no item is selected in the wx.ListCtrl.
  311. """
  312. sel = self.dropdownlistbox.GetFirstSelected()
  313. if sel < 0:
  314. return
  315. if self._colFetch != -1:
  316. col = self._colFetch
  317. else:
  318. col = self._colSearch
  319. itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
  320. cmd = shlex.split(str(self.GetValue()))
  321. if len(cmd) > 0 and cmd[0] in self._choicesCmd:
  322. # -> append text (skip last item)
  323. if self._choiceType == 'param':
  324. itemtext = itemtext.split(' ')[0]
  325. self.SetValue(' '.join(cmd) + ' ' + itemtext + '=')
  326. optType = self._module.get_param(itemtext)['prompt']
  327. if optType in ('raster', 'vector'):
  328. # -> raster/vector map
  329. self.SetChoices(self._choicesMap[optType], optType)
  330. elif self._choiceType == 'flag':
  331. itemtext = itemtext.split(' ')[0]
  332. if len(itemtext) > 1:
  333. prefix = '--'
  334. else:
  335. prefix = '-'
  336. self.SetValue(' '.join(cmd[:-1]) + ' ' + prefix + itemtext)
  337. elif self._choiceType in ('raster', 'vector'):
  338. self.SetValue(' '.join(cmd[:-1]) + ' ' + cmd[-1].split('=', 1)[0] + '=' + itemtext)
  339. else:
  340. # -> reset text
  341. self.SetValue(itemtext + ' ')
  342. # define module
  343. self._setModule(itemtext)
  344. # use parameters as default choices
  345. self._choiceType = 'param'
  346. self.SetChoices(self._choicesMap['param'], type = 'param')
  347. self.SetInsertionPointEnd()
  348. self._showDropDown(False)
  349. def GetListCtrl(self):
  350. """!Method required by listmix.ColumnSorterMixin"""
  351. return self.dropdownlistbox
  352. def SetHistoryItems(self):
  353. """!Read history file and update combobox items"""
  354. env = grass.gisenv()
  355. try:
  356. fileHistory = open(os.path.join(env['GISDBASE'],
  357. env['LOCATION_NAME'],
  358. env['MAPSET'],
  359. '.bash_history'), 'r')
  360. except IOError:
  361. self.SetItems([])
  362. return
  363. try:
  364. hist = []
  365. for line in fileHistory.readlines():
  366. hist.append(line.replace('\n', ''))
  367. self.SetItems(hist)
  368. finally:
  369. fileHistory.close()
  370. return
  371. self.SetItems([])
  372. def SetChoices(self, choices, type = 'module'):
  373. """!Sets the choices available in the popup wx.ListBox.
  374. The items will be sorted case insensitively.
  375. @param choices list of choices
  376. @param type type of choices (module, param, flag, raster, vector)
  377. """
  378. self._choices = choices
  379. self._choiceType = type
  380. self.dropdownlistbox.SetWindowStyleFlag(wx.LC_REPORT | wx.LC_SINGLE_SEL |
  381. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER)
  382. if not isinstance(choices, list):
  383. self._choices = [ x for x in choices ]
  384. if self._choiceType not in ('raster', 'vector'):
  385. # do not sort raster/vector maps
  386. utils.ListSortLower(self._choices)
  387. self._updateDataList(self._choices)
  388. self.dropdownlistbox.InsertColumn(0, "")
  389. for num, colVal in enumerate(self._choices):
  390. index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
  391. self.dropdownlistbox.SetStringItem(index, 0, colVal)
  392. self.dropdownlistbox.SetItemData(index, num)
  393. self._setListSize()
  394. # there is only one choice for both search and fetch if setting a single column:
  395. self._colSearch = 0
  396. self._colFetch = -1
  397. def OnClick(self, event):
  398. """Left mouse button pressed"""
  399. sel = self.dropdownlistbox.GetFirstSelected()
  400. if not self.dropdown.IsShown():
  401. if sel > -1:
  402. self.dropdownlistbox.Select(sel)
  403. else:
  404. self.dropdownlistbox.Select(0)
  405. self._listItemVisible()
  406. self._showDropDown()
  407. else:
  408. self.dropdown.Hide()
  409. def OnCommandSelect(self, event):
  410. """!Command selected from history"""
  411. self._historyItem = event.GetSelection() - len(self.GetItems())
  412. self.SetFocus()
  413. def OnListClick(self, evt):
  414. """!Left mouse button pressed"""
  415. toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
  416. #no values on poition, return
  417. if toSel == -1: return
  418. self.dropdownlistbox.Select(toSel)
  419. def OnListDClick(self, evt):
  420. """!Mouse button double click"""
  421. self._setValueFromSelected()
  422. def OnListColClick(self, evt):
  423. """!Left mouse button pressed on column"""
  424. col = evt.GetColumn()
  425. # reverse the sort
  426. if col == self._colSearch:
  427. self._ascending = not self._ascending
  428. self.SortListItems( evt.GetColumn(), ascending=self._ascending )
  429. self._colSearch = evt.GetColumn()
  430. evt.Skip()
  431. def OnListItemSelected(self, event):
  432. """!Item selected"""
  433. self._setValueFromSelected()
  434. event.Skip()
  435. def OnEnteredText(self, event):
  436. """!Text entered"""
  437. text = event.GetString()
  438. if not text:
  439. # control is empty; hide dropdown if shown:
  440. if self.dropdown.IsShown():
  441. self._showDropDown(False)
  442. event.Skip()
  443. return
  444. try:
  445. cmd = shlex.split(str(text))
  446. except ValueError, e:
  447. self.statusbar.SetStatusText(str(e))
  448. cmd = text.split(' ')
  449. pattern = str(text)
  450. if len(cmd) > 0 and cmd[0] in self._choicesCmd and not self._module:
  451. self._setModule(cmd[0])
  452. elif len(cmd) > 1 and cmd[0] in self._choicesCmd:
  453. if self._module:
  454. if len(cmd[-1].split('=', 1)) == 1:
  455. # new option
  456. if cmd[-1][0] == '-':
  457. # -> flags
  458. self.SetChoices(self._choicesMap['flag'], type = 'flag')
  459. pattern = cmd[-1].lstrip('-')
  460. else:
  461. # -> options
  462. self.SetChoices(self._choicesMap['param'], type = 'param')
  463. pattern = cmd[-1]
  464. else:
  465. # value
  466. pattern = cmd[-1].split('=', 1)[1]
  467. else:
  468. # search for GRASS modules
  469. if self._module:
  470. # -> switch back to GRASS modules list
  471. self.SetChoices(self._choicesCmd)
  472. self._module = None
  473. self._choiceType = None
  474. self._choiceType
  475. self._choicesMap
  476. found = False
  477. choices = self._choices
  478. for numCh, choice in enumerate(choices):
  479. if choice.lower().startswith(pattern):
  480. found = True
  481. if found:
  482. self._showDropDown(True)
  483. item = self.dropdownlistbox.GetItem(numCh)
  484. toSel = item.GetId()
  485. self.dropdownlistbox.Select(toSel)
  486. break
  487. if not found:
  488. self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
  489. if self._hideOnNoMatch:
  490. self._showDropDown(False)
  491. if self._module and '=' not in cmd[-1]:
  492. message = ''
  493. if cmd[-1][0] == '-': # flag
  494. message = _("Warning: flag <%s> not found in '%s'") % \
  495. (cmd[-1][1:], self._module.name)
  496. else: # option
  497. message = _("Warning: option <%s> not found in '%s'") % \
  498. (cmd[-1], self._module.name)
  499. self.statusbar.SetStatusText(message)
  500. if self._module and len(cmd[-1]) == 2 and cmd[-1][-2] == '=':
  501. optType = self._module.get_param(cmd[-1][:-2])['prompt']
  502. if optType in ('raster', 'vector'):
  503. # -> raster/vector map
  504. self.SetChoices(self._choicesMap[optType], optType)
  505. self._listItemVisible()
  506. event.Skip()
  507. def OnKeyDown (self, event):
  508. """!Do some work when the user press on the keys: up and down:
  509. move the cursor left and right: move the search
  510. """
  511. skip = True
  512. sel = self.dropdownlistbox.GetFirstSelected()
  513. visible = self.dropdown.IsShown()
  514. KC = event.GetKeyCode()
  515. if KC == wx.WXK_RIGHT:
  516. # right -> show choices
  517. if sel < (self.dropdownlistbox.GetItemCount() - 1):
  518. self.dropdownlistbox.Select(sel + 1)
  519. self._listItemVisible()
  520. self._showDropDown()
  521. skip = False
  522. elif KC == wx.WXK_UP:
  523. if visible:
  524. if sel > 0:
  525. self.dropdownlistbox.Select(sel - 1)
  526. self._listItemVisible()
  527. self._showDropDown()
  528. skip = False
  529. else:
  530. self._historyItem -= 1
  531. try:
  532. self.SetValue(self.GetItems()[self._historyItem])
  533. except IndexError:
  534. self._historyItem += 1
  535. elif KC == wx.WXK_DOWN:
  536. if visible:
  537. if sel < (self.dropdownlistbox.GetItemCount() - 1):
  538. self.dropdownlistbox.Select(sel + 1)
  539. self._listItemVisible()
  540. self._showDropDown()
  541. skip = False
  542. else:
  543. if self._historyItem < -1:
  544. self._historyItem += 1
  545. self.SetValue(self.GetItems()[self._historyItem])
  546. if visible:
  547. if event.GetKeyCode() == wx.WXK_RETURN:
  548. self._setValueFromSelected()
  549. skip = False
  550. if event.GetKeyCode() == wx.WXK_ESCAPE:
  551. self._showDropDown(False)
  552. skip = False
  553. if skip:
  554. event.Skip()
  555. def OnControlChanged(self, event):
  556. """!Control changed"""
  557. if self.IsShown():
  558. self._showDropDown(False)
  559. event.Skip()
  560. class GPromptSTC(wx.stc.StyledTextCtrl):
  561. """!Styled GRASS prompt with autocomplete and calltips"""
  562. def __init__(self, parent, id, onRun, margin=False, wrap=None):
  563. wx.stc.StyledTextCtrl.__init__(self, parent, id)
  564. self.parent = parent
  565. self.SetUndoCollection(True)
  566. self.RunCmd = onRun
  567. #
  568. # styles
  569. #
  570. self.SetWrapMode(True)
  571. #
  572. # create command and map lists for autocompletion
  573. #
  574. self.AutoCompSetIgnoreCase(False)
  575. self.rastlist = []
  576. self.vectlist = []
  577. self.imglist = []
  578. self.r3list = []
  579. self.dblist = []
  580. self.genlist = []
  581. self.displist = []
  582. #
  583. # Get available GRASS commands and parse into lists by command type for autocomplete
  584. #
  585. for item in globalvar.grassCmd['all']:
  586. if len(item.split('.')) > 1:
  587. start,end = item.split('.',1)
  588. if start == 'r': self.rastlist.append(end)
  589. elif start == 'v': self.vectlist.append(end)
  590. elif start == 'i': self.imglist.append(end)
  591. elif start == 'r3': self.r3list.append(end)
  592. elif start == 'db': self.dblist.append(end)
  593. elif start == 'g': self.genlist.append(end)
  594. elif start == 'd': self.displist.append(end)
  595. self.rastlist.sort()
  596. self.vectlist.sort()
  597. self.imglist.sort()
  598. self.r3list.sort()
  599. self.dblist.sort()
  600. self.genlist.sort()
  601. self.displist.sort()
  602. #
  603. # Create lists of element types and possible arguments for autocomplete
  604. #
  605. self.datatypes = []
  606. self.maplists = {}
  607. self.maptype = ''
  608. self.datatypes = ['rast',
  609. 'rast3d',
  610. 'vect',
  611. 'oldvect',
  612. 'asciivect',
  613. 'labels',
  614. 'region',
  615. 'region3d',
  616. 'group',
  617. '3dview']
  618. self.drastcmd = ['d.rast',
  619. 'd.rgb',
  620. 'd.his',
  621. 'd.rast.arrow',
  622. 'd.rast.num']
  623. self.dvectcmd = ['d.vect',
  624. 'd.vect.chart'
  625. 'd.thematic.area',
  626. 'd.vect.thematic']
  627. self.rastargs = ['map',
  628. 'input',
  629. 'rast',
  630. 'raster',
  631. 'red',
  632. 'green',
  633. 'blue',
  634. 'h_map',
  635. 'i_map',
  636. 's_map',
  637. 'hue_input',
  638. 'intensity_input',
  639. 'saturation_input',
  640. 'red_input',
  641. 'green_input',
  642. 'blue_input']
  643. self.__getfiles()
  644. #
  645. # command history buffer
  646. #
  647. self.cmdbuffer = []
  648. self.cmdindex = 0
  649. #
  650. # line margins
  651. #
  652. # TODO print number only from cmdlog
  653. self.SetMarginWidth(1, 0)
  654. self.SetMarginWidth(2, 0)
  655. if margin:
  656. self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
  657. self.SetMarginWidth(0, 30)
  658. else:
  659. self.SetMarginWidth(0, 0)
  660. #
  661. # miscellaneous
  662. #
  663. self.SetViewWhiteSpace(False)
  664. # self.SetTabWidth(4)
  665. self.SetUseTabs(False)
  666. self.UsePopUp(True)
  667. self.SetSelBackground(True, "#FFFF00")
  668. self.SetUseHorizontalScrollBar(True)
  669. #
  670. # bindings
  671. #
  672. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  673. self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed)
  674. def __getfiles(self):
  675. """!Get accessible files for autocomplete"""
  676. for item in self.datatypes:
  677. mlist = grass.read_command("g.mlist", "m", type=item).splitlines()
  678. mlist.sort()
  679. self.maplists[item] = mlist
  680. def OnKeyPressed(self, event):
  681. """!Key press capture for autocompletion, calltips, and command history"""
  682. #keycodes used: "." = 46, "=" = 61, "," = 44
  683. line = ''
  684. entry = ''
  685. usage = ''
  686. cmdtype = ''
  687. cmdname = ''
  688. cmd = ''
  689. # CAN CHANGE: event.ControlDown() for manual autocomplete
  690. if event.GetKeyCode() == 46 and not event.ShiftDown():
  691. #GRASS command autocomplete when "." is pressed after r,v,i,g,db, or d
  692. listcmds = []
  693. pos = self.GetCurrentPos()
  694. self.InsertText(pos,'.')
  695. self.CharRight()
  696. entry = self.GetTextLeft()
  697. if entry not in ['r.','v.','i.','g.','db.','d.']:
  698. return
  699. if entry == 'r.': listcmds = self.rastlist
  700. elif entry == 'v.': listcmds = self.vectlist
  701. elif entry == 'i.': listcmds = self.imglist
  702. elif entry == 'r3.': listcmds = self.r3list
  703. elif entry == 'db.': listcmds = self.dblist
  704. elif entry == 'g.': listcmds = self.genlist
  705. elif entry == 'd.': listcmds = self.displist
  706. if listcmds == []:
  707. return
  708. else:
  709. self.AutoCompShow(0, " ".join(listcmds))
  710. elif event.GetKeyCode() == wx.WXK_TAB:
  711. #GRASS command calltips
  712. #Must be a command to the left somewhere
  713. pos = self.GetCurrentPos()
  714. entry = self.GetTextLeft()
  715. cmd = entry.split()[0].strip()
  716. if cmd not in globalvar.grassCmd['all']:
  717. return
  718. usage, description = self.GetCommandUsage(cmd)
  719. self.CallTipSetBackground("PALE GREEN")
  720. self.CallTipSetForeground("BLACK")
  721. self.CallTipShow(pos, usage+'\n\n'+description)
  722. elif (event.GetKeyCode() == wx.WXK_SPACE and event.ControlDown()) or \
  723. event.GetKeyCode() == 61 or event.GetKeyCode() == 44:
  724. #Autocompletion for map/data file name entry after '=', ',', or manually
  725. pos = self.GetCurrentPos()
  726. entry = self.GetTextLeft()
  727. if event.GetKeyCode() != 44:
  728. self.maptype = ''
  729. arg = ''
  730. cmdtype = ''
  731. cmdname = ''
  732. cmd = ''
  733. if entry.strip()[0:2] in ['r.','v.','i.','g.','db.','d.']:
  734. cmdtype = entry.strip()[0]
  735. cmd = entry.split()[0].strip()
  736. if cmd in globalvar.grassCmd['all']:
  737. cmdname = cmd.split('.')[1]
  738. else:
  739. #No complete GRASS command found
  740. cmd = ''
  741. cmdname = ''
  742. elif entry.strip()[0:4] == 'nviz':
  743. cmdtype = ''
  744. cmdname = cmd = 'nviz'
  745. else:
  746. #No partial or complete GRASS command found
  747. return
  748. cmdargs = entry.strip('=')
  749. try:
  750. arg = cmdargs.rsplit(' ',1)[1]
  751. except:
  752. arg = ''
  753. if event.GetKeyCode() == 61:
  754. # autocompletion after '='
  755. # insert the '=' and move to after the '=', ready for a map name
  756. self.InsertText(pos,'=')
  757. self.CharRight()
  758. maplist = []
  759. self.maptype = ''
  760. # what kind of map/data type is desired?
  761. if (((cmdtype in ['r', 'i'] or cmd in self.drastcmd) and arg in self.rastargs) or
  762. ((cmd=='nviz' or cmdtype=='r3') and arg in ['elevation','color']) or
  763. arg in ['rast', 'raster']):
  764. self.maptype = 'rast'
  765. elif (((cmdtype=='v' or cmd in self.dvectcmd) and arg in ['map', 'input']) or
  766. (cmdtype=='r3' and arg=='input') or
  767. arg in ['vect', 'vector', 'points']):
  768. self.maptype = 'vect'
  769. elif ((cmdtype=='r3' and arg in ['map', 'input']) or
  770. (cmdtype=='nviz' and arg=='volume') or arg=='rast3d'):
  771. self.maptype = 'rast3d'
  772. elif arg=='labels':
  773. self.maptype ='labels'
  774. elif arg=='region':
  775. self.maptype ='region'
  776. elif arg=='region3d':
  777. self.maptype ='region3d'
  778. elif arg=='group':
  779. self.maptype ='group'
  780. elif arg=='3dview':
  781. self.maptype ='3dview'
  782. elif event.GetKeyCode() == 44:
  783. # autocompletion after ','
  784. # if comma is pressed, use the same maptype as previous for multiple map entries
  785. # insert the comma and move to after the comma ready for a map name
  786. self.InsertText(pos,',')
  787. self.CharRight()
  788. #must apply to an entry where '=[string]' has already been entered
  789. if '=' not in arg:
  790. return
  791. elif event.GetKeyCode() == wx.WXK_SPACE and event.ControlDown():
  792. # manual autocompletion
  793. # map entries without arguments (as in r.info [mapname]) use ctrl-shift
  794. maplist = []
  795. if cmdtype=='r' or cmdtype=='i':
  796. self.maptype = 'rast'
  797. elif cmdtype=='v':
  798. self.maptype = 'vect'
  799. elif cmdtype=='r3':
  800. self.maptype = 'rast3d'
  801. if self.maptype == '':
  802. return
  803. else:
  804. maplist = self.maplists[self.maptype]
  805. self.AutoCompShow(0, " ".join(maplist))
  806. elif event.GetKeyCode() in [wx.WXK_UP,wx.WXK_DOWN] and event.ControlDown():
  807. # Command history using ctrl-up and ctrl-down
  808. if self.cmdbuffer == []: return
  809. txt = ''
  810. self.DocumentEnd()
  811. # move through command history list index values
  812. if event.GetKeyCode() == wx.WXK_UP:
  813. self.cmdindex = self.cmdindex - 1
  814. if event.GetKeyCode() == wx.WXK_DOWN:
  815. self.cmdindex = self.cmdindex + 1
  816. if self.cmdindex < 0:
  817. self.cmdindex = 0
  818. if self.cmdindex > len(self.cmdbuffer) - 1:
  819. self.cmdindex = len(self.cmdbuffer) - 1
  820. try:
  821. txt = self.cmdbuffer[self.cmdindex]
  822. except:
  823. pass
  824. # clear current line and insert command history
  825. self.DelLineLeft()
  826. self.DelLineRight()
  827. pos = self.GetCurrentPos()
  828. self.InsertText(pos,txt)
  829. self.LineEnd()
  830. elif event.GetKeyCode() == wx.WXK_RETURN and self.AutoCompActive() == False:
  831. # Run command on line when <return> is pressed
  832. # find the command to run
  833. line = str(self.GetCurLine()[0]).strip()
  834. if len(line) == 0:
  835. return
  836. # parse command into list
  837. # TODO: shell commands should probably be passed as string
  838. cmd = shlex.split(str(line))
  839. #send the command list to the processor
  840. self.RunCmd(cmd)
  841. #add command to history
  842. self.cmdbuffer.append(line)
  843. #keep command history to a managable size
  844. if len(self.cmdbuffer) > 200:
  845. del self.cmdbuffer[0]
  846. self.cmdindex = len(self.cmdbuffer)
  847. else:
  848. event.Skip()
  849. def GetTextLeft(self):
  850. """!Returns all text left of the caret"""
  851. entry = ''
  852. pos = self.GetCurrentPos()
  853. self.HomeExtend()
  854. entry = self.GetSelectedText().strip()
  855. self.SetCurrentPos(pos)
  856. return entry
  857. def GetCommandUsage(self, command):
  858. """!Returns command syntax by running command help"""
  859. usage = ''
  860. description = ''
  861. ret, out = gcmd.RunCommand(command, 'help', getErrorMsg = True)
  862. if ret == 0:
  863. cmdhelp = out.splitlines()
  864. addline = False
  865. helplist = []
  866. description = ''
  867. for line in cmdhelp:
  868. if "Usage:" in line:
  869. addline = True
  870. continue
  871. elif "Flags:" in line:
  872. addline = False
  873. break
  874. elif addline == True:
  875. line = line.strip()
  876. helplist.append(line)
  877. for line in cmdhelp:
  878. if "Description:" in line:
  879. addline = True
  880. continue
  881. elif "Keywords:" in line:
  882. addline = False
  883. break
  884. elif addline == True:
  885. description += (line + ' ')
  886. description = description.strip()
  887. for line in helplist:
  888. usage += line + '\n'
  889. return usage.strip(), description
  890. else:
  891. return ''
  892. def OnDestroy(self, evt):
  893. """!The clipboard contents can be preserved after
  894. the app has exited"""
  895. wx.TheClipboard.Flush()
  896. evt.Skip()
  897. def OnCmdErase(self, event):
  898. """!Erase command prompt"""
  899. self.Home()
  900. self.DelLineRight()
  901. def OnRunCmd(self, event):
  902. """!Run command"""
  903. cmdString = event.GetString()
  904. if self.parent.GetName() != "LayerManager":
  905. return
  906. if cmdString[:2] == 'd.' and not self.parent.curr_page:
  907. self.parent.NewDisplay(show=True)
  908. cmd = shlex.split(str(cmdString))
  909. if len(cmd) > 1:
  910. self.parent.goutput.RunCmd(cmd, switchPage = True)
  911. else:
  912. self.parent.goutput.RunCmd(cmd, switchPage = False)
  913. self.OnUpdateStatusBar(None)
  914. def OnUpdateStatusBar(self, event):
  915. """!Update Layer Manager status bar"""
  916. if self.parent.GetName() != "LayerManager":
  917. return
  918. if event is None:
  919. self.parent.statusbar.SetStatusText("")
  920. else:
  921. self.parent.statusbar.SetStatusText(_("Type GRASS command and run by pressing ENTER"))
  922. event.Skip()