123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- """!
- @package prompt.py
- @brief GRASS prompt
- Classes:
- - GPrompt
- - PromptListCtrl
- - TextCtrlAutoComplete
- @todo: fix TextCtrlAutoComplete to work also on Macs (missing
- wx.PopupWindow())
- (C) 2009 by the GRASS Development Team
- This program is free software under the GNU General Public
- License (>=v2). Read the file COPYING that comes with GRASS
- for details.
- @author Martin Landa <landa.martin gmail.com>
- """
- import os
- import sys
- import shlex
- import wx
- import wx.lib.mixins.listctrl as listmix
- from grass.script import core as grass
- import globalvar
- import utils
- import menuform
- import menudata
- class GPrompt:
- """!Interactive GRASS prompt"""
- def __init__(self, parent):
- self.parent = parent # GMFrame
-
- # dictionary of modules (description, keywords, ...)
- self.modules = self.parent.menudata.GetModules()
-
- self.panel, self.input = self.__create()
-
- def __create(self):
- """!Create widget"""
- cmdprompt = wx.Panel(self.parent)
-
- #
- # search
- #
- searchTxt = wx.StaticText(parent = cmdprompt, id = wx.ID_ANY,
- label = _("Search:"))
-
- self.searchBy = wx.Choice(parent = cmdprompt, id = wx.ID_ANY,
- choices = [_("description"),
- _("keywords")])
- winHeight = self.searchBy.GetSize()[1]
- self.search = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
- value = "", size = (-1, 25))
-
- label = wx.Button(parent = cmdprompt, id = wx.ID_ANY,
- label = _("Cmd >"), size = (-1, winHeight))
- label.SetToolTipString(_("Click for erasing command prompt"))
- ### todo: fix TextCtrlAutoComplete to work also on Macs
- ### reason: missing wx.PopupWindow()
- try:
- cmdinput = TextCtrlAutoComplete(parent = cmdprompt, id = wx.ID_ANY,
- value = "",
- style = wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
- size = (-1, winHeight),
- statusbar = self.parent.statusbar)
- except NotImplementedError:
- # wx.PopupWindow may be not available in wxMac
- # see http://trac.wxwidgets.org/ticket/9377
- cmdinput = wx.TextCtrl(parent = cmdprompt, id = wx.ID_ANY,
- value = "",
- style=wx.TE_LINEWRAP | wx.TE_PROCESS_ENTER,
- size = (-1, 25))
- self.searchBy.Enable(False)
- self.search.Enable(False)
-
- cmdinput.SetFont(wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL, 0, ''))
-
- wx.CallAfter(cmdinput.SetInsertionPoint, 0)
-
- # bidnings
- label.Bind(wx.EVT_BUTTON, self.OnCmdErase)
- cmdinput.Bind(wx.EVT_TEXT_ENTER, self.OnRunCmd)
- cmdinput.Bind(wx.EVT_TEXT, self.OnUpdateStatusBar)
- self.search.Bind(wx.EVT_TEXT, self.OnSearchModule)
-
- # layout
- sizer = wx.GridBagSizer(hgap=5, vgap=5)
- sizer.AddGrowableCol(2)
- sizer.Add(item = searchTxt,
- flag = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL,
- pos = (0, 0))
- sizer.Add(item = self.searchBy,
- flag = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
- pos = (0, 1))
-
- sizer.Add(item = self.search,
- flag = wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
- border = 5,
- pos = (0, 2))
-
- sizer.Add(item = label,
- flag = wx.LEFT | wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER,
- border = 5,
- pos = (1, 0))
-
- sizer.Add(item = cmdinput,
- flag = wx.EXPAND | wx.RIGHT,
- border = 5,
- pos = (1, 1), span = (1, 2))
-
- cmdprompt.SetSizer(sizer)
- sizer.Fit(cmdprompt)
- cmdprompt.Layout()
-
- return cmdprompt, cmdinput
- def __checkKey(self, text, keywords):
- """!Check if text is in keywords"""
- found = 0
- keys = text.split(',')
- if len(keys) > 1: # -> multiple keys
- for k in keys[:-1]:
- k = k.strip()
- for key in keywords:
- if k == key: # full match
- found += 1
- break
- k = keys[-1].strip()
- for key in keywords:
- if k in key: # partial match
- found +=1
- break
- else:
- for key in keywords:
- if text in key: # partial match
- found +=1
- break
-
- if found == len(keys):
- return True
-
- return False
-
- def GetPanel(self):
- """!Get main widget panel"""
- return self.panel
- def GetInput(self):
- """!Get main prompt widget"""
- return self.input
-
- def OnCmdErase(self, event):
- """!Erase command prompt"""
- self.input.SetValue('')
-
- def OnRunCmd(self, event):
- """!Run command"""
- cmdString = event.GetString()
-
- if self.parent.GetName() != "LayerManager":
- return
-
- if cmdString[:2] == 'd.' and not self.parent.curr_page:
- self.parent.NewDisplay(show=True)
-
- cmd = shlex.split(str(cmdString))
- if len(cmd) > 1:
- self.parent.goutput.RunCmd(cmd, switchPage = True)
- else:
- self.parent.goutput.RunCmd(cmd, switchPage = False)
-
- self.OnUpdateStatusBar(None)
-
- def OnUpdateStatusBar(self, event):
- """!Update Layer Manager status bar"""
- if self.parent.GetName() != "LayerManager":
- return
-
- if event is None:
- self.parent.statusbar.SetStatusText("")
- else:
- self.parent.statusbar.SetStatusText(_("Type GRASS command and run by pressing ENTER"))
- event.Skip()
-
- def OnSearchModule(self, event):
- """!Search module by metadata"""
- text = event.GetString()
- if not text:
- self.input.SetChoices(globalvar.grassCmd['all'])
- return
-
- modules = []
- for module, data in self.modules.iteritems():
- if self.searchBy.GetSelection() == 0: # -> description
- if text in data['desc']:
- modules.append(module)
- else: # -> keywords
- if self.__checkKey(text, data['keywords']):
- modules.append(module)
-
- self.parent.statusbar.SetStatusText(_("%d modules found") % len(modules))
- self.input.SetChoices(modules)
-
- class PromptListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
- def __init__(self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition,
- size = wx.DefaultSize, style = 0):
- wx.ListCtrl.__init__(self, parent, id, pos, size, style)
- listmix.ListCtrlAutoWidthMixin.__init__(self)
-
- class TextCtrlAutoComplete(wx.ComboBox, listmix.ColumnSorterMixin):
- def __init__ (self, parent, statusbar,
- id = wx.ID_ANY, choices = [], **kwargs):
- """!Constructor works just like wx.TextCtrl except you can pass in a
- list of choices. You can also change the choice list at any time
- by calling setChoices.
-
- Inspired by http://wiki.wxpython.org/TextCtrlAutoComplete
- """
- self.statusbar = statusbar
-
- if kwargs.has_key('style'):
- kwargs['style'] = wx.TE_PROCESS_ENTER | kwargs['style']
- else:
- kwargs['style'] = wx.TE_PROCESS_ENTER
-
- wx.ComboBox.__init__(self, parent, id, **kwargs)
-
- # some variables
- self._choices = choices
- self._hideOnNoMatch = True
- self._module = None # currently selected module
- self._choiceType = None # type of choice (module, params, flags, raster, vector ...)
- self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
-
- # sort variable needed by listmix
- self.itemDataMap = dict()
-
- # widgets
- try:
- self.dropdown = wx.PopupWindow(self)
- except NotImplementedError:
- self.Destroy()
- raise NotImplementedError
-
- # create the list and bind the events
- self.dropdownlistbox = PromptListCtrl(parent = self.dropdown,
- style = wx.LC_REPORT | wx.LC_SINGLE_SEL | \
- wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER,
- pos = wx.Point(0, 0))
-
- listmix.ColumnSorterMixin.__init__(self, 1)
-
- # set choices (list of GRASS modules)
- self._choicesCmd = globalvar.grassCmd['all']
- self._choicesMap = dict()
- for type in ('raster', 'vector'):
- self._choicesMap[type] = grass.list_strings(type = type[:4])
- # first search for GRASS module
- self.SetChoices(self._choicesCmd)
-
- self.SetMinSize(self.GetSize())
- # read history
- self.SetHistoryItems()
-
- # bindings...
- self.Bind(wx.EVT_KILL_FOCUS, self.OnControlChanged)
- self.Bind(wx.EVT_TEXT, self.OnEnteredText)
- self.Bind(wx.EVT_KEY_DOWN , self.OnKeyDown)
- self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
- # if need drop down on left click
- self.dropdown.Bind(wx.EVT_LISTBOX , self.OnListItemSelected, self.dropdownlistbox)
- self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.OnListClick)
- self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.OnListDClick)
- self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.OnListColClick)
- self.Bind(wx.EVT_COMBOBOX, self.OnCommandSelect)
-
- def _updateDataList(self, choices):
- """!Update data list"""
- # delete, if need, all the previous data
- if self.dropdownlistbox.GetColumnCount() != 0:
- self.dropdownlistbox.DeleteAllColumns()
- self.dropdownlistbox.DeleteAllItems()
- # and update the dict
- if choices:
- for numVal, data in enumerate(choices):
- self.itemDataMap[numVal] = data
- else:
- numVal = 0
- self.SetColumnCount(numVal)
-
- def _setListSize(self):
- """!Set list size"""
- choices = self._choices
- longest = 0
- for choice in choices:
- longest = max(len(choice), longest)
- longest += 3
- itemcount = min(len( choices ), 7) + 2
- charheight = self.dropdownlistbox.GetCharHeight()
- charwidth = self.dropdownlistbox.GetCharWidth()
- self.popupsize = wx.Size(charwidth*longest, charheight*itemcount)
- self.dropdownlistbox.SetSize(self.popupsize)
- self.dropdown.SetClientSize(self.popupsize)
- def _showDropDown(self, show = True):
- """!Either display the drop down list (show = True) or hide it
- (show = False).
- """
- if show:
- size = self.dropdown.GetSize()
- width, height = self.GetSizeTuple()
- x, y = self.ClientToScreenXY(0, height)
- if size.GetWidth() != width:
- size.SetWidth(width)
- self.dropdown.SetSize(size)
- self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
- if (y + size.GetHeight()) < self._screenheight:
- self.dropdown.SetPosition(wx.Point(x, y))
- else:
- self.dropdown.SetPosition(wx.Point(x, y - height - size.GetHeight()))
-
- self.dropdown.Show(show)
-
- def _listItemVisible(self):
- """!Moves the selected item to the top of the list ensuring it is
- always visible.
- """
- toSel = self.dropdownlistbox.GetFirstSelected()
- if toSel == -1:
- return
- self.dropdownlistbox.EnsureVisible(toSel)
-
- def _setValueFromSelected(self):
- """!Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
- Will do nothing if no item is selected in the wx.ListCtrl.
- """
- sel = self.dropdownlistbox.GetFirstSelected()
- if sel > -1:
- if self._colFetch != -1:
- col = self._colFetch
- else:
- col = self._colSearch
- itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
-
- cmd = shlex.split(str(self.GetValue()))
- if len(cmd) > 1:
- # -> append text (skip last item)
- if self._choiceType == 'param':
- self.SetValue(' '.join(cmd[:-1]) + ' ' + itemtext + '=')
- optType = self._module.get_param(itemtext)['prompt']
- if optType in ('raster', 'vector'):
- # -> raster/vector map
- self.SetChoices(self._choicesMap[optType], optType)
- elif self._choiceType == 'flag':
- if len(itemtext) > 1:
- prefix = '--'
- else:
- prefix = '-'
- self.SetValue(' '.join(cmd[:-1]) + ' ' + prefix + itemtext)
- elif self._choiceType in ('raster', 'vector'):
- self.SetValue(' '.join(cmd[:-1]) + ' ' + cmd[-1].split('=', 1)[0] + '=' + itemtext)
- else:
- # -> reset text
- self.SetValue(itemtext + ' ')
- self.SetInsertionPointEnd()
-
- self._showDropDown(False)
-
- def GetListCtrl(self):
- """!Method required by listmix.ColumnSorterMixin"""
- return self.dropdownlistbox
-
- def SetHistoryItems(self):
- """!Read history file and update combobox items"""
- env = grass.gisenv()
- try:
- fileHistory = open(os.path.join(env['GISDBASE'],
- env['LOCATION_NAME'],
- env['MAPSET'],
- '.bash_history'), 'r')
- except IOError:
- self.SetItems([])
- return
-
- try:
- hist = []
- for line in fileHistory.readlines():
- hist.append(line.replace('\n', ''))
-
- self.SetItems(hist)
- finally:
- fileHistory.close()
- return
-
- self.SetItems([])
-
- def SetChoices(self, choices, type = 'module'):
- """!Sets the choices available in the popup wx.ListBox.
- The items will be sorted case insensitively.
- """
- self._choices = choices
- self._choiceType = type
-
- self.dropdownlistbox.SetWindowStyleFlag(wx.LC_REPORT | wx.LC_SINGLE_SEL | \
- wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER)
- if not isinstance(choices, list):
- self._choices = [ x for x in choices ]
- if self._choiceType not in ('raster', 'vector'):
- # do not sort raster/vector maps
- utils.ListSortLower(self._choices)
-
- self._updateDataList(self._choices)
-
- self.dropdownlistbox.InsertColumn(0, "")
- for num, colVal in enumerate(self._choices):
- index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
- self.dropdownlistbox.SetStringItem(index, 0, colVal)
- self.dropdownlistbox.SetItemData(index, num)
- self._setListSize()
-
- # there is only one choice for both search and fetch if setting a single column:
- self._colSearch = 0
- self._colFetch = -1
- def OnClick(self, event):
- """Left mouse button pressed"""
- sel = self.dropdownlistbox.GetFirstSelected()
- if not self.dropdown.IsShown():
- if sel > -1:
- self.dropdownlistbox.Select(sel)
- else:
- self.dropdownlistbox.Select(0)
- self._listItemVisible()
- self._showDropDown()
- else:
- self.dropdown.Hide()
-
- def OnCommandSelect(self, event):
- """!Command selected from history"""
- self.SetFocus()
-
- def OnListClick(self, evt):
- """!Left mouse button pressed"""
- toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
- #no values on poition, return
- if toSel == -1: return
- self.dropdownlistbox.Select(toSel)
- def OnListDClick(self, evt):
- """!Mouse button double click"""
- self._setValueFromSelected()
- def OnListColClick(self, evt):
- """!Left mouse button pressed on column"""
- col = evt.GetColumn()
- # reverse the sort
- if col == self._colSearch:
- self._ascending = not self._ascending
- self.SortListItems( evt.GetColumn(), ascending=self._ascending )
- self._colSearch = evt.GetColumn()
- evt.Skip()
- def OnListItemSelected(self, event):
- """!Item selected"""
- self._setValueFromSelected()
- event.Skip()
- def OnEnteredText(self, event):
- """!Text entered"""
- text = event.GetString()
-
- if not text:
- # control is empty; hide dropdown if shown:
- if self.dropdown.IsShown():
- self._showDropDown(False)
- event.Skip()
- return
-
- cmd = shlex.split(str(text))
- pattern = str(text)
- if len(cmd) > 1:
- # search for module's options
- if cmd[0] in self._choicesCmd and not self._module:
- self._module = menuform.GUI().ParseInterface(cmd = cmd)
- if self._module:
- if len(cmd[-1].split('=', 1)) == 1:
- # new option
- if cmd[-1][0] == '-':
- # -> flags
- self.SetChoices(self._module.get_list_flags(), type = 'flag')
- pattern = cmd[-1].lstrip('-')
- else:
- # -> options
- self.SetChoices(self._module.get_list_params(), type = 'param')
- pattern = cmd[-1]
- else:
- # value
- pattern = cmd[-1].split('=', 1)[1]
- else:
- # search for GRASS modules
- if self._module:
- # -> switch back to GRASS modules list
- self.SetChoices(self._choicesCmd)
- self._module = None
- self._choiceType = None
-
- found = False
- choices = self._choices
- for numCh, choice in enumerate(choices):
- if choice.lower().startswith(pattern):
- found = True
- if found:
- self._showDropDown(True)
- item = self.dropdownlistbox.GetItem(numCh)
- toSel = item.GetId()
- self.dropdownlistbox.Select(toSel)
- break
-
- if not found:
- self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
- if self._hideOnNoMatch:
- self._showDropDown(False)
- if self._module and '=' not in cmd[-1]:
- message = ''
- if cmd[-1][0] == '-': # flag
- message = "Warning: flag <%s> not found in '%s'" % \
- (cmd[-1][1:], self._module.name)
- else: # option
- message = "Warning: option <%s> not found in '%s'" % \
- (cmd[-1], self._module.name)
- self.statusbar.SetStatusText(message)
-
- if self._module and len(cmd[-1]) == 2 and cmd[-1][-2] == '=':
- optType = self._module.get_param(cmd[-1][:-2])['prompt']
- if optType in ('raster', 'vector'):
- # -> raster/vector map
- self.SetChoices(self._choicesMap[optType], optType)
-
- self._listItemVisible()
-
- event.Skip()
-
- def OnKeyDown (self, event):
- """
- Do some work when the user press on the keys: up and down:
- move the cursor left and right: move the search
- """
- skip = True
- sel = self.dropdownlistbox.GetFirstSelected()
- visible = self.dropdown.IsShown()
- KC = event.GetKeyCode()
- if KC == wx.WXK_DOWN:
- if sel < (self.dropdownlistbox.GetItemCount() - 1):
- self.dropdownlistbox.Select(sel + 1)
- self._listItemVisible()
- self._showDropDown()
- skip = False
- elif KC == wx.WXK_UP:
- if sel > 0:
- self.dropdownlistbox.Select(sel - 1)
- self._listItemVisible()
- self._showDropDown()
- skip = False
-
- if visible:
- if event.GetKeyCode() == wx.WXK_RETURN:
- self._setValueFromSelected()
- skip = False
- if event.GetKeyCode() == wx.WXK_ESCAPE:
- self._showDropDown(False)
- skip = False
- if skip:
- event.Skip()
-
- def OnControlChanged(self, event):
- """!Control changed"""
- if self.IsShown():
- self._showDropDown(False)
-
- event.Skip()
|