Changeset 4297

Show
Ignore:
Timestamp:
07/19/08 12:17:06 (3 months ago)
Author:
ed
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/dabo/ui/uiwx/dShell.py

    r4070 r4297  
    11# -*- coding: utf-8 -*- 
    22import __builtin__ 
     3import time 
    34import wx 
    45import wx.stc as stc 
     
    1314 
    1415dabo.ui.loadUI("wx") 
     16from dabo.ui import dKeys 
     17 
     18 
     19class _LookupPanel(dabo.ui.dPanel): 
     20    """Used for the command history search""" 
     21    def afterInit(self): 
     22        self._history = None 
     23        self._displayedHistory = None 
     24        self.currentSearch = "" 
     25        self.needRefilter = False 
     26        self.lblSearch = dabo.ui.dLabel(self) 
     27        self.lstMatch = dabo.ui.dListBox(self, ValueMode="string", Choices=[], 
     28                OnMouseLeftDoubleClick=self.selectCmd, OnKeyChar=self.onListKey) 
     29        self.Sizer = dabo.ui.dSizer("v", DefaultBorder=4) 
     30        self.Sizer.append(self.lblSearch, halign="center") 
     31        self.Sizer.append(self.lstMatch, "x", 1) 
     32        self.Width = 400 
     33        self.layout() 
     34 
     35 
     36    def clear(self): 
     37        """Reset to original state.""" 
     38        self.ok = False 
     39        self.currentSearch = self.lblSearch.Caption = "" 
     40        self.refilter() 
     41 
     42 
     43    def onListKey(self, evt): 
     44        """Process keypresses in the command list control""" 
     45        kc = evt.keyCode 
     46        char = evt.keyChar 
     47        if kc in (dKeys.key_Return, dKeys.key_Numpad_enter): 
     48            self.closeDialog(True) 
     49            return 
     50        elif kc == dKeys.key_Escape: 
     51            self.closeDialog(False) 
     52        if kc in dKeys.arrows or char is None: 
     53            #ignore 
     54            return 
     55        if kc == dKeys.key_Back: 
     56            self.currentSearch = self.currentSearch[:-1] 
     57        else: 
     58            self.currentSearch += char 
     59        self.lblSearch.Caption = self.currentSearch 
     60        self.layout() 
     61        self.needRefilter = True 
     62        evt.stop()   
     63 
     64 
     65    def closeDialog(self, ok): 
     66        """Hide the dialog, and set the ok/cancel flag""" 
     67        self.ok = ok 
     68        self.Form.hide() 
     69 
     70 
     71    def getCmd(self): 
     72        return self.lstMatch.Value 
     73 
     74 
     75    def selectCmd(self, evt): 
     76        self.closeDialog(True) 
     77 
     78 
     79    def onIdle(self, evt): 
     80        """For performance, don't filter on every keypress. Wait until idle.""" 
     81        if self.needRefilter: 
     82            self.needRefilter = False 
     83            self.refilter() 
     84 
     85 
     86    def refilter(self): 
     87        """Display only those commands that contain the search string""" 
     88        self.DisplayedHistory = self.History.filter("cmd", self.currentSearch, "contains") 
     89        sel = self.lstMatch.Value 
     90        self.lstMatch.Choices = [rec["cmd"] for rec in self.DisplayedHistory] 
     91        if sel: 
     92            try: 
     93                self.lstMatch.Value = sel 
     94            except ValueError: 
     95                self._selectFirst() 
     96        else: 
     97            self._selectFirst() 
     98 
     99 
     100    def _selectFirst(self): 
     101        """Select the first item in the list, if available.""" 
     102        if len(self.lstMatch.Choices): 
     103            self.lstMatch.PositionValue = 0 
     104 
     105 
     106    def _getHistory(self): 
     107        if self._history is None: 
     108            self._history = dabo.db.dDataSet() 
     109        return self._history 
     110 
     111    def _setHistory(self, val): 
     112        if self._constructed(): 
     113            self._history = self._displayedHistory = val 
     114            try: 
     115                self.lstMatch.Choices = [rec["cmd"] for rec in self.DisplayedHistory] 
     116                self._selectFirst() 
     117            except AttributeError: 
     118                pass 
     119        else: 
     120            self._properties["History"] = val 
     121 
     122 
     123    def _getDisplayedHistory(self): 
     124        if self._displayedHistory is None: 
     125            self._displayedHistory = self.History 
     126        return self._displayedHistory 
     127 
     128    def _setDisplayedHistory(self, val): 
     129        if self._constructed(): 
     130            self._displayedHistory = val 
     131        else: 
     132            self._properties["DisplayedHistory"] = val 
     133 
     134 
     135    DisplayedHistory = property(_getDisplayedHistory, _setDisplayedHistory, None, 
     136            _("Filtered copy of the History  (dDataSet)")) 
     137 
     138    History = property(_getHistory, _setHistory, None, 
     139            _("Dataset containing the command history  (dDataSet)")) 
     140 
    15141 
    16142 
     
    41167 
    42168 
    43 #     def processLine(self): 
    44 #      """This is a workaroundfor the fact that otherwise, the global _() function  
    45 #       will get replaced by the Python interpreter's default behavior of stuffing  
    46 #       the results of the last evaluation into the __builtin__ module's '_' attribute. 
    47 #       This results in all subsequent localization attempts failing, so after every line 
    48 #       that executes we re-assign the value to the one held in dabo.dLocalize. 
    49 #       """ 
    50 #       super(_Shell, self).processLine() 
    51 #       __builtin__._ = dabo.dLocalize._translationFunction 
    52          
    53  
     169  def processLine(self): 
     170       """This is part of the underlying class. We need to add the command that  
     171        gets processed into our internal stack. 
     172        """ 
     173        edt = self.CanEdit() 
     174        super(_Shell, self).processLine() 
     175        if edt: 
     176            # push the latest command into the stack 
     177            self.Form.addToHistory(self.history[0]) 
     178         
     179         
    54180    def setDefaultFont(self, fontFace, fontSize): 
    55181        # Global default styles for all languages 
     
    151277class dShell(dSplitForm): 
    152278    def _onDestroy(self, evt): 
     279        self._clearOldHistory() 
    153280        __builtin__.raw_input = self._oldRawInput 
    154281 
     
    162289    def _afterInit(self): 
    163290        super(dShell, self)._afterInit() 
     291        self.cmdHistKey = self.PreferenceManager.command_history 
     292        self._historyPanel = None 
     293        self._lastCmd = None 
    164294 
    165295        # PyShell sets the raw_input function to a function of PyShell, 
     
    199329        cp.Sizer.append1x(self.shell) 
    200330        self.shell.Bind(wx.EVT_RIGHT_UP, self.shellRight) 
     331        self.shell.bindEvent(dEvents.KeyDown, self.onShellKeyDown) 
     332         
     333        # Restore the history 
     334        self.restoreHistory() 
    201335 
    202336        # create the output control 
     
    237371 
    238372 
     373    def addToHistory(self, cmd): 
     374        if cmd == self._lastCmd: 
     375            # Don't add again 
     376            return 
     377        self._lastCmd = cmd 
     378        stamp = "%s" % int(round(time.time() * 100, 0)) 
     379        self.cmdHistKey.setValue(stamp, cmd) 
     380 
     381 
     382    def onShellKeyDown(self, evt): 
     383        if evt.controlDown and evt.keyChar in ("r", "R"): 
     384            if not (evt.commandDown or evt.altDown or evt.metaDown): 
     385                evt.stop() 
     386                self.historyPop() 
     387 
     388 
     389    def _loadHistory(self): 
     390        ck = self.cmdHistKey 
     391        cmds = [] 
     392        for k in ck.getPrefKeys(): 
     393            cmds.append({"stamp": k, "cmd": ck.get(k)}) 
     394        dsu = dabo.db.dDataSet(cmds) 
     395        ds = dsu.sort("stamp", "desc") 
     396        return ds 
     397 
     398 
     399    def historyPop(self): 
     400        """Let the user type in part of a command, and retrieve the matching commands 
     401        from their history. 
     402        """ 
     403        ds = self._loadHistory() 
     404        hp = self._HistoryPanel 
     405        hp.History = ds 
     406        fp = self.FloatingPanel 
     407        # We want it centered, so set Owner to None 
     408        fp.Owner = None 
     409        hp.clear() 
     410        fp.show() 
     411        if hp.ok: 
     412            cmd = hp.getCmd() 
     413            if cmd: 
     414                pos = self.shell.history.index(cmd) 
     415                self.shell.replaceFromHistory(pos - self.shell.historyIndex) 
     416 
     417 
     418    def restoreHistory(self): 
     419        """Get the stored history from previous sessions, and set the shell's 
     420        internal command history list to it. 
     421        """ 
     422        ds = self._loadHistory() 
     423        self.shell.history = [rec["cmd"] for rec in ds] 
     424 
     425 
     426    def _clearOldHistory(self): 
     427        """For performance reasons, only save up to 500 commands.""" 
     428        numToSave = 500 
     429        ck = self.cmdHistKey 
     430        ds = self._loadHistory() 
     431        if len(ds) <= numToSave: 
     432            return 
     433        cutoff = ds[numToSave]["stamp"] 
     434        bad = [] 
     435        for rec in ds: 
     436            if rec["stamp"] <= cutoff: 
     437                bad.append(rec["stamp"]) 
     438        for bs in bad: 
     439            ck.deletePref(bs) 
     440 
     441         
    239442    def outputRightDown(self, evt): 
    240443        pop = dabo.ui.dMenu() 
     
    306509 
    307510 
     511    def _getFontSize(self): 
     512        return self.shell.FontSize 
     513 
     514    def _setFontSize(self, val): 
     515        if self._constructed(): 
     516            self.shell.FontSize = val 
     517        else: 
     518            self._properties["FontSize"] = val 
     519 
     520 
     521    def _getFontFace(self): 
     522        return self.shell.FontFace 
     523 
     524    def _setFontFace(self, val): 
     525        if self._constructed(): 
     526            self.shell.FontFace = val 
     527        else: 
     528            self._properties["FontFace"] = val 
     529 
     530 
     531    def _getHistoryPanel(self): 
     532        fp = self.FloatingPanel 
     533        try: 
     534            create = self._historyPanel is None 
     535        except AttributeError: 
     536            create = True 
     537        if create: 
     538            fp.clear() 
     539            pnl = self._historyPanel = _LookupPanel(fp) 
     540            pnl.Height = max(200, self.Height-100) 
     541            fp.Sizer.append(pnl) 
     542            fp.fitToSizer() 
     543        return self._historyPanel 
     544 
     545 
    308546    def _getSplitState(self): 
    309547        return self._splitState 
     
    322560             
    323561 
    324     def _getFontSize(self): 
    325         return self.shell.FontSize 
    326  
    327     def _setFontSize(self, val): 
    328         if self._constructed(): 
    329             self.shell.FontSize = val 
    330         else: 
    331             self._properties["FontSize"] = val 
    332  
    333  
    334     def _getFontFace(self): 
    335         return self.shell.FontFace 
    336  
    337     def _setFontFace(self, val): 
    338         if self._constructed(): 
    339             self.shell.FontFace = val 
    340         else: 
    341             self._properties["FontFace"] = val 
    342  
    343  
    344562    FontFace = property(_getFontFace, _setFontFace, None, 
    345563            _("Name of the font face used in the shell  (str)")) 
     
    347565    FontSize = property(_getFontSize, _setFontSize, None, 
    348566            _("Size of the font used in the shell  (int)")) 
     567 
     568    _HistoryPanel = property(_getHistoryPanel, None, None, 
     569            _("Popup to display the command history  (read-only) (dDialog)")) 
    349570 
    350571    SplitState = property(_getSplitState, _setSplitState, None,