prompt.py 16 KB

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