# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>


from qt import *

from Icons import *
from Scanner import Scanner
from SourceViewer import SourceViewer
from Explorer import Explorer
from Shell import Shell
from Info import *


class UserInterface(QMainWindow):
    """UserInterface(dbs,prog=None)

    Create the user interface.  dbs is the DebugServer instance.  prog is the
    name of the optional program to debug.

    """
    def __init__(self,dbs,prog=None):
        QMainWindow.__init__(self)

        self.dbs = dbs

        self.setCaption(Program)

        self.connect(dbs,PYSIGNAL('clientGone'),self.handleClientGone)
        self.connect(dbs,PYSIGNAL('clientLine'),self.handleLineChange)
        self.connect(dbs,PYSIGNAL('clientExit'),self.handleExit)

        # Create the workspace now so that we can connect QActions to it.  This
        # is done to match the appearence of Qt Designer.
        vb = QVBox(self)
        self.setCentralWidget(vb)
        vb.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken)
        vb.setMargin(1)
        vb.setLineWidth(1)
        self.mdi = QWorkspace(vb)

        # Set the initial size.
        self.mdi.resize(QSize(1000,500).boundedTo(qApp.desktop().size()))

        # Define the user interface actions.
        openAct = QAction('Open',QIconSet(IconOpen),'&Open...',
                          Qt.CTRL+Qt.Key_O,self)
        openAct.setStatusTip('Open a Python program to debug')
        openAct.setWhatsThis(
"""<b>Open a Python program</b>"""
"""<p>You will be asked for the name of a Python program to be debugged."""
""" Any Python program currently debugged will then be stopped.</p>"""
                            )
        self.connect(openAct,SIGNAL('activated()'),self.handleOpen)

        exitAct = QAction('Exit','E&xit',0,self)
        exitAct.setStatusTip('Exit the debugger')
        exitAct.setWhatsThis(
"""<b>Exit the debugger</b>"""
"""<p>Any Python program being debugged will be stopped.</p>"""
                            )
        self.connect(exitAct,SIGNAL('activated()'),
                     qApp,SLOT('closeAllWindows()'))

        self.tileAct = QAction('Tile','&Tile',0,self)
        self.tileAct.setStatusTip('Tile the windows')
        self.tileAct.setWhatsThis(
"""<b>Tile the windows</b>"""
"""<p>Rearrange and resize the windows so that they are tiled.</p>"""
                                 )
        self.connect(self.tileAct,SIGNAL('activated()'),self.mdi.tile)

        self.cascadeAct = QAction('Cascade','&Cascade',0,self)
        self.cascadeAct.setStatusTip('Cascade the windows')
        self.cascadeAct.setWhatsThis(
"""<b>Cascade the windows</b>"""
"""<p>Rearrange and resize the windows so that they are cascaded.</p>"""
                                    )
        self.connect(self.cascadeAct,SIGNAL('activated()'),self.mdi.cascade)

        self.connect(self.tileAct,SIGNAL('activated()'),self.mdi.tile)

        self.explorerAct = QAction('Explorer','&Explorer',0,self,None,1)
        self.explorerAct.setStatusTip('Toggle the Explorer window')
        self.explorerAct.setWhatsThis(
"""<b>Toggle the Explorer window</b>"""
"""<p>If the Explorer window is hidden then display it. It is displayed"""
""" then close it.</p>"""
                                     )
        self.connect(self.explorerAct,SIGNAL('activated()'),self.handleExplorer)

        self.shellAct = QAction('Shell','&Shell',0,self,None,1)
        self.shellAct.setStatusTip('Toggle the Shell window')
        self.shellAct.setWhatsThis(
"""<b>Toggle the Shell window</b>"""
"""<p>If the Shell window is hidden then display it. It is displayed"""
""" then close it.</p>"""
                                  )
        self.connect(self.shellAct,SIGNAL('activated()'),self.handleShell)

        whatsThisAct = QAction('What\'s This?',QIconSet(IconWhatsThis),
                               'What\'s This?',Qt.SHIFT+Qt.Key_F1,self)
        whatsThisAct.setStatusTip('Context sensitive help')
        whatsThisAct.setWhatsThis(
"""<b>Display context sensitive help</b>"""
"""<p>In What's This? mode, the mouse cursor shows an arrow with a question"""
""" mark, and you can click on the interface elements to get a short"""
""" description of what they do and how to use them. In dialogs, this"""
""" feature can be accessed using the context help button in the"""
""" titlebar.</p>"""
                                 )
        self.connect(whatsThisAct,SIGNAL('activated()'),self.whatsThis)

        aboutAct = QAction('About','&About',0,self)
        aboutAct.setStatusTip('Display information about this software')
        aboutAct.setWhatsThis(
"""<b>About """ + Program + """</b>"""
"""<p>Dispay some information about this software.</p>"""
                             )
        self.connect(aboutAct,SIGNAL('activated()'),self.handleAbout)

        aboutQtAct = QAction('About Qt','About &Qt',0,self)
        aboutQtAct.setStatusTip('Display information about the Qt toolkit')
        aboutQtAct.setWhatsThis(
"""<b>About Qt</b>"""
"""<p>Dispay some information about the Qt toolkit.</p>"""
                               )
        self.connect(aboutQtAct,SIGNAL('activated()'),self.handleAboutQt)

        self.runAct = QAction('Run',QIconSet(IconRun),'&Run...',Qt.Key_F5,self)
        self.runAct.setStatusTip('Run the program')
        self.runAct.setWhatsThis(
"""<b>Run</b>"""
"""<p>Set the command line arguments set the current line to be the first"""
""" executable Python statement of the program.</p>"""
                                )
        self.connect(self.runAct,SIGNAL('activated()'),self.handleRun)

        self.debugActGrp = QActionGroup(self)

        act = QAction('Continue',QIconSet(IconContinue),'&Continue',Qt.Key_F6,
                      self.debugActGrp)
        act.setStatusTip('Continue running the program from the current line')
        act.setWhatsThis(
"""<b>Continue</b>"""
"""<p>Continue running the program from the current line. The program will"""
""" stop when it terminates or when a breakpoint is reached.</p>"""
                        )
        self.connect(act,SIGNAL('activated()'),self.handleContinue)

        act = QAction('Step',QIconSet(IconStep),'&Step',Qt.Key_F7,
                      self.debugActGrp)
        act.setStatusTip('Execute a single Python statement staying in the current frame')
        act.setWhatsThis(
"""<b>Step</b>"""
"""<p>Execute a single Python statement staying in the same frame. If"""
""" the statement is an <tt>import</tt> statement, a class constructor, or a"""
""" method or function call then control is returned to the debugger after"""
""" the statement has completed.</p>"""
                        )
        self.connect(act,SIGNAL('activated()'),self.handleStep)

        #act = QAction('Step Into',QIconSet(IconStepInto),'Step &Into',Qt.Key_F8,
                      #self.debugActGrp)
        #act.setStatusTip('Execute a single Python statement possibly dropping into another frame')
        #act.setWhatsThis(
#"""<b>Step Into</b>"""
#"""<p>Execute a single Python statement possibly dropping into another"""
#""" frame. If the statement is an <tt>import</tt> statement, a class"""
#""" constructor, or a method or function call then control is returned to"""
#""" the debugger at the first line of the body of the module, constructor,"""
#""" method or function.</p>"""
                        #)
        #self.connect(act,SIGNAL('activated()'),self.handleStepInto)

        self.debugActGrp.setEnabled(0)

        # Create the menus.
        mb = self.menuBar()

        menu = QPopupMenu(self)
        mb.insertItem('&File',menu)
        openAct.addTo(menu)
        menu.insertSeparator()
        exitAct.addTo(menu)

        menu = QPopupMenu(self)
        mb.insertItem('&Debug',menu)
        self.runAct.addTo(menu)
        self.debugActGrp.addTo(menu)

        menu = QPopupMenu(self)
        mb.insertItem('&Window',menu)
        self.connect(menu,SIGNAL('aboutToShow()'),self.handleShowWindowMenu)
        self.windowMenu = menu

        mb.insertSeparator()

        menu = QPopupMenu(self)
        mb.insertItem('&Help',menu)
        aboutAct.addTo(menu)
        aboutQtAct.addTo(menu)
        menu.insertSeparator()
        whatsThisAct.addTo(menu)

        # Create the toolbar.
        tb = QToolBar(self)
        self.addToolBar(tb)

        openAct.addTo(tb)
        tb.addSeparator()
        self.runAct.addTo(tb)
        self.debugActGrp.addTo(tb)
        tb.addSeparator()
        whatsThisAct.addTo(tb)

        # Set up the status bar.
        sb = self.statusBar()

        self.sbFile = QLabel(sb)
        sb.addWidget(self.sbFile,0,1)
        QWhatsThis.add(self.sbFile,
"""<p>This part of the status bar displays the name of file containing the"""
""" Python statement that is about to be executed.</p>"""
"""<p>It is blank if there is no program currently being debugged.</p>"""
                      )

        self.sbLine = QLabel(sb)
        sb.addWidget(self.sbLine,0,1)
        QWhatsThis.add(self.sbLine,
"""<p>This part of the status bar displays the line number of the Python"""
""" statement that is about to be executed.</p>"""
"""<p>It is blank if there is no program currently being debugged.</p>"""
                      )

        self.setSbFile()

        # Initialise the instance variables.
        self.scanners = []
        self.srcWindows = []
        self.currentProg = QFileInfo()
        self.isProg = 0
        self.currentScanner = None
        self.argvHistory = QStringList()

        # The initial user interface is the Explorer and the Shell.
        self.explorer = Explorer(self.mdi)
        self.connect(self,PYSIGNAL('debuggedProgramChanged'),
                     self.explorer.handleProgramChange)
        self.connect(self.explorer,PYSIGNAL('pythonFile'),
                     self.handlePythonFile)
        self.explorer.show()

        self.shell = Shell(dbs,self.mdi)
        self.shell.show()

        if prog is not None:
            self.handleOpen(prog)

    def handleAbout(self):
        """
        Private slot to handle the About dialog.
        """
        QMessageBox.information(self,Program,
"""<h3> About """ + Program + """</h3>"""
"""<p>""" + Program + """ is a graphical debugger for the Python"""
""" programming language. It is written using the PyQt Python bindings for"""
""" the Qt GUI toolkit.</p>"""
"""<p>This version is """ + Version + """.</p>"""
"""<p>For more information see"""
""" <tt>http://www.thekompany.com/projects/pykde/</tt>.</p>"""
"""<p>""" + Copyright + """</p>"""
                               )

    def handleAboutQt(self):
        """
        Private slot to handle the About Qt dialog.
        """
        QMessageBox.aboutQt(self,Program)

    def handleOpen(self,prog=None):
        """
        Public slot to open a new Python program to debug.
        """
        # Get the program name if one wasn't specified.
        if prog is None:
            prog = QFileDialog.getOpenFileName(self.currentProg.absFilePath(),'Python Files (*.py)',self)

            if prog.isNull():
                return

        self.currentProg.setFile(prog)
        prog = self.currentProg.absFilePath()

        # Open up the new program.
        try:
            sv = self.displayPythonFile(prog)
        except IOError:
            return

        # Close any existing program.
        self.closeProgram()

        self.setCaption(QString(Program + ' - %1').arg(prog))

        # Signal the change in program.
        self.emit(PYSIGNAL('debuggedProgramChanged'),(self.currentProg,))

        self.runAct.setEnabled(1)
        self.isProg = 1
        sv.show()

    def closeProgram(self):
        """
        Private method to close the current program.
        """
        if self.isProg:
            for sv in self.srcWindows:
                sv.close()

            self.srcWindows = []
            self.scanners = []
            self.isProg = 0
            self.runAct.setEnabled(0)
            self.debugActGrp.setEnabled(0)

    def handleLineChange(self,fn,line):
        """
        Private method to handle a change to the current line.
        """
        self.setFileLine(fn,line)
        self.debugActGrp.setEnabled(1)

    def handleExit(self,status):
        """
        Private method to handle the debugged program terminating.
        """
        if self.currentScanner is not None:
            self.currentScanner.highlight()
            self.currentScanner = None

        self.setSbFile()

        QMessageBox.information(self,Program,
                    '<tt>%s</tt> has terminated with an exit status of %s.' %
                        (self.currentProg.absFilePath(),status))

    def handleClientGone(self,unplanned):
        """
        Private method to handle the disconnection of the debugger client.
        """
        if unplanned:
            QMessageBox.information(self,Program,
                    'The program being debugged has terminated unexpectedly.')

    def handlePythonFile(self,pyfn):
        """
        Private method to handle the user selecting a Python file with
        Explorer.
        """
        try:
            self.displayPythonFile(pyfn).show()
        except IOError:
            pass

    def displayPythonFile(self,fn):
        """
        Private method to display a file in a Source Viewer.  An exception will
        be raised if the file could not be read.
        """
        scn = self.getScanner(fn)
        sv = SourceViewer(scn,self.mdi)
        self.srcWindows.append(sv)

        return sv

    def handleShowWindowMenu(self):
        """
        Private slot to set up the Window menu.
        """
        self.windowMenu.clear()

        self.tileAct.addTo(self.windowMenu)
        self.cascadeAct.addTo(self.windowMenu)

        self.windowMenu.insertSeparator()

        # Set the rest of the options according to what is being displayed.
        self.explorerAct.addTo(self.windowMenu)
        self.explorerAct.setOn(not self.explorer.isHidden())

        self.shellAct.addTo(self.windowMenu)
        self.shellAct.setOn(not self.shell.isHidden())

        # Now do any Source Viewer windows.
        idx = 0

        for sv in self.srcWindows:
            if idx == 0:
                self.windowMenu.insertSeparator()

            id = self.windowMenu.insertItem(sv.getFileName(),
                                            self.handleSVWindow)
            self.windowMenu.setItemParameter(id,idx)
            self.windowMenu.setItemChecked(id,not sv.isHidden())

            idx = idx + 1

    def handleExplorer(self):
        """
        Private slot to handle the toggle of the Explorer window.
        """
        self.toggleWindow(self.explorer)

    def handleShell(self):
        """
        Private slot to handle the toggle of the Shell window.
        """
        self.toggleWindow(self.shell)

    def handleSVWindow(self,idx):
        """
        Private method to handle to toggle of a Source Viewer window.
        """
        self.toggleWindow(self.srcWindows[idx])

    def toggleWindow(self,w):
        """
        Private method to toggle a workspace window.
        """
        if w.isHidden():
            w.show()
        else:
            w.hide()

    def setFileLine(self,fn,line):
        """
        Private method to update the user interface when the current program or
        line changes.
        """
        self.setSbFile(fn,line)

        try:
            self.currentScanner = self.getScanner(fn)
        except IOError:
            return

        # Change the highlighted line.
        self.currentScanner.highlight(line)

        # If there is already a viewer then make sure it isn't hidden.  We
        # would really like to make sure it is completely visible, but
        # QWorkspace is really limited in the control it gives over it's
        # contents.
        for sv in self.srcWindows:
            if sv.canvas() is self.currentScanner:
                break
        else:
            sv = SourceViewer(self.currentScanner,self.mdi)
            self.srcWindows.append(sv)

        sv.highlightVisible()

    def setSbFile(self,fn=None,line=None):
        """
        Private method to set the file in the status bar.
        """
        if fn is None:
            line = None
            fn = ''

        self.sbFile.setText(QString('File: %1').arg(fn,-50))

        if line is None:
            line = ''

        self.sbLine.setText(QString('Line: %1').arg(line,5))

    def handleRun(self):
        """
        Private method to handle the Run action.
        """
        # Get the command line arguments.
        if len(self.argvHistory) > 0:
            curr = 0
        else:
            curr = -1

        argv, ok = QInputDialog.getItem('Run',
                                        'Enter the command line arguments',
                                        self.argvHistory,curr,1,self)

        if ok:
            if argv.isNull():
                argv = QString('')

            # This moves any previous occurence of these arguments to the head
            # of the list.
            self.argvHistory.remove(argv)
            self.argvHistory.prepend(argv)

            # Ask the client to open the new program.
            self.dbs.remoteLoad(self.currentProg,argv)

    def handleContinue(self):
        """
        Private method to handle the Continue action.
        """
        self.enterRemote()
        self.dbs.remoteContinue()

    def handleStep(self):
        """
        Private method to handle the Step action.
        """
        self.enterRemote()
        self.dbs.remoteStep()

    def enterRemote(self):
        """
        Private method to update the user interface prior to executing some of
        the program being debugged.
        """
        # Disable further debug commands from the user.
        self.debugActGrp.setEnabled(0)

        if self.currentScanner is not None:
            self.currentScanner.highlight()

    def getScanner(self,fn):
        """
        Private method to return the scanner displaying the given file creating
        a new one if needed.
        """
        for scn in self.scanners:
            if QString.compare(fn,scn.getFileName()) == 0:
                break
        else:
            scn = Scanner(self.dbs,fn)
            self.scanners.append(scn)

        return scn
