source: grass/trunk/gui/wxpython/gui_core/prompt.py

Last change on this file was 74307, checked in by neteler, 5 years ago

i18N: cleanup gettext usage for Python code (fixes #3790) (contributed by pmav99)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id
  • Property svn:mime-type set to text/x-python
File size: 22.0 KB
Line 
1"""
2@package gui_core.prompt
3
4@brief wxGUI command prompt
5
6Classes:
7 - prompt::GPrompt
8 - prompt::GPromptSTC
9
10(C) 2009-2014 by the GRASS Development Team
11
12This program is free software under the GNU General Public License
13(>=v2). Read the file COPYING that comes with GRASS for details.
14
15@author Martin Landa <landa.martin gmail.com>
16@author Michael Barton <michael.barton@asu.edu>
17@author Vaclav Petras <wenzeslaus gmail.com> (copy&paste customization)
18"""
19
20import os
21import difflib
22import codecs
23import sys
24
25import wx
26import wx.stc
27
28from grass.script import core as grass
29from grass.script import task as gtask
30
31from grass.pydispatch.signal import Signal
32
33from core import globalvar
34from core import utils
35from core.gcmd import EncodeString, DecodeString
36
37
38class GPrompt(object):
39 """Abstract class for interactive wxGUI prompt
40
41 Signal promptRunCmd - emitted to run command from prompt
42 - attribute 'cmd'
43
44 See subclass GPromptPopUp and GPromptSTC.
45 """
46
47 def __init__(self, parent, menuModel):
48 self.parent = parent # GConsole
49 self.panel = self.parent.GetPanel()
50
51 self.promptRunCmd = Signal('GPrompt.promptRunCmd')
52
53 # probably only subclasses need this
54 self._menuModel = menuModel
55
56 self.mapList = self._getListOfMaps()
57 self.mapsetList = utils.ListOfMapsets()
58
59 # auto complete items
60 self.autoCompList = list()
61 self.autoCompFilter = None
62
63 # command description (gtask.grassTask)
64 self.cmdDesc = None
65
66 self.cmdbuffer = self._readHistory()
67 self.cmdindex = len(self.cmdbuffer)
68
69 # list of traced commands
70 self.commands = list()
71
72 def _readHistory(self):
73 """Get list of commands from history file"""
74 hist = list()
75 env = grass.gisenv()
76 try:
77 fileHistory = codecs.open(
78 os.path.join(
79 env['GISDBASE'],
80 env['LOCATION_NAME'],
81 env['MAPSET'],
82 '.bash_history'),
83 encoding='utf-8',
84 mode='r',
85 errors='replace')
86 except IOError:
87 return hist
88
89 try:
90 for line in fileHistory.readlines():
91 hist.append(line.replace('\n', ''))
92 finally:
93 fileHistory.close()
94
95 return hist
96
97 def _getListOfMaps(self):
98 """Get list of maps"""
99 result = dict()
100 result['raster'] = grass.list_strings('raster')
101 result['vector'] = grass.list_strings('vector')
102
103 return result
104
105 def _runCmd(self, cmdString):
106 """Run command
107
108 :param str cmdString: command to run
109 """
110 if not cmdString:
111 return
112
113 # parse command into list
114 try:
115 cmd = utils.split(str(cmdString))
116 except UnicodeError:
117 cmd = utils.split(EncodeString((cmdString)))
118 cmd = list(map(DecodeString, cmd))
119
120 self.promptRunCmd.emit(cmd=cmd)
121
122 self.OnCmdErase(None)
123 self.ShowStatusText('')
124
125 def GetCommands(self):
126 """Get list of launched commands"""
127 return self.commands
128
129 def ClearCommands(self):
130 """Clear list of commands"""
131 del self.commands[:]
132
133
134class GPromptSTC(GPrompt, wx.stc.StyledTextCtrl):
135 """Styled wxGUI prompt with autocomplete and calltips"""
136
137 def __init__(self, parent, menuModel, margin=False):
138 GPrompt.__init__(self, parent=parent, menuModel=menuModel)
139 wx.stc.StyledTextCtrl.__init__(self, self.panel, id=wx.ID_ANY)
140
141 #
142 # styles
143 #
144 self.SetWrapMode(True)
145 self.SetUndoCollection(True)
146
147 #
148 # create command and map lists for autocompletion
149 #
150 self.AutoCompSetIgnoreCase(False)
151
152 #
153 # line margins
154 #
155 # TODO print number only from cmdlog
156 self.SetMarginWidth(1, 0)
157 self.SetMarginWidth(2, 0)
158 if margin:
159 self.SetMarginType(0, wx.stc.STC_MARGIN_NUMBER)
160 self.SetMarginWidth(0, 30)
161 else:
162 self.SetMarginWidth(0, 0)
163
164 #
165 # miscellaneous
166 #
167 self.SetViewWhiteSpace(False)
168 self.SetUseTabs(False)
169 self.UsePopUp(True)
170 self.SetSelBackground(True, "#FFFF00")
171 self.SetUseHorizontalScrollBar(True)
172
173 #
174 # bindings
175 #
176 self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
177 self.Bind(wx.EVT_CHAR, self.OnChar)
178 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyPressed)
179 self.Bind(wx.stc.EVT_STC_AUTOCOMP_SELECTION, self.OnItemSelected)
180 self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemChanged)
181 if sys.platform != 'darwin': # unstable on Mac with wxPython 3
182 self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
183
184 # signal which requests showing of a notification
185 self.showNotification = Signal('GPromptSTC.showNotification')
186
187 # signal to notify selected command
188 self.commandSelected = Signal('GPromptSTC.commandSelected')
189
190 def OnTextSelectionChanged(self, event):
191 """Copy selected text to clipboard and skip event.
192 The same function is in GStc class (goutput.py).
193 """
194 wx.CallAfter(self.Copy)
195 event.Skip()
196
197 def OnItemChanged(self, event):
198 """Change text in statusbar
199 if the item selection in the auto-completion list is changed"""
200 # list of commands
201 if self.toComplete['entity'] == 'command':
202 item = self.toComplete['cmd'].rpartition(
203 '.')[0] + '.' + self.autoCompList[event.GetIndex()]
204 try:
205 nodes = self._menuModel.SearchNodes(key='command', value=item)
206 desc = ''
207 if nodes:
208 self.commandSelected.emit(command=item)
209 desc = nodes[0].data['description']
210 except KeyError:
211 desc = ''
212 self.ShowStatusText(desc)
213 # list of flags
214 elif self.toComplete['entity'] == 'flags':
215 desc = self.cmdDesc.get_flag(
216 self.autoCompList[
217 event.GetIndex()])['description']
218 self.ShowStatusText(desc)
219 # list of parameters
220 elif self.toComplete['entity'] == 'params':
221 item = self.cmdDesc.get_param(self.autoCompList[event.GetIndex()])
222 desc = item['name'] + '=' + item['type']
223 if not item['required']:
224 desc = '[' + desc + ']'
225 desc += ': ' + item['description']
226 self.ShowStatusText(desc)
227 # list of flags and commands
228 elif self.toComplete['entity'] == 'params+flags':
229 if self.autoCompList[event.GetIndex()][0] == '-':
230 desc = self.cmdDesc.get_flag(
231 self.autoCompList[
232 event.GetIndex()].strip('-'))['description']
233 else:
234 item = self.cmdDesc.get_param(
235 self.autoCompList[event.GetIndex()])
236 desc = item['name'] + '=' + item['type']
237 if not item['required']:
238 desc = '[' + desc + ']'
239 desc += ': ' + item['description']
240 self.ShowStatusText(desc)
241 else:
242 self.ShowStatusText('')
243
244 def OnItemSelected(self, event):
245 """Item selected from the list"""
246 lastWord = self.GetWordLeft()
247 # to insert selection correctly if selected word partly matches written
248 # text
249 match = difflib.SequenceMatcher(None, event.GetText(), lastWord)
250 matchTuple = match.find_longest_match(
251 0, len(event.GetText()), 0, len(lastWord))
252
253 compl = event.GetText()[matchTuple[2]:]
254 text = self.GetTextLeft() + compl
255 # add space or '=' at the end
256 end = '='
257 for char in ('.', '-', '='):
258 if text.split(' ')[-1].find(char) >= 0:
259 end = ' '
260
261 compl += end
262 text += end
263
264 self.AddText(compl)
265 pos = len(text)
266 self.SetCurrentPos(pos)
267
268 cmd = text.strip().split(' ')[0]
269
270 if not self.cmdDesc or cmd != self.cmdDesc.get_name():
271 try:
272 self.cmdDesc = gtask.parse_interface(cmd)
273 except IOError:
274 self.cmdDesc = None
275
276 def OnKillFocus(self, event):
277 """Hides autocomplete"""
278 # hide autocomplete
279 if self.AutoCompActive():
280 self.AutoCompCancel()
281 event.Skip()
282
283 def SetTextAndFocus(self, text):
284 pos = len(text)
285 self.commandSelected.emit(command=text)
286 self.SetText(text)
287 self.SetSelectionStart(pos)
288 self.SetCurrentPos(pos)
289 self.SetFocus()
290
291 def UpdateCmdHistory(self, cmd):
292 """Update command history
293
294 :param cmd: command given as a string
295 """
296 # add command to history
297 self.cmdbuffer.append(cmd)
298 # update also traced commands
299 self.commands.append(cmd)
300
301 # keep command history to a managable size
302 if len(self.cmdbuffer) > 200:
303 del self.cmdbuffer[0]
304 self.cmdindex = len(self.cmdbuffer)
305
306 def EntityToComplete(self):
307 """Determines which part of command (flags, parameters) should
308 be completed at current cursor position"""
309 entry = self.GetTextLeft()
310 toComplete = dict(cmd=None, entity=None)
311 try:
312 cmd = entry.split()[0].strip()
313 except IndexError:
314 return toComplete
315
316 try:
317 splitted = utils.split(str(entry))
318 except ValueError: # No closing quotation error
319 return toComplete
320 if len(splitted) > 0 and cmd in globalvar.grassCmd:
321 toComplete['cmd'] = cmd
322 if entry[-1] == ' ':
323 words = entry.split(' ')
324 if any(word.startswith('-') for word in words):
325 toComplete['entity'] = 'params'
326 else:
327 toComplete['entity'] = 'params+flags'
328 else:
329 # get word left from current position
330 word = self.GetWordLeft(withDelimiter=True)
331
332 if word[0] == '=' and word[-1] == '@':
333 toComplete['entity'] = 'mapsets'
334 elif word[0] == '=':
335 # get name of parameter
336 paramName = self.GetWordLeft(
337 withDelimiter=False, ignoredDelimiter='=').strip('=')
338 if paramName:
339 try:
340 param = self.cmdDesc.get_param(paramName)
341 except (ValueError, AttributeError):
342 return toComplete
343 else:
344 return toComplete
345
346 if param['values']:
347 toComplete['entity'] = 'param values'
348 elif param['prompt'] == 'raster' and param['element'] == 'cell':
349 toComplete['entity'] = 'raster map'
350 elif param['prompt'] == 'vector' and param['element'] == 'vector':
351 toComplete['entity'] = 'vector map'
352 elif word[0] == '-':
353 toComplete['entity'] = 'flags'
354 elif word[0] == ' ':
355 toComplete['entity'] = 'params'
356 else:
357 toComplete['entity'] = 'command'
358 toComplete['cmd'] = cmd
359
360 return toComplete
361
362 def GetWordLeft(self, withDelimiter=False, ignoredDelimiter=None):
363 """Get word left from current cursor position. The beginning
364 of the word is given by space or chars: .,-=
365
366 :param withDelimiter: returns the word with the initial delimeter
367 :param ignoredDelimiter: finds the word ignoring certain delimeter
368 """
369 textLeft = self.GetTextLeft()
370
371 parts = list()
372 if ignoredDelimiter is None:
373 ignoredDelimiter = ''
374
375 for char in set(' .,-=') - set(ignoredDelimiter):
376 if not withDelimiter:
377 delimiter = ''
378 else:
379 delimiter = char
380 parts.append(delimiter + textLeft.rpartition(char)[2])
381 return min(parts, key=lambda x: len(x))
382
383 def ShowList(self):
384 """Show sorted auto-completion list if it is not empty"""
385 if len(self.autoCompList) > 0:
386 self.autoCompList.sort()
387 self.AutoCompShow(
388 lenEntered=0, itemList=' '.join(
389 self.autoCompList))
390
391 def OnKeyPressed(self, event):
392 """Key pressed capture special treatment for tabulator to show help"""
393 pos = self.GetCurrentPos()
394 if event.GetKeyCode() == wx.WXK_TAB:
395 # show GRASS command calltips (to hide press 'ESC')
396 entry = self.GetTextLeft()
397 try:
398 cmd = entry.split()[0].strip()
399 except IndexError:
400 cmd = ''
401
402 if cmd not in globalvar.grassCmd:
403 return
404
405 info = gtask.command_info(cmd)
406
407 self.CallTipSetBackground("#f4f4d1")
408 self.CallTipSetForeground("BLACK")
409 self.CallTipShow(pos, info['usage'] + '\n\n' + info['description'])
410 elif event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER) and \
411 not self.AutoCompActive():
412 # run command on line when <return> is pressed
413 self._runCmd(self.GetCurLine()[0].strip())
414 elif event.GetKeyCode() in [wx.WXK_UP, wx.WXK_DOWN] and \
415 not self.AutoCompActive():
416 # Command history using up and down
417 if len(self.cmdbuffer) < 1:
418 return
419
420 self.DocumentEnd()
421
422 # move through command history list index values
423 if event.GetKeyCode() == wx.WXK_UP:
424 self.cmdindex = self.cmdindex - 1
425 if event.GetKeyCode() == wx.WXK_DOWN:
426 self.cmdindex = self.cmdindex + 1
427 if self.cmdindex < 0:
428 self.cmdindex = 0
429 if self.cmdindex > len(self.cmdbuffer) - 1:
430 self.cmdindex = len(self.cmdbuffer) - 1
431
432 try:
433 # without strip causes problem on windows
434 txt = self.cmdbuffer[self.cmdindex].strip()
435 except KeyError:
436 txt = ''
437
438 # clear current line and insert command history
439 self.DelLineLeft()
440 self.DelLineRight()
441 pos = self.GetCurrentPos()
442 self.InsertText(pos, txt)
443 self.LineEnd()
444
445 self.ShowStatusText('')
446 else:
447 event.Skip()
448
449 def OnChar(self, event):
450 """Key char capture for autocompletion, calltips, and command history
451
452 .. todo::
453 event.ControlDown() for manual autocomplete
454 """
455 # keycodes used: "." = 46, "=" = 61, "-" = 45
456 pos = self.GetCurrentPos()
457 # complete command after pressing '.'
458 if event.GetKeyCode() == 46:
459 self.autoCompList = list()
460 entry = self.GetTextLeft()
461 self.InsertText(pos, '.')
462 self.CharRight()
463 self.toComplete = self.EntityToComplete()
464 try:
465 if self.toComplete['entity'] == 'command':
466 for command in globalvar.grassCmd:
467 try:
468 if command.find(self.toComplete['cmd']) == 0:
469 dotNumber = list(
470 self.toComplete['cmd']).count('.')
471 self.autoCompList.append(
472 command.split('.', dotNumber)[-1])
473 except UnicodeDecodeError as e: # TODO: fix it
474 sys.stderr.write(
475 DecodeString(command) + ": " + unicode(e))
476
477 except (KeyError, TypeError):
478 return
479 self.ShowList()
480
481 # complete flags after pressing '-'
482 elif (event.GetKeyCode() == 45) \
483 or event.GetKeyCode() == wx.WXK_NUMPAD_SUBTRACT \
484 or event.GetKeyCode() == wx.WXK_SUBTRACT:
485 self.autoCompList = list()
486 entry = self.GetTextLeft()
487 self.InsertText(pos, '-')
488 self.CharRight()
489 self.toComplete = self.EntityToComplete()
490 if self.toComplete['entity'] == 'flags' and self.cmdDesc:
491 if self.GetTextLeft()[-2:] == ' -': # complete e.g. --quite
492 for flag in self.cmdDesc.get_options()['flags']:
493 if len(flag['name']) == 1:
494 self.autoCompList.append(flag['name'])
495 else:
496 for flag in self.cmdDesc.get_options()['flags']:
497 if len(flag['name']) > 1:
498 self.autoCompList.append(flag['name'])
499 self.ShowList()
500
501 # complete map or values after parameter
502 elif event.GetKeyCode() == 61:
503 self.autoCompList = list()
504 self.InsertText(pos, '=')
505 self.CharRight()
506 self.toComplete = self.EntityToComplete()
507 if self.toComplete['entity'] == 'raster map':
508 self.autoCompList = self.mapList['raster']
509 elif self.toComplete['entity'] == 'vector map':
510 self.autoCompList = self.mapList['vector']
511 elif self.toComplete['entity'] == 'param values':
512 param = self.GetWordLeft(
513 withDelimiter=False,
514 ignoredDelimiter='=').strip(' =')
515 self.autoCompList = self.cmdDesc.get_param(param)['values']
516 self.ShowList()
517
518 # complete mapset ('@')
519 elif event.GetKeyCode() == 64:
520 self.autoCompList = list()
521 self.InsertText(pos, '@')
522 self.CharRight()
523 self.toComplete = self.EntityToComplete()
524
525 if self.toComplete['entity'] == 'mapsets':
526 self.autoCompList = self.mapsetList
527 self.ShowList()
528
529 # complete after pressing CTRL + Space
530 elif event.GetKeyCode() == wx.WXK_SPACE and event.ControlDown():
531 self.autoCompList = list()
532 self.toComplete = self.EntityToComplete()
533
534 # complete command
535 if self.toComplete['entity'] == 'command':
536 for command in globalvar.grassCmd:
537 if command.find(self.toComplete['cmd']) == 0:
538 dotNumber = list(self.toComplete['cmd']).count('.')
539 self.autoCompList.append(
540 command.split('.', dotNumber)[-1])
541
542 # complete flags in such situations (| is cursor):
543 # r.colors -| ...w, q, l
544 # r.colors -w| ...w, q, l
545 elif self.toComplete['entity'] == 'flags' and self.cmdDesc:
546 for flag in self.cmdDesc.get_options()['flags']:
547 if len(flag['name']) == 1:
548 self.autoCompList.append(flag['name'])
549
550 # complete parameters in such situations (| is cursor):
551 # r.colors -w | ...color, map, rast, rules
552 # r.colors col| ...color
553 elif self.toComplete['entity'] == 'params' and self.cmdDesc:
554 for param in self.cmdDesc.get_options()['params']:
555 if param['name'].find(
556 self.GetWordLeft(withDelimiter=False)) == 0:
557 self.autoCompList.append(param['name'])
558
559 # complete flags or parameters in such situations (| is cursor):
560 # r.colors | ...-w, -q, -l, color, map, rast, rules
561 # r.colors color=grey | ...-w, -q, -l, color, map, rast, rules
562 elif self.toComplete['entity'] == 'params+flags' and self.cmdDesc:
563 self.autoCompList = list()
564
565 for param in self.cmdDesc.get_options()['params']:
566 self.autoCompList.append(param['name'])
567 for flag in self.cmdDesc.get_options()['flags']:
568 if len(flag['name']) == 1:
569 self.autoCompList.append('-' + flag['name'])
570 else:
571 self.autoCompList.append('--' + flag['name'])
572
573 self.ShowList()
574
575 # complete map or values after parameter
576 # r.buffer input=| ...list of raster maps
577 # r.buffer units=| ... feet, kilometers, ...
578 elif self.toComplete['entity'] == 'raster map':
579 self.autoCompList = list()
580 self.autoCompList = self.mapList['raster']
581 elif self.toComplete['entity'] == 'vector map':
582 self.autoCompList = list()
583 self.autoCompList = self.mapList['vector']
584 elif self.toComplete['entity'] == 'param values':
585 self.autoCompList = list()
586 param = self.GetWordLeft(
587 withDelimiter=False,
588 ignoredDelimiter='=').strip(' =')
589 self.autoCompList = self.cmdDesc.get_param(param)['values']
590
591 self.ShowList()
592
593 elif event.GetKeyCode() == wx.WXK_SPACE:
594 items = self.GetTextLeft().split()
595 if len(items) == 1:
596 cmd = items[0].strip()
597 if cmd in globalvar.grassCmd and \
598 (not self.cmdDesc or cmd != self.cmdDesc.get_name()):
599 try:
600 self.cmdDesc = gtask.parse_interface(cmd)
601 except IOError:
602 self.cmdDesc = None
603 event.Skip()
604
605 else:
606 event.Skip()
607
608 def ShowStatusText(self, text):
609 """Requests showing of notification, e.g. showing in a statusbar."""
610 self.showNotification.emit(message=text)
611
612 def GetTextLeft(self):
613 """Returns all text left of the caret"""
614 pos = self.GetCurrentPos()
615 self.HomeExtend()
616 entry = self.GetSelectedText()
617 self.SetCurrentPos(pos)
618
619 return entry
620
621 def OnDestroy(self, event):
622 """The clipboard contents can be preserved after
623 the app has exited"""
624 wx.TheClipboard.Flush()
625 event.Skip()
626
627 def OnCmdErase(self, event):
628 """Erase command prompt"""
629 self.Home()
630 self.DelLineRight()
Note: See TracBrowser for help on using the repository browser.