prompt.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  1. """!
  2. @package gui_core.prompt
  3. @brief wxGUI command prompt
  4. Classes:
  5. - prompt::PromptListCtrl
  6. - prompt::TextCtrlAutoComplete
  7. - prompt::GPrompt
  8. - prompt::GPromptPopUp
  9. - prompt::GPromptSTC
  10. (C) 2009-2011 by the GRASS Development Team
  11. This program is free software under the GNU General Public License
  12. (>=v2). Read the file COPYING that comes with GRASS for details.
  13. @author Martin Landa <landa.martin gmail.com>
  14. @author Michael Barton <michael.barton@asu.edu>
  15. @author Vaclav Petras <wenzeslaus gmail.com> (copy&paste customization)
  16. """
  17. import os
  18. import sys
  19. import difflib
  20. import codecs
  21. import wx
  22. import wx.stc
  23. import wx.lib.mixins.listctrl as listmix
  24. from grass.script import core as grass
  25. from grass.script import task as gtask
  26. from core import globalvar
  27. from core import utils
  28. from core.gcmd import EncodeString, DecodeString, GetRealCmd
  29. class PromptListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  30. """!PopUp window used by GPromptPopUp"""
  31. def __init__(self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition,
  32. size = wx.DefaultSize, style = 0):
  33. wx.ListCtrl.__init__(self, parent, id, pos, size, style)
  34. listmix.ListCtrlAutoWidthMixin.__init__(self)
  35. class TextCtrlAutoComplete(wx.ComboBox, listmix.ColumnSorterMixin):
  36. """!Auto complete text area used by GPromptPopUp"""
  37. def __init__ (self, parent, statusbar,
  38. id = wx.ID_ANY, choices = [], **kwargs):
  39. """!Constructor works just like wx.TextCtrl except you can pass in a
  40. list of choices. You can also change the choice list at any time
  41. by calling setChoices.
  42. Inspired by http://wiki.wxpython.org/TextCtrlAutoComplete
  43. """
  44. self.statusbar = statusbar
  45. if 'style' in kwargs:
  46. kwargs['style'] = wx.TE_PROCESS_ENTER | kwargs['style']
  47. else:
  48. kwargs['style'] = wx.TE_PROCESS_ENTER
  49. wx.ComboBox.__init__(self, parent, id, **kwargs)
  50. # some variables
  51. self._choices = choices
  52. self._hideOnNoMatch = True
  53. self._module = None # currently selected module
  54. self._choiceType = None # type of choice (module, params, flags, raster, vector ...)
  55. self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
  56. self._historyItem = 0 # last item
  57. # sort variable needed by listmix
  58. self.itemDataMap = dict()
  59. # widgets
  60. try:
  61. self.dropdown = wx.PopupWindow(self)
  62. except NotImplementedError:
  63. self.Destroy()
  64. raise NotImplementedError
  65. # create the list and bind the events
  66. self.dropdownlistbox = PromptListCtrl(parent = self.dropdown,
  67. style = wx.LC_REPORT | wx.LC_SINGLE_SEL | \
  68. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER,
  69. pos = wx.Point(0, 0))
  70. listmix.ColumnSorterMixin.__init__(self, 1)
  71. # set choices (list of GRASS modules)
  72. self._choicesCmd = globalvar.grassCmd
  73. self._choicesMap = dict()
  74. for type in ('raster', 'vector'):
  75. self._choicesMap[type] = grass.list_strings(type = type[:4])
  76. # first search for GRASS module
  77. self.SetChoices(self._choicesCmd)
  78. self.SetMinSize(self.GetSize())
  79. # bindings...
  80. self.Bind(wx.EVT_KILL_FOCUS, self.OnControlChanged)
  81. self.Bind(wx.EVT_TEXT, self.OnEnteredText)
  82. self.Bind(wx.EVT_TEXT_ENTER, self.OnRunCmd)
  83. self.Bind(wx.EVT_KEY_DOWN , self.OnKeyDown)
  84. ### self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
  85. # if need drop down on left click
  86. self.dropdown.Bind(wx.EVT_LISTBOX , self.OnListItemSelected, self.dropdownlistbox)
  87. self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.OnListClick)
  88. self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.OnListDClick)
  89. self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.OnListColClick)
  90. self.Bind(wx.EVT_COMBOBOX, self.OnCommandSelect)
  91. def _updateDataList(self, choices):
  92. """!Update data list"""
  93. # delete, if need, all the previous data
  94. if self.dropdownlistbox.GetColumnCount() != 0:
  95. self.dropdownlistbox.DeleteAllColumns()
  96. self.dropdownlistbox.DeleteAllItems()
  97. # and update the dict
  98. if choices:
  99. for numVal, data in enumerate(choices):
  100. self.itemDataMap[numVal] = data
  101. else:
  102. numVal = 0
  103. self.SetColumnCount(numVal)
  104. def _setListSize(self):
  105. """!Set list size"""
  106. choices = self._choices
  107. longest = 0
  108. for choice in choices:
  109. longest = max(len(choice), longest)
  110. longest += 3
  111. itemcount = min(len( choices ), 7) + 2
  112. charheight = self.dropdownlistbox.GetCharHeight()
  113. charwidth = self.dropdownlistbox.GetCharWidth()
  114. self.popupsize = wx.Size(charwidth*longest, charheight*itemcount)
  115. self.dropdownlistbox.SetSize(self.popupsize)
  116. self.dropdown.SetClientSize(self.popupsize)
  117. def _showDropDown(self, show = True):
  118. """!Either display the drop down list (show = True) or hide it
  119. (show = False).
  120. """
  121. if show:
  122. size = self.dropdown.GetSize()
  123. width, height = self.GetSizeTuple()
  124. x, y = self.ClientToScreenXY(0, height)
  125. if size.GetWidth() != width:
  126. size.SetWidth(width)
  127. self.dropdown.SetSize(size)
  128. self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
  129. if (y + size.GetHeight()) < self._screenheight:
  130. self.dropdown.SetPosition(wx.Point(x, y))
  131. else:
  132. self.dropdown.SetPosition(wx.Point(x, y - height - size.GetHeight()))
  133. self.dropdown.Show(show)
  134. def _listItemVisible(self):
  135. """!Moves the selected item to the top of the list ensuring it is
  136. always visible.
  137. """
  138. toSel = self.dropdownlistbox.GetFirstSelected()
  139. if toSel == -1:
  140. return
  141. self.dropdownlistbox.EnsureVisible(toSel)
  142. def _setModule(self, name):
  143. """!Set module's choices (flags, parameters)"""
  144. # get module's description
  145. if name in self._choicesCmd and not self._module:
  146. try:
  147. self._module = gtask.parse_interface(name)
  148. except IOError:
  149. self._module = None
  150. # set choices (flags)
  151. self._choicesMap['flag'] = self._module.get_list_flags()
  152. for idx in range(len(self._choicesMap['flag'])):
  153. item = self._choicesMap['flag'][idx]
  154. desc = self._module.get_flag(item)['label']
  155. if not desc:
  156. desc = self._module.get_flag(item)['description']
  157. self._choicesMap['flag'][idx] = '%s (%s)' % (item, desc)
  158. # set choices (parameters)
  159. self._choicesMap['param'] = self._module.get_list_params()
  160. for idx in range(len(self._choicesMap['param'])):
  161. item = self._choicesMap['param'][idx]
  162. desc = self._module.get_param(item)['label']
  163. if not desc:
  164. desc = self._module.get_param(item)['description']
  165. self._choicesMap['param'][idx] = '%s (%s)' % (item, desc)
  166. def _setValueFromSelected(self):
  167. """!Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
  168. Will do nothing if no item is selected in the wx.ListCtrl.
  169. """
  170. sel = self.dropdownlistbox.GetFirstSelected()
  171. if sel < 0:
  172. return
  173. if self._colFetch != -1:
  174. col = self._colFetch
  175. else:
  176. col = self._colSearch
  177. itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
  178. cmd = utils.split(str(self.GetValue()))
  179. if len(cmd) > 0 and cmd[0] in self._choicesCmd:
  180. # -> append text (skip last item)
  181. if self._choiceType == 'param':
  182. itemtext = itemtext.split(' ')[0]
  183. self.SetValue(' '.join(cmd) + ' ' + itemtext + '=')
  184. optType = self._module.get_param(itemtext)['prompt']
  185. if optType in ('raster', 'vector'):
  186. # -> raster/vector map
  187. self.SetChoices(self._choicesMap[optType], optType)
  188. elif self._choiceType == 'flag':
  189. itemtext = itemtext.split(' ')[0]
  190. if len(itemtext) > 1:
  191. prefix = '--'
  192. else:
  193. prefix = '-'
  194. self.SetValue(' '.join(cmd[:-1]) + ' ' + prefix + itemtext)
  195. elif self._choiceType in ('raster', 'vector'):
  196. self.SetValue(' '.join(cmd[:-1]) + ' ' + cmd[-1].split('=', 1)[0] + '=' + itemtext)
  197. else:
  198. # -> reset text
  199. self.SetValue(itemtext + ' ')
  200. # define module
  201. self._setModule(itemtext)
  202. # use parameters as default choices
  203. self._choiceType = 'param'
  204. self.SetChoices(self._choicesMap['param'], type = 'param')
  205. self.SetInsertionPointEnd()
  206. self._showDropDown(False)
  207. def GetListCtrl(self):
  208. """!Method required by listmix.ColumnSorterMixin"""
  209. return self.dropdownlistbox
  210. def SetChoices(self, choices, type = 'module'):
  211. """!Sets the choices available in the popup wx.ListBox.
  212. The items will be sorted case insensitively.
  213. @param choices list of choices
  214. @param type type of choices (module, param, flag, raster, vector)
  215. """
  216. self._choices = choices
  217. self._choiceType = type
  218. self.dropdownlistbox.SetWindowStyleFlag(wx.LC_REPORT | wx.LC_SINGLE_SEL |
  219. wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER)
  220. if not isinstance(choices, list):
  221. self._choices = [ x for x in choices ]
  222. if self._choiceType not in ('raster', 'vector'):
  223. # do not sort raster/vector maps
  224. utils.ListSortLower(self._choices)
  225. self._updateDataList(self._choices)
  226. self.dropdownlistbox.InsertColumn(0, "")
  227. for num, colVal in enumerate(self._choices):
  228. index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
  229. self.dropdownlistbox.SetStringItem(index, 0, colVal)
  230. self.dropdownlistbox.SetItemData(index, num)
  231. self._setListSize()
  232. # there is only one choice for both search and fetch if setting a single column:
  233. self._colSearch = 0
  234. self._colFetch = -1
  235. def OnClick(self, event):
  236. """Left mouse button pressed"""
  237. sel = self.dropdownlistbox.GetFirstSelected()
  238. if not self.dropdown.IsShown():
  239. if sel > -1:
  240. self.dropdownlistbox.Select(sel)
  241. else:
  242. self.dropdownlistbox.Select(0)
  243. self._listItemVisible()
  244. self._showDropDown()
  245. else:
  246. self.dropdown.Hide()
  247. def OnCommandSelect(self, event):
  248. """!Command selected from history"""
  249. self._historyItem = event.GetSelection() - len(self.GetItems())
  250. self.SetFocus()
  251. def OnListClick(self, evt):
  252. """!Left mouse button pressed"""
  253. toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
  254. #no values on poition, return
  255. if toSel == -1: return
  256. self.dropdownlistbox.Select(toSel)
  257. def OnListDClick(self, evt):
  258. """!Mouse button double click"""
  259. self._setValueFromSelected()
  260. def OnListColClick(self, evt):
  261. """!Left mouse button pressed on column"""
  262. col = evt.GetColumn()
  263. # reverse the sort
  264. if col == self._colSearch:
  265. self._ascending = not self._ascending
  266. self.SortListItems( evt.GetColumn(), ascending=self._ascending )
  267. self._colSearch = evt.GetColumn()
  268. evt.Skip()
  269. def OnListItemSelected(self, event):
  270. """!Item selected"""
  271. self._setValueFromSelected()
  272. event.Skip()
  273. def OnEnteredText(self, event):
  274. """!Text entered"""
  275. text = event.GetString()
  276. if not text:
  277. # control is empty; hide dropdown if shown:
  278. if self.dropdown.IsShown():
  279. self._showDropDown(False)
  280. event.Skip()
  281. return
  282. try:
  283. cmd = utils.split(str(text))
  284. except ValueError, e:
  285. self.statusbar.SetStatusText(str(e))
  286. cmd = text.split(' ')
  287. pattern = str(text)
  288. if len(cmd) > 0 and cmd[0] in self._choicesCmd and not self._module:
  289. self._setModule(cmd[0])
  290. elif len(cmd) > 1 and cmd[0] in self._choicesCmd:
  291. if self._module:
  292. if len(cmd[-1].split('=', 1)) == 1:
  293. # new option
  294. if cmd[-1][0] == '-':
  295. # -> flags
  296. self.SetChoices(self._choicesMap['flag'], type = 'flag')
  297. pattern = cmd[-1].lstrip('-')
  298. else:
  299. # -> options
  300. self.SetChoices(self._choicesMap['param'], type = 'param')
  301. pattern = cmd[-1]
  302. else:
  303. # value
  304. pattern = cmd[-1].split('=', 1)[1]
  305. else:
  306. # search for GRASS modules
  307. if self._module:
  308. # -> switch back to GRASS modules list
  309. self.SetChoices(self._choicesCmd)
  310. self._module = None
  311. self._choiceType = None
  312. self._choiceType
  313. self._choicesMap
  314. found = False
  315. choices = self._choices
  316. for numCh, choice in enumerate(choices):
  317. if choice.lower().startswith(pattern):
  318. found = True
  319. if found:
  320. self._showDropDown(True)
  321. item = self.dropdownlistbox.GetItem(numCh)
  322. toSel = item.GetId()
  323. self.dropdownlistbox.Select(toSel)
  324. break
  325. if not found:
  326. self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
  327. if self._hideOnNoMatch:
  328. self._showDropDown(False)
  329. if self._module and '=' not in cmd[-1]:
  330. message = ''
  331. if cmd[-1][0] == '-': # flag
  332. message = _("Warning: flag <%(flag)s> not found in '%(module)s'") % \
  333. { 'flag' : cmd[-1][1:], 'module' : self._module.name }
  334. else: # option
  335. message = _("Warning: option <%(param)s> not found in '%(module)s'") % \
  336. { 'param' : cmd[-1], 'module' : self._module.name }
  337. self.statusbar.SetStatusText(message)
  338. if self._module and len(cmd[-1]) == 2 and cmd[-1][-2] == '=':
  339. optType = self._module.get_param(cmd[-1][:-2])['prompt']
  340. if optType in ('raster', 'vector'):
  341. # -> raster/vector map
  342. self.SetChoices(self._choicesMap[optType], optType)
  343. self._listItemVisible()
  344. event.Skip()
  345. def OnKeyDown (self, event):
  346. """!Do some work when the user press on the keys: up and down:
  347. move the cursor left and right: move the search
  348. """
  349. skip = True
  350. sel = self.dropdownlistbox.GetFirstSelected()
  351. visible = self.dropdown.IsShown()
  352. KC = event.GetKeyCode()
  353. if KC == wx.WXK_RIGHT:
  354. # right -> show choices
  355. if sel < (self.dropdownlistbox.GetItemCount() - 1):
  356. self.dropdownlistbox.Select(sel + 1)
  357. self._listItemVisible()
  358. self._showDropDown()
  359. skip = False
  360. elif KC == wx.WXK_UP:
  361. if visible:
  362. if sel > 0:
  363. self.dropdownlistbox.Select(sel - 1)
  364. self._listItemVisible()
  365. self._showDropDown()
  366. skip = False
  367. else:
  368. self._historyItem -= 1
  369. try:
  370. self.SetValue(self.GetItems()[self._historyItem])
  371. except IndexError:
  372. self._historyItem += 1
  373. elif KC == wx.WXK_DOWN:
  374. if visible:
  375. if sel < (self.dropdownlistbox.GetItemCount() - 1):
  376. self.dropdownlistbox.Select(sel + 1)
  377. self._listItemVisible()
  378. self._showDropDown()
  379. skip = False
  380. else:
  381. if self._historyItem < -1:
  382. self._historyItem += 1
  383. self.SetValue(self.GetItems()[self._historyItem])
  384. if visible:
  385. if event.GetKeyCode() == wx.WXK_RETURN:
  386. self._setValueFromSelected()
  387. skip = False
  388. if event.GetKeyCode() == wx.WXK_ESCAPE:
  389. self._showDropDown(False)
  390. skip = False
  391. if skip:
  392. event.Skip()
  393. def OnControlChanged(self, event):
  394. """!Control changed"""
  395. if self.IsShown():
  396. self._showDropDown(False)
  397. event.Skip()
  398. class GPrompt(object):
  399. """!Abstract class for interactive wxGUI prompt
  400. See subclass GPromptPopUp and GPromptSTC.
  401. """
  402. def __init__(self, parent, modulesData):
  403. self.parent = parent # GConsole
  404. self.panel = self.parent.GetPanel()
  405. if self.parent.parent.GetName() not in ("LayerManager", "Modeler"):
  406. self.standAlone = True
  407. else:
  408. self.standAlone = False
  409. # probably only subclasses need this
  410. self.modulesData = modulesData
  411. self.mapList = self._getListOfMaps()
  412. self.mapsetList = utils.ListOfMapsets()
  413. # auto complete items
  414. self.autoCompList = list()
  415. self.autoCompFilter = None
  416. # command description (gtask.grassTask)
  417. self.cmdDesc = None
  418. self.cmdbuffer = self._readHistory()
  419. self.cmdindex = len(self.cmdbuffer)
  420. # list of traced commands
  421. self.commands = list()
  422. def _readHistory(self):
  423. """!Get list of commands from history file"""
  424. hist = list()
  425. env = grass.gisenv()
  426. try:
  427. fileHistory = codecs.open(os.path.join(env['GISDBASE'],
  428. env['LOCATION_NAME'],
  429. env['MAPSET'],
  430. '.bash_history'),
  431. encoding = 'utf-8', mode = 'r', errors='replace')
  432. except IOError:
  433. return hist
  434. try:
  435. for line in fileHistory.readlines():
  436. hist.append(line.replace('\n', ''))
  437. finally:
  438. fileHistory.close()
  439. return hist
  440. def _getListOfMaps(self):
  441. """!Get list of maps"""
  442. result = dict()
  443. result['raster'] = grass.list_strings('rast')
  444. result['vector'] = grass.list_strings('vect')
  445. return result
  446. def _runCmd(self, cmdString):
  447. """!Run command
  448. @param cmdString command to run (given as a string)
  449. """
  450. if self.parent.GetName() == "ModelerDialog":
  451. self.parent.OnOk(None)
  452. return
  453. if not cmdString or self.standAlone:
  454. return
  455. if cmdString[:2] == 'd.' and not self.parent.parent.GetMapDisplay():
  456. self.parent.parent.NewDisplay(show = True)
  457. self.commands.append(cmdString) # trace commands
  458. # parse command into list
  459. try:
  460. cmd = utils.split(str(cmdString))
  461. except UnicodeError:
  462. cmd = utils.split(EncodeString((cmdString)))
  463. cmd = map(DecodeString, cmd)
  464. # send the command list to the processor
  465. if cmd[0] in ('r.mapcalc', 'r3.mapcalc') and len(cmd) == 1:
  466. self.parent.parent.OnMapCalculator(event = None, cmd = cmd)
  467. else:
  468. self.parent.RunCmd(cmd)
  469. # add command to history & clean prompt
  470. self.UpdateCmdHistory(cmd)
  471. self.OnCmdErase(None)
  472. self.parent.parent.statusbar.SetStatusText('')
  473. def OnUpdateStatusBar(self, event):
  474. """!Update Layer Manager status bar"""
  475. if self.standAlone:
  476. return
  477. if event is None:
  478. self.parent.parent.statusbar.SetStatusText("")
  479. else:
  480. self.parent.parent.statusbar.SetStatusText(_("Type GRASS command and run by pressing ENTER"))
  481. event.Skip()
  482. def GetPanel(self):
  483. """!Get main widget panel"""
  484. return self.panel
  485. def GetInput(self):
  486. """!Get main prompt widget"""
  487. return self.input
  488. def SetFilter(self, data, module = True):
  489. """!Set filter
  490. @param data data dict
  491. @param module True to filter modules, otherwise data
  492. """
  493. if module:
  494. # TODO: remove this and module param
  495. raise NotImplementedError("Replace by call to common ModulesData object (SetFilter with module=True)")
  496. else:
  497. if data:
  498. self.dataList = data
  499. else:
  500. self.dataList = self._getListOfMaps()
  501. def GetCommands(self):
  502. """!Get list of launched commands"""
  503. return self.commands
  504. def ClearCommands(self):
  505. """!Clear list of commands"""
  506. del self.commands[:]
  507. class GPromptPopUp(GPrompt, TextCtrlAutoComplete):
  508. """!Interactive wxGUI prompt - popup version"""
  509. def __init__(self, parent):
  510. GPrompt.__init__(self, parent)
  511. ### todo: fix TextCtrlAutoComplete to work also on Macs
  512. ### reason: missing wx.PopupWindow()
  513. try:
  514. TextCtrlAutoComplete.__init__(self, parent = self.panel, id = wx.ID_ANY,
  515. value = "",
  516. style = wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  517. statusbar = self.parent.parent.statusbar)
  518. self.SetItems(self._readHistory())
  519. except NotImplementedError:
  520. # wx.PopupWindow may be not available in wxMac
  521. # see http://trac.wxwidgets.org/ticket/9377
  522. wx.TextCtrl.__init__(parent = self.panel, id = wx.ID_ANY,
  523. value = "",
  524. style=wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
  525. size = (-1, 25))
  526. self.searchBy.Enable(False)
  527. self.search.Enable(False)
  528. self.SetFont(wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL, 0, ''))
  529. wx.CallAfter(self.SetInsertionPoint, 0)
  530. # bidnings
  531. self.Bind(wx.EVT_TEXT_ENTER, self.OnRunCmd)
  532. self.Bind(wx.EVT_TEXT, self.OnUpdateStatusBar)
  533. def OnCmdErase(self, event):
  534. """!Erase command prompt"""
  535. self.input.SetValue('')
  536. def OnRunCmd(self, event):
  537. """!Run command"""
  538. self._runCmd(event.GetString())
  539. class GPromptSTC(GPrompt, wx.stc.StyledTextCtrl):
  540. """!Styled wxGUI prompt with autocomplete and calltips"""
  541. def __init__(self, parent, modulesData, id = wx.ID_ANY, margin = False):
  542. GPrompt.__init__(self, parent, modulesData)
  543. wx.stc.StyledTextCtrl.__init__(self, self.panel, id)
  544. #
  545. # styles
  546. #
  547. self.SetWrapMode(True)
  548. self.SetUndoCollection(True)
  549. #
  550. # create command and map lists for autocompletion
  551. #
  552. self.AutoCompSetIgnoreCase(False)
  553. #
  554. # line margins
  555. #
  556. # TODO print number only from cmdlog
  557. self.SetMarginWidth(1, 0)
  558. self.SetMarginWidth(2, 0)
  559. if margin:
  560. self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
  561. self.SetMarginWidth(0, 30)
  562. else:
  563. self.SetMarginWidth(0, 0)
  564. #
  565. # miscellaneous
  566. #
  567. self.SetViewWhiteSpace(False)
  568. self.SetUseTabs(False)
  569. self.UsePopUp(True)
  570. self.SetSelBackground(True, "#FFFF00")
  571. self.SetUseHorizontalScrollBar(True)
  572. #
  573. # bindings
  574. #
  575. self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
  576. self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed)
  577. self.Bind(wx.stc.EVT_STC_AUTOCOMP_SELECTION, self.OnItemSelected)
  578. self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemChanged)
  579. self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
  580. def OnTextSelectionChanged(self, event):
  581. """!Copy selected text to clipboard and skip event.
  582. The same function is in GStc class (goutput.py).
  583. """
  584. wx.CallAfter(self.Copy)
  585. event.Skip()
  586. def OnItemChanged(self, event):
  587. """!Change text in statusbar
  588. if the item selection in the auto-completion list is changed"""
  589. # list of commands
  590. if self.toComplete['entity'] == 'command':
  591. item = self.toComplete['cmd'].rpartition('.')[0] + '.' + self.autoCompList[event.GetIndex()]
  592. try:
  593. desc = self.modulesData.GetCommandDesc(item)
  594. except KeyError:
  595. desc = ''
  596. self.ShowStatusText(desc)
  597. # list of flags
  598. elif self.toComplete['entity'] == 'flags':
  599. desc = self.cmdDesc.get_flag(self.autoCompList[event.GetIndex()])['description']
  600. self.ShowStatusText(desc)
  601. # list of parameters
  602. elif self.toComplete['entity'] == 'params':
  603. item = self.cmdDesc.get_param(self.autoCompList[event.GetIndex()])
  604. desc = item['name'] + '=' + item['type']
  605. if not item['required']:
  606. desc = '[' + desc + ']'
  607. desc += ': ' + item['description']
  608. self.ShowStatusText(desc)
  609. # list of flags and commands
  610. elif self.toComplete['entity'] == 'params+flags':
  611. if self.autoCompList[event.GetIndex()][0] == '-':
  612. desc = self.cmdDesc.get_flag(self.autoCompList[event.GetIndex()].strip('-'))['description']
  613. else:
  614. item = self.cmdDesc.get_param(self.autoCompList[event.GetIndex()])
  615. desc = item['name'] + '=' + item['type']
  616. if not item['required']:
  617. desc = '[' + desc + ']'
  618. desc += ': ' + item['description']
  619. self.ShowStatusText(desc)
  620. else:
  621. self.ShowStatusText('')
  622. def OnItemSelected(self, event):
  623. """!Item selected from the list"""
  624. lastWord = self.GetWordLeft()
  625. # to insert selection correctly if selected word partly matches written text
  626. match = difflib.SequenceMatcher(None, event.GetText(), lastWord)
  627. matchTuple = match.find_longest_match(0, len(event.GetText()), 0, len(lastWord))
  628. compl = event.GetText()[matchTuple[2]:]
  629. text = self.GetTextLeft() + compl
  630. # add space or '=' at the end
  631. end = '='
  632. for char in ('.','-','='):
  633. if text.split(' ')[-1].find(char) >= 0:
  634. end = ' '
  635. compl += end
  636. text += end
  637. self.AddText(compl)
  638. pos = len(text)
  639. self.SetCurrentPos(pos)
  640. cmd = text.strip().split(' ')[0]
  641. if not self.cmdDesc or cmd != self.cmdDesc.get_name():
  642. if cmd in ('r.mapcalc', 'r3.mapcalc') and \
  643. self.parent.parent.GetName() == 'LayerManager':
  644. self.parent.parent.OnMapCalculator(event = None, cmd = [cmd])
  645. # add command to history & clean prompt
  646. self.UpdateCmdHistory([cmd])
  647. self.OnCmdErase(None)
  648. else:
  649. try:
  650. self.cmdDesc = gtask.parse_interface(GetRealCmd(cmd))
  651. except IOError:
  652. self.cmdDesc = None
  653. def OnKillFocus(self, event):
  654. """!Hides autocomplete"""
  655. # hide autocomplete
  656. if self.AutoCompActive():
  657. self.AutoCompCancel()
  658. event.Skip()
  659. def SetTextAndFocus(self, text):
  660. pos = len(text)
  661. self.SetText(text)
  662. self.SetSelectionStart(pos)
  663. self.SetCurrentPos(pos)
  664. self.SetFocus()
  665. def UpdateCmdHistory(self, cmd):
  666. """!Update command history
  667. @param cmd command given as a list
  668. """
  669. # add command to history
  670. self.cmdbuffer.append(' '.join(cmd))
  671. # keep command history to a managable size
  672. if len(self.cmdbuffer) > 200:
  673. del self.cmdbuffer[0]
  674. self.cmdindex = len(self.cmdbuffer)
  675. def EntityToComplete(self):
  676. """!Determines which part of command (flags, parameters) should
  677. be completed at current cursor position"""
  678. entry = self.GetTextLeft()
  679. toComplete = dict()
  680. try:
  681. cmd = entry.split()[0].strip()
  682. except IndexError:
  683. return None
  684. try:
  685. splitted = utils.split(str(entry))
  686. except ValueError: # No closing quotation error
  687. return None
  688. if len(splitted) > 1:
  689. if cmd in globalvar.grassCmd:
  690. toComplete['cmd'] = cmd
  691. if entry[-1] == ' ':
  692. words = entry.split(' ')
  693. if any(word.startswith('-') for word in words):
  694. toComplete['entity'] = 'params'
  695. else:
  696. toComplete['entity'] = 'params+flags'
  697. else:
  698. # get word left from current position
  699. word = self.GetWordLeft(withDelimiter = True)
  700. if word[0] == '=' and word[-1] == '@':
  701. toComplete['entity'] = 'mapsets'
  702. elif word[0] == '=':
  703. # get name of parameter
  704. paramName = self.GetWordLeft(withDelimiter = False, ignoredDelimiter = '=').strip('=')
  705. if paramName:
  706. try:
  707. param = self.cmdDesc.get_param(paramName)
  708. except (ValueError, AttributeError):
  709. return None
  710. else:
  711. return None
  712. if param['values']:
  713. toComplete['entity'] = 'param values'
  714. elif param['prompt'] == 'raster' and param['element'] == 'cell':
  715. toComplete['entity'] = 'raster map'
  716. elif param['prompt'] == 'vector' and param['element'] == 'vector':
  717. toComplete['entity'] = 'vector map'
  718. elif word[0] == '-':
  719. toComplete['entity'] = 'flags'
  720. elif word[0] == ' ':
  721. toComplete['entity'] = 'params'
  722. else:
  723. return None
  724. else:
  725. toComplete['entity'] = 'command'
  726. toComplete['cmd'] = cmd
  727. return toComplete
  728. def GetWordLeft(self, withDelimiter = False, ignoredDelimiter = None):
  729. """!Get word left from current cursor position. The beginning
  730. of the word is given by space or chars: .,-=
  731. @param withDelimiter returns the word with the initial delimeter
  732. @param ignoredDelimiter finds the word ignoring certain delimeter
  733. """
  734. textLeft = self.GetTextLeft()
  735. parts = list()
  736. if ignoredDelimiter is None:
  737. ignoredDelimiter = ''
  738. for char in set(' .,-=') - set(ignoredDelimiter):
  739. if not withDelimiter:
  740. delimiter = ''
  741. else:
  742. delimiter = char
  743. parts.append(delimiter + textLeft.rpartition(char)[2])
  744. return min(parts, key=lambda x: len(x))
  745. def ShowList(self):
  746. """!Show sorted auto-completion list if it is not empty"""
  747. if len(self.autoCompList) > 0:
  748. self.autoCompList.sort()
  749. self.AutoCompShow(lenEntered = 0, itemList = ' '.join(self.autoCompList))
  750. def OnKeyPressed(self, event):
  751. """!Key press capture for autocompletion, calltips, and command history
  752. @todo event.ControlDown() for manual autocomplete
  753. """
  754. # keycodes used: "." = 46, "=" = 61, "-" = 45
  755. pos = self.GetCurrentPos()
  756. # complete command after pressing '.'
  757. if event.GetKeyCode() == 46 and not event.ShiftDown():
  758. self.autoCompList = list()
  759. entry = self.GetTextLeft()
  760. self.InsertText(pos, '.')
  761. self.CharRight()
  762. self.toComplete = self.EntityToComplete()
  763. try:
  764. if self.toComplete['entity'] == 'command':
  765. self.autoCompList = self.modulesData.GetDictOfModules()[entry.strip()]
  766. except (KeyError, TypeError):
  767. return
  768. self.ShowList()
  769. # complete flags after pressing '-'
  770. elif event.GetKeyCode() == 45 and not event.ShiftDown():
  771. self.autoCompList = list()
  772. entry = self.GetTextLeft()
  773. self.InsertText(pos, '-')
  774. self.CharRight()
  775. self.toComplete = self.EntityToComplete()
  776. if self.toComplete['entity'] == 'flags' and self.cmdDesc:
  777. if self.GetTextLeft()[-2:] == ' -': # complete e.g. --quite
  778. for flag in self.cmdDesc.get_options()['flags']:
  779. if len(flag['name']) == 1:
  780. self.autoCompList.append(flag['name'])
  781. else:
  782. for flag in self.cmdDesc.get_options()['flags']:
  783. if len(flag['name']) > 1:
  784. self.autoCompList.append(flag['name'])
  785. self.ShowList()
  786. # complete map or values after parameter
  787. elif event.GetKeyCode() == 61 and not event.ShiftDown():
  788. self.autoCompList = list()
  789. self.InsertText(pos, '=')
  790. self.CharRight()
  791. self.toComplete = self.EntityToComplete()
  792. if self.toComplete and 'entity' in self.toComplete:
  793. if self.toComplete['entity'] == 'raster map':
  794. self.autoCompList = self.mapList['raster']
  795. elif self.toComplete['entity'] == 'vector map':
  796. self.autoCompList = self.mapList['vector']
  797. elif self.toComplete['entity'] == 'param values':
  798. param = self.GetWordLeft(withDelimiter = False, ignoredDelimiter='=').strip(' =')
  799. self.autoCompList = self.cmdDesc.get_param(param)['values']
  800. self.ShowList()
  801. # complete mapset ('@')
  802. elif event.GetKeyCode() == 50 and event.ShiftDown():
  803. self.autoCompList = list()
  804. self.InsertText(pos, '@')
  805. self.CharRight()
  806. self.toComplete = self.EntityToComplete()
  807. if self.toComplete and self.toComplete['entity'] == 'mapsets':
  808. self.autoCompList = self.mapsetList
  809. self.ShowList()
  810. # complete after pressing CTRL + Space
  811. elif event.GetKeyCode() == wx.WXK_SPACE and event.ControlDown():
  812. self.autoCompList = list()
  813. self.toComplete = self.EntityToComplete()
  814. if self.toComplete is None:
  815. return
  816. #complete command
  817. if self.toComplete['entity'] == 'command':
  818. for command in globalvar.grassCmd:
  819. if command.find(self.toComplete['cmd']) == 0:
  820. dotNumber = list(self.toComplete['cmd']).count('.')
  821. self.autoCompList.append(command.split('.',dotNumber)[-1])
  822. # complete flags in such situations (| is cursor):
  823. # r.colors -| ...w, q, l
  824. # r.colors -w| ...w, q, l
  825. elif self.toComplete['entity'] == 'flags' and self.cmdDesc:
  826. for flag in self.cmdDesc.get_options()['flags']:
  827. if len(flag['name']) == 1:
  828. self.autoCompList.append(flag['name'])
  829. # complete parameters in such situations (| is cursor):
  830. # r.colors -w | ...color, map, rast, rules
  831. # r.colors col| ...color
  832. elif self.toComplete['entity'] == 'params' and self.cmdDesc:
  833. for param in self.cmdDesc.get_options()['params']:
  834. if param['name'].find(self.GetWordLeft(withDelimiter=False)) == 0:
  835. self.autoCompList.append(param['name'])
  836. # complete flags or parameters in such situations (| is cursor):
  837. # r.colors | ...-w, -q, -l, color, map, rast, rules
  838. # r.colors color=grey | ...-w, -q, -l, color, map, rast, rules
  839. elif self.toComplete['entity'] == 'params+flags' and self.cmdDesc:
  840. self.autoCompList = list()
  841. for param in self.cmdDesc.get_options()['params']:
  842. self.autoCompList.append(param['name'])
  843. for flag in self.cmdDesc.get_options()['flags']:
  844. if len(flag['name']) == 1:
  845. self.autoCompList.append('-' + flag['name'])
  846. else:
  847. self.autoCompList.append('--' + flag['name'])
  848. self.ShowList()
  849. # complete map or values after parameter
  850. # r.buffer input=| ...list of raster maps
  851. # r.buffer units=| ... feet, kilometers, ...
  852. elif self.toComplete['entity'] == 'raster map':
  853. self.autoCompList = list()
  854. self.autoCompList = self.mapList['raster']
  855. elif self.toComplete['entity'] == 'vector map':
  856. self.autoCompList = list()
  857. self.autoCompList = self.mapList['vector']
  858. elif self.toComplete['entity'] == 'param values':
  859. self.autoCompList = list()
  860. param = self.GetWordLeft(withDelimiter = False, ignoredDelimiter='=').strip(' =')
  861. self.autoCompList = self.cmdDesc.get_param(param)['values']
  862. self.ShowList()
  863. elif event.GetKeyCode() == wx.WXK_TAB:
  864. # show GRASS command calltips (to hide press 'ESC')
  865. entry = self.GetTextLeft()
  866. try:
  867. cmd = entry.split()[0].strip()
  868. except IndexError:
  869. cmd = ''
  870. if cmd not in globalvar.grassCmd:
  871. return
  872. info = gtask.command_info(GetRealCmd(cmd))
  873. self.CallTipSetBackground("#f4f4d1")
  874. self.CallTipSetForeground("BLACK")
  875. self.CallTipShow(pos, info['usage'] + '\n\n' + info['description'])
  876. elif event.GetKeyCode() in [wx.WXK_UP, wx.WXK_DOWN] and \
  877. not self.AutoCompActive():
  878. # Command history using up and down
  879. if len(self.cmdbuffer) < 1:
  880. return
  881. self.DocumentEnd()
  882. # move through command history list index values
  883. if event.GetKeyCode() == wx.WXK_UP:
  884. self.cmdindex = self.cmdindex - 1
  885. if event.GetKeyCode() == wx.WXK_DOWN:
  886. self.cmdindex = self.cmdindex + 1
  887. if self.cmdindex < 0:
  888. self.cmdindex = 0
  889. if self.cmdindex > len(self.cmdbuffer) - 1:
  890. self.cmdindex = len(self.cmdbuffer) - 1
  891. try:
  892. txt = self.cmdbuffer[self.cmdindex]
  893. except:
  894. txt = ''
  895. # clear current line and insert command history
  896. self.DelLineLeft()
  897. self.DelLineRight()
  898. pos = self.GetCurrentPos()
  899. self.InsertText(pos,txt)
  900. self.LineEnd()
  901. self.parent.parent.statusbar.SetStatusText('')
  902. elif event.GetKeyCode() == wx.WXK_RETURN and \
  903. self.AutoCompActive() == False:
  904. # run command on line when <return> is pressed
  905. self._runCmd(self.GetCurLine()[0].strip())
  906. elif event.GetKeyCode() == wx.WXK_SPACE:
  907. items = self.GetTextLeft().split()
  908. if len(items) == 1:
  909. cmd = items[0].strip()
  910. if cmd in globalvar.grassCmd and \
  911. cmd != 'r.mapcalc' and \
  912. (not self.cmdDesc or cmd != self.cmdDesc.get_name()):
  913. try:
  914. self.cmdDesc = gtask.parse_interface(GetRealCmd(cmd))
  915. except IOError:
  916. self.cmdDesc = None
  917. event.Skip()
  918. else:
  919. event.Skip()
  920. def ShowStatusText(self, text):
  921. """!Sets statusbar text, if it's too long, it is cut off"""
  922. maxLen = self.parent.parent.statusbar.GetFieldRect(0).GetWidth()/ 7 # any better way?
  923. if len(text) < maxLen:
  924. self.parent.parent.statusbar.SetStatusText(text)
  925. else:
  926. self.parent.parent.statusbar.SetStatusText(text[:maxLen]+'...')
  927. def GetTextLeft(self):
  928. """!Returns all text left of the caret"""
  929. pos = self.GetCurrentPos()
  930. self.HomeExtend()
  931. entry = self.GetSelectedText()
  932. self.SetCurrentPos(pos)
  933. return entry
  934. def OnDestroy(self, event):
  935. """!The clipboard contents can be preserved after
  936. the app has exited"""
  937. wx.TheClipboard.Flush()
  938. event.Skip()
  939. def OnCmdErase(self, event):
  940. """!Erase command prompt"""
  941. self.Home()
  942. self.DelLineRight()