# -*- coding: utf-8 -*-

# Copyright (c) 2002, 2003 Detlev Offenbach <detlev@die-offenbachs.de>
# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>
#

"""
Module implementing a graphical Python shell.
"""

import sys

from qt import *

import Preferences

class Shell(QTextEdit):
    """
    Module implementing a graphical Python shell.
    
    A user can enter commands that are executed in the remote 
    Python interpreter.  
    """
    def __init__(self,dbs,parent=None):
        """
        Constructor
        
        Arguments
        
            dbs -- rference to the debug server object
            
            parent -- parent widget (QWidget)
        """
        QTextEdit.__init__(self,parent)

        self.setTextFormat(Qt.PlainText)
        self.setWordWrap(QTextEdit.WidgetWidth)
        self.setWrapPolicy(QTextEdit.Anywhere)
        self.passive = Preferences.getDebugger("PassiveDbgEnabled")
        if self.passive:
            self.setCaption(self.trUtf8('Shell - Passive'))
        else:
            self.setCaption(self.trUtf8('Shell'))
        font = Preferences.getShell("Font")
        self.setFont(font)

        QWhatsThis.add(self,self.trUtf8(
"""<b>The Shell Window</b>"""
"""<p>This is simply the Python interpreter running in a window. The"""
""" interpreter is the one that is used to run the program being debugged."""
""" This means that you can execute any Python command while the program"""
""" being debugged is running.</p>"""
"""<p>You can use the cursor keys while entering commands. There is also a"""
""" history of commands that can be recalled using the up and down cursor"""
""" keys.</p>"""
"""<p>The shell has two special commands. 'reset' kills the Python shell"""
""" and starts a new one. 'clear' clears the display of the shell window."""
""" These commands are available through the context menu as well.</p>"""
"""<p>Pressing the Tab key after some text has been entered will show"""
""" a list of possible commandline completions. The relevant entry may"""
""" be selected from this list. If only one entry is available, this will"""
""" inserted automatically.</p>"""
"""<p>In passive debugging mode the shell is only available after the"""
""" program to be debugged has connected to the IDE until it has finished."""
""" This is indicated by a different prompt and by an indication in the"""
""" window caption.</p>"""
                      ))

        self.connect(dbs,PYSIGNAL('clientStatement'),self.handleClientStatement)
        self.connect(dbs,PYSIGNAL('clientOutput'),self.write)
        self.connect(dbs,PYSIGNAL('clientGone'),self.initialise)
        self.connect(dbs,PYSIGNAL('clientRawInput'),self.raw_input)
        self.connect(dbs,PYSIGNAL('clientBanner'),self.writeBanner)
        self.connect(dbs,PYSIGNAL('clientCompletionList'),self.showCompletions)
        self.dbs = dbs

        # Initialise instance variables.
        self.initialise()
        self.history = []
        self.histidx = -1

        # Make sure we have prompts.
        if self.passive:
            sys.ps1 = self.trUtf8("Passive >>> ")
        else:
            try:
                sys.ps1
            except AttributeError:
                sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "

        # Display the banner.
        self.getBanner()
        
        # Create a little context menu
        self.menu = QPopupMenu(self)
        self.menu.insertItem(self.trUtf8('Copy'), self.copy)
        self.menu.insertItem(self.trUtf8('Paste'), self.paste)
        self.menu.insertSeparator()
        self.menu.insertItem(self.trUtf8('Clear'), self.handleClear)
        self.menu.insertItem(self.trUtf8('Reset'), self.handleReset)

    def initialise(self):
        """
        Private method to get ready for a new remote interpreter.
        """
        self.buf = QString()
        self.point = 0
        self.prline = 0
        self.prcol = 0
        self.inContinue = 0
        self.inRawMode = 0

    def getBanner(self):
        """
        Private method to get the banner for the remote interpreter.
        
        It requests the Python version and platform running on the
        debug client side.
        """
        if self.passive:
            self.writeBanner('','','')
        else:
            self.dbs.remoteBanner()
            
    def writeBanner(self, version, platform, dbgclient):
        """
        Private method to write a banner with info from the debug client.
        
        Arguments
        
            version -- Python version string (string)
            
            platform -- platform of the remote interpreter (string)
            
            dbgclient -- debug client type used (string)
        """
        if self.passive:
            self.write('Passive Debug Mode')
        else:
            self.write(self.trUtf8('Python %1 on %2, %3\n')
                        .arg(version)
                        .arg(platform)
                        .arg(dbgclient))

        self.write(sys.ps1)
        
    def handleClientStatement(self,more):
        """
        Private method to handle the response from the debugger client.
        
        Arguments
        
            more -- flag indicating that more user input is required
        """
        self.inContinue = more

        if more:
            prompt = sys.ps2
        else:
            prompt = sys.ps1

        self.write(prompt)

    def getEndPos(self):
        """
        Private method to return the line and column of the last character.
        
        Returns
        
            tuple of two values (int, int) giving the line and column
        """
        line = self.paragraphs() - 1
        return (line, self.paragraphLength(line))

    def write(self,s):
        """
        Private method to display some text.
        
        Arguments
        
            s -- text to be displayed (string or QString)
        """
        line, col = self.getEndPos()
        self.setCursorPosition(line, col)
        self.insert(s)
        self.prline, self.prcol = self.getCursorPosition()

    def raw_input(self,s):
        """
        Private method to handle raw input.
        
        Arguments
        
            s -- prompt to be displayed (string or QString)
        """
        self.setFocus()
        self.inRawMode = 1
        self.write(s)
        
    def paste(self):
        """
        Reimplemented slot to handle the paste action.
        """
        s = qApp.clipboard().text()
        if not s.isNull():
            self.insertText(s)
        
    def insertText(self,s):
        """
        Private method to insert some text at the current cursor position.
        
        Arguments
        
            s -- text to be inserted (string or QString)
        """
        line, col = self.getCursorPosition()
        self.insertAt(s,line,col)
        self.buf.insert(self.point,s)
        self.point = self.point + len(str(s))
        self.setCursorPosition(line, col + len(str(s)))
        
    def keyPressEvent(self,ev):
        """
        Re-implemented to handle the user input a key at a time.
        
        Arguments
        
            ev -- key event (QKeyPressEvent)
        """
        txt = ev.text()
        key = ev.key()
        asc = ev.ascii()

        # See it is text to insert.
        if txt.length() and \
           key not in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Delete, Qt.Key_Backspace) and \
           (asc == 0 or asc >= 32):
            self.insertText(txt)
            return

        if ev.state() & Qt.ControlButton or ev.state() & Qt.ShiftButton:
            if key == Qt.Key_C:
                self.copy()
            elif key == Qt.Key_V:
                self.paste()
            else:
                ev.ignore()
            return

        if key == Qt.Key_Backspace:
            if self.point:
                self.doKeyboardAction(QTextEdit.ActionBackspace)
                self.point = self.point - 1
                self.buf.remove(self.point,1)
        elif key == Qt.Key_Delete:
            self.doKeyboardAction(QTextEdit.ActionDelete)
            self.buf.remove(self.point,1)
        elif key == Qt.Key_Enter or key == Qt.Key_Return:
            line, col = self.getEndPos()
            self.setCursorPosition(line,col)
            self.insert('\n')

            if not self.inRawMode:
                if self.buf.isNull():
                    cmd = ''
                else:
                    self.history.append(QString(self.buf))
                    self.histidx = -1
                    cmd = str(self.buf)
                if cmd == 'reset':
                    self.dbs.startClient(0)
                    self.clear()
                    # Display the banner.
                    self.getBanner()
                    if not self.passive:
                        self.buf.truncate(0)
                        self.point = 0
                        return
                    else:
                        cmd = ''
                elif cmd == 'clear':
                    self.clear()
                    # Display the banner.
                    self.getBanner()
                    if not self.passive:
                        self.buf.truncate(0)
                        self.point = 0
                        return
                    else:
                        cmd = ''

                self.dbs.remoteStatement(cmd)
            else:
                self.inRawMode = 0
                if self.buf.isNull():
                    cmd = ''
                else:
                    cmd = str(self.buf)
                self.dbs.remoteRawInput(cmd)

            self.buf.truncate(0)
            self.point = 0
        elif key == Qt.Key_Tab:
            if self.inContinue and self.buf.stripWhiteSpace().isEmpty():
                self.insertText(txt)
            else:
                self.dbs.remoteCompletion(self.buf)
        elif key == Qt.Key_Left:
            if self.point:
                self.moveCursor(QTextEdit.MoveBackward,0)
                self.point = self.point - 1
        elif key == Qt.Key_Right:
            if self.point < self.buf.length():
                self.moveCursor(QTextEdit.MoveForward,0)
                self.point = self.point + 1
        elif key == Qt.Key_Home:
            self.setCursorPosition(self.prline,self.prcol)
            self.point = 0
        elif key == Qt.Key_End:
            self.moveCursor(QTextEdit.MoveLineEnd,0)
            self.point = self.buf.length()
        elif key == Qt.Key_Up:
            if self.histidx < 0:
                self.histidx = len(self.history)

            if self.histidx > 0:
                self.histidx = self.histidx - 1
                self.useHistory()

        elif key == Qt.Key_Down:
            if self.histidx >= 0 and self.histidx < len(self.history):
                self.histidx = self.histidx + 1
                self.useHistory()
        else:
            ev.ignore()

    def useHistory(self):
        """
        Private method to display a command from the history.
        """
        if self.histidx < len(self.history):
            cmd = self.history[self.histidx]
        else:
            cmd = QString()

        self.setCursorPosition(self.prline,self.prcol)
        self.setSelection(self.prline,self.prcol,\
                          self.prline,self.paragraphLength(self.prline))
        self.removeSelectedText()
        self.buf.truncate(0)
        self.point = 0
        self.insertText(cmd)

    def focusNextPrevChild(self,next):
        """
        Reimplemented to stop Tab moving to the next window.
        
        While the user is entering a multi-line command, the movement to
        the next window by the Tab key being pressed is suppressed.
        
        Arguments
        
            next -- next window
            
        Returns
        
            flag indicating the movement
        """
        if next and self.inContinue:
            return 0

        return QTextEdit.focusNextPrevChild(self,next)

    def contentsContextMenuEvent(self,ev):
        """
        Reimplemented to show our own context menu.
        
        Arguments
        
            ev -- context menu event (QContextMenuEvent)
        """
        self.menu.popup(ev.globalPos())
        ev.accept()
        
    def handleClear(self):
        """
        Private slot to handle the 'clear' context menu entry.
        """
        self.clear()
        # Display the banner.
        self.getBanner()
        
    def handleReset(self):
        """
        Private slot to handle the 'reset' context menu entry.
        """
        self.dbs.startClient(0)
        self.handleClear()
        
    def handlePreferencesChanged(self):
        """
        Public slot to handle the preferencesChanged signal.
        """
        self.setFont(Preferences.getShell("Font"))
        
    def showCompletions(self, completions, text):
        """
        Private method to display the possible completions.
        """
        if len(completions) == 0:
            return
            
        if len(completions) > 1:
            if len(completions) > 100:
                res = QMessageBox.information(None,
                    self.trUtf8("Shell Completion"),
                    self.trUtf8("""Display all %1 possibilities?""")
                        .arg(len(completions)),
                    self.trUtf8("&Yes"),
                    self.trUtf8("&No"),
                    None,
                    0, -1)
                if res == 1:
                    return
                    
            completions.sort()
            comps = QStringList()
            for comp in completions:
                comps.append(comp)
            txt, ok = QInputDialog.getItem(\
                self.trUtf8("Shell Completion"),
                self.trUtf8("Please select:"),
                comps,
                0, 0)
            if not ok:
                return
            txt = str(txt)
        else:
            txt = completions[0]
        if text != "":
            txt = txt.replace(text, "")
        self.insertText(txt)
