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