dabo Commit
Revision 4297
Date: 2008-07-19 12:17:06 -0700 (Sat, 19 Jul 2008)
Author: Ed
Trac: http://svn.dabodev.com/trac/dabo/changeset/4297
Changed:
U trunk/dabo/ui/uiwx/dShell.py
Log:
Added features similar to the bash history and reverse-i-search to the shell.
Pressing Ctrl-R will bring up a dialog containing your previous commands; you
can type to filter only those containing your search string. You can then press
Enter or double-click on the command, and it will be entered into the shell.
Diff:
Modified: trunk/dabo/ui/uiwx/dShell.py
===================================================================
--- trunk/dabo/ui/uiwx/dShell.py 2008-07-19 17:02:07 UTC (rev 4296)
+++ trunk/dabo/ui/uiwx/dShell.py 2008-07-19 19:17:06 UTC (rev 4297)
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import __builtin__
+import time
import wx
import wx.stc as stc
import wx.py
@@ -12,8 +13,133 @@
from dPemMixin import dPemMixin
dabo.ui.loadUI("wx")
+from dabo.ui import dKeys
+class _LookupPanel(dabo.ui.dPanel):
+ """Used for the command history search"""
+ def afterInit(self):
+ self._history = None
+ self._displayedHistory = None
+ self.currentSearch = ""
+ self.needRefilter = False
+ self.lblSearch = dabo.ui.dLabel(self)
+ self.lstMatch = dabo.ui.dListBox(self, ValueMode="string",
Choices=[],
+ OnMouseLeftDoubleClick=self.selectCmd,
OnKeyChar=self.onListKey)
+ self.Sizer = dabo.ui.dSizer("v", DefaultBorder=4)
+ self.Sizer.append(self.lblSearch, halign="center")
+ self.Sizer.append(self.lstMatch, "x", 1)
+ self.Width = 400
+ self.layout()
+
+
+ def clear(self):
+ """Reset to original state."""
+ self.ok = False
+ self.currentSearch = self.lblSearch.Caption = ""
+ self.refilter()
+
+
+ def onListKey(self, evt):
+ """Process keypresses in the command list control"""
+ kc = evt.keyCode
+ char = evt.keyChar
+ if kc in (dKeys.key_Return, dKeys.key_Numpad_enter):
+ self.closeDialog(True)
+ return
+ elif kc == dKeys.key_Escape:
+ self.closeDialog(False)
+ if kc in dKeys.arrows or char is None:
+ #ignore
+ return
+ if kc == dKeys.key_Back:
+ self.currentSearch = self.currentSearch[:-1]
+ else:
+ self.currentSearch += char
+ self.lblSearch.Caption = self.currentSearch
+ self.layout()
+ self.needRefilter = True
+ evt.stop()
+
+
+ def closeDialog(self, ok):
+ """Hide the dialog, and set the ok/cancel flag"""
+ self.ok = ok
+ self.Form.hide()
+
+
+ def getCmd(self):
+ return self.lstMatch.Value
+
+
+ def selectCmd(self, evt):
+ self.closeDialog(True)
+
+
+ def onIdle(self, evt):
+ """For performance, don't filter on every keypress. Wait until
idle."""
+ if self.needRefilter:
+ self.needRefilter = False
+ self.refilter()
+
+
+ def refilter(self):
+ """Display only those commands that contain the search string"""
+ self.DisplayedHistory = self.History.filter("cmd",
self.currentSearch, "contains")
+ sel = self.lstMatch.Value
+ self.lstMatch.Choices = [rec["cmd"] for rec in
self.DisplayedHistory]
+ if sel:
+ try:
+ self.lstMatch.Value = sel
+ except ValueError:
+ self._selectFirst()
+ else:
+ self._selectFirst()
+
+
+ def _selectFirst(self):
+ """Select the first item in the list, if available."""
+ if len(self.lstMatch.Choices):
+ self.lstMatch.PositionValue = 0
+
+
+ def _getHistory(self):
+ if self._history is None:
+ self._history = dabo.db.dDataSet()
+ return self._history
+
+ def _setHistory(self, val):
+ if self._constructed():
+ self._history = self._displayedHistory = val
+ try:
+ self.lstMatch.Choices = [rec["cmd"] for rec in
self.DisplayedHistory]
+ self._selectFirst()
+ except AttributeError:
+ pass
+ else:
+ self._properties["History"] = val
+
+
+ def _getDisplayedHistory(self):
+ if self._displayedHistory is None:
+ self._displayedHistory = self.History
+ return self._displayedHistory
+
+ def _setDisplayedHistory(self, val):
+ if self._constructed():
+ self._displayedHistory = val
+ else:
+ self._properties["DisplayedHistory"] = val
+
+
+ DisplayedHistory = property(_getDisplayedHistory, _setDisplayedHistory,
None,
+ _("Filtered copy of the History (dDataSet)"))
+
+ History = property(_getHistory, _setHistory, None,
+ _("Dataset containing the command history (dDataSet)"))
+
+
+
class _Shell(dPemMixin, wx.py.shell.Shell):
def __init__(self, parent, properties=None, attProperties=None,
*args, **kwargs):
@@ -40,17 +166,17 @@
self.FontSize = 10
-# def processLine(self):
-# """This is a workaroundfor the fact that otherwise, the global
_() function
-# will get replaced by the Python interpreter's default behavior
of stuffing
-# the results of the last evaluation into the __builtin__
module's '_' attribute.
-# This results in all subsequent localization attempts failing,
so after every line
-# that executes we re-assign the value to the one held in
dabo.dLocalize.
-# """
-# super(_Shell, self).processLine()
-# __builtin__._ = dabo.dLocalize._translationFunction
+ def processLine(self):
+ """This is part of the underlying class. We need to add the
command that
+ gets processed into our internal stack.
+ """
+ edt = self.CanEdit()
+ super(_Shell, self).processLine()
+ if edt:
+ # push the latest command into the stack
+ self.Form.addToHistory(self.history[0])
-
+
def setDefaultFont(self, fontFace, fontSize):
# Global default styles for all languages
self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%s,size:%d" %
(fontFace, fontSize))
@@ -150,6 +276,7 @@
class dShell(dSplitForm):
def _onDestroy(self, evt):
+ self._clearOldHistory()
__builtin__.raw_input = self._oldRawInput
@@ -161,6 +288,9 @@
def _afterInit(self):
super(dShell, self)._afterInit()
+ self.cmdHistKey = self.PreferenceManager.command_history
+ self._historyPanel = None
+ self._lastCmd = None
# PyShell sets the raw_input function to a function of PyShell,
# but doesn't set it back on destroy, resulting in errors later
@@ -198,6 +328,10 @@
cp.Sizer.append1x(self.shell)
self.shell.Bind(wx.EVT_RIGHT_UP, self.shellRight)
+ self.shell.bindEvent(dEvents.KeyDown, self.onShellKeyDown)
+
+ # Restore the history
+ self.restoreHistory()
# create the output control
outControl = dabo.ui.dEditBox(op, RegID="edtOut",
@@ -236,6 +370,75 @@
dabo.ui.callAfter(ed.SetSelection, endpos, endpos)
+ def addToHistory(self, cmd):
+ if cmd == self._lastCmd:
+ # Don't add again
+ return
+ self._lastCmd = cmd
+ stamp = "%s" % int(round(time.time() * 100, 0))
+ self.cmdHistKey.setValue(stamp, cmd)
+
+
+ def onShellKeyDown(self, evt):
+ if evt.controlDown and evt.keyChar in ("r", "R"):
+ if not (evt.commandDown or evt.altDown or evt.metaDown):
+ evt.stop()
+ self.historyPop()
+
+
+ def _loadHistory(self):
+ ck = self.cmdHistKey
+ cmds = []
+ for k in ck.getPrefKeys():
+ cmds.append({"stamp": k, "cmd": ck.get(k)})
+ dsu = dabo.db.dDataSet(cmds)
+ ds = dsu.sort("stamp", "desc")
+ return ds
+
+
+ def historyPop(self):
+ """Let the user type in part of a command, and retrieve the
matching commands
+ from their history.
+ """
+ ds = self._loadHistory()
+ hp = self._HistoryPanel
+ hp.History = ds
+ fp = self.FloatingPanel
+ # We want it centered, so set Owner to None
+ fp.Owner = None
+ hp.clear()
+ fp.show()
+ if hp.ok:
+ cmd = hp.getCmd()
+ if cmd:
+ pos = self.shell.history.index(cmd)
+ self.shell.replaceFromHistory(pos -
self.shell.historyIndex)
+
+
+ def restoreHistory(self):
+ """Get the stored history from previous sessions, and set the
shell's
+ internal command history list to it.
+ """
+ ds = self._loadHistory()
+ self.shell.history = [rec["cmd"] for rec in ds]
+
+
+ def _clearOldHistory(self):
+ """For performance reasons, only save up to 500 commands."""
+ numToSave = 500
+ ck = self.cmdHistKey
+ ds = self._loadHistory()
+ if len(ds) <= numToSave:
+ return
+ cutoff = ds[numToSave]["stamp"]
+ bad = []
+ for rec in ds:
+ if rec["stamp"] <= cutoff:
+ bad.append(rec["stamp"])
+ for bs in bad:
+ ck.deletePref(bs)
+
+
def outputRightDown(self, evt):
pop = dabo.ui.dMenu()
pop.append(_("Clear"), OnHit=self.onClearOutput)
@@ -305,22 +508,6 @@
self.shell.SetZoom(self.shell.GetZoom()-1)
- def _getSplitState(self):
- return self._splitState
-
- def _setSplitState(self, val):
- if self._splitState != val:
- self._splitState = val
- if val:
- self.split()
- self.shell.interp.stdout = self._pseudoOut
- self.shell.interp.stderr = self._pseudoErr
- else:
- self.unsplit()
- self.shell.interp.stdout = self._stdOut
- self.shell.interp.stderr = self._stdErr
-
-
def _getFontSize(self):
return self.shell.FontSize
@@ -341,12 +528,46 @@
self._properties["FontFace"] = val
+ def _getHistoryPanel(self):
+ fp = self.FloatingPanel
+ try:
+ create = self._historyPanel is None
+ except AttributeError:
+ create = True
+ if create:
+ fp.clear()
+ pnl = self._historyPanel = _LookupPanel(fp)
+ pnl.Height = max(200, self.Height-100)
+ fp.Sizer.append(pnl)
+ fp.fitToSizer()
+ return self._historyPanel
+
+
+ def _getSplitState(self):
+ return self._splitState
+
+ def _setSplitState(self, val):
+ if self._splitState != val:
+ self._splitState = val
+ if val:
+ self.split()
+ self.shell.interp.stdout = self._pseudoOut
+ self.shell.interp.stderr = self._pseudoErr
+ else:
+ self.unsplit()
+ self.shell.interp.stdout = self._stdOut
+ self.shell.interp.stderr = self._stdErr
+
+
FontFace = property(_getFontFace, _setFontFace, None,
_("Name of the font face used in the shell (str)"))
FontSize = property(_getFontSize, _setFontSize, None,
_("Size of the font used in the shell (int)"))
+ _HistoryPanel = property(_getHistoryPanel, None, None,
+ _("Popup to display the command history (read-only)
(dDialog)"))
+
SplitState = property(_getSplitState, _setSplitState, None,
_("""Controls whether the output is in a separate pane
(default)
or intermixed with the commands. (bool)"""))
_______________________________________________
Post Messages to: [email protected]
Subscription Maintenance: http://leafe.com/mailman/listinfo/dabo-dev
Searchable Archives: http://leafe.com/archives/search/dabo-dev
This message: http://leafe.com/archives/byMID/[EMAIL PROTECTED]