prompt.py 16 KB

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