1. Brendan Howell
  2. PyCessing

Source

PyCessing / mainwindow.py

"""
    mainwindow.py
    Copyright 2011 Brendan Howell (brendan@howell-ersatz.com)

    This file is part of PyCessing.

    PyCessing is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    PyCessing is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with PyCessing.  If not, see <http://www.gnu.org/licenses/>.
"""


#!/usr/bin/python

#mainwindow.py - main window IDE for pycessing
#TODO: version header from git
#TODO: wrap strings in TR()
#TODO: handle file dirty in header
#TODO: cut and paste from help browser

from PyQt4 import QtGui, QtCore, Qsci
import os, subprocess, signal, tempfile, sys, glob

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.running = False
        tmp, self.tempFile = tempfile.mkstemp(".cess")
        os.close(tmp)
        tmp, self.logFile = tempfile.mkstemp()
        os.close(tmp)

        self.resize(960, 500)
        self.setWindowTitle('PyCessing')
        self.fileName = None
        self.cwd = os.path.dirname(os.path.abspath(sys.argv[0]))
        #print self.cwd
        
        self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
        self.setCentralWidget(self.splitter)
        
        self.setupToolbar()
        self.setupEditor()
        self.setupMessageBox()
        self.statusBar()
        
        self.setupFileMenu()
        self.setupEditMenu()
        self.setupProgramMenu()
        self.setupHelpMenu()
        self.sketchPID = None

        
    def about(self):
        QtGui.QMessageBox.about(self,"About Pycessing","Copyright 2011 <br />Brendan Howell<br />Under the GPL.")
        
    #TODO: this should open a new tab (or window)
    def newFile(self):
        response = None
        
        if self.textEdit.isModified():
            #options = QtGui.QMessageBox.Ok + QtGui.QMessageBox.NoButton
            response = QtGui.QMessageBox.critical(self,"New Program","Current file is not saved.  Are you sure you want to discard your changes?", QtGui.QMessageBox.Discard, QtGui.QMessageBox.Cancel, QtGui.QMessageBox.Save)
        
        if response == QtGui.QMessageBox.Cancel:
            return False
        elif response == QtGui.QMessageBox.Save:
            self.saveFile()
        
        self.textEdit.clear()
        self.fileName = None
        self.setWindowTitle('PyCessing')
        return True
        
    def openFile(self):
    	if not(self.newFile()):
    	    return
        self.fileName = QtGui.QFileDialog.getOpenFileName(self, "Open File", "", "Pycessing Files (*.cess)")
        if(self.fileName):
            newFile = open(self.fileName)
            self.textEdit.setText(newFile.read())
            newFile.close()
            shortname = os.path.basename(str(self.fileName))
            self.setWindowTitle('PyCessing - ' + shortname)
            
    def openExample(self, filename):
        if not(self.newFile()):
            return
        exfile = os.path.join(self.cwd, 'examples/' + filename + ".cess")
        examplecode = open(exfile)
        self.textEdit.setText(examplecode.read())
        examplecode.close()
        self.setWindowTitle('PyCessing - ' + filename)
            
    def setupEditor(self):
        font = QtGui.QFont()
        font.setFamily("Droid Sans Mono")
        font.setFixedPitch(True)
        font.setPointSize(13)
        font.setItalic(False)
        fontMetrics = QtGui.QFontMetrics(font)
        
        self.textEdit = Qsci.QsciScintilla(self)
        lexer = Qsci.QsciLexerPython()
        lexer.setFont(font)
        self.textEdit.setLexer(lexer)
        
        self.textEdit.setAutoIndent(True)
        self.textEdit.setIndentationWidth(4)
        self.textEdit.setIndentationsUseTabs(False)
        
        self.textEdit.setMarginWidth(0, fontMetrics.width("00000") + 5)
        self.textEdit.setMarginLineNumbers(0,True)
        
        self.textEdit.setEdgeMode(Qsci.QsciScintilla.EdgeLine)
        self.textEdit.setEdgeColumn(80)
        self.textEdit.setWrapMode(Qsci.QsciScintilla.WrapWord)
        self.textEdit.setWrapVisualFlags(Qsci.QsciScintilla.WrapFlagByText)
        self.textEdit.setBraceMatching(Qsci.QsciScintilla.SloppyBraceMatch)
        
        self.splitter.addWidget(self.textEdit)
        size = self.textEdit.sizeHint()
        if size.height() < 300:
            size.setHeight(300)
        print "size: " + str(size.width()) + " x " + str(size.height())
        self.textEdit.resize(800,500)
        
    def setupMessageBox(self):
    	self.messageBox = QtGui.QTextEdit(self)
    	self.messageBox.setReadOnly(True)
    	self.splitter.addWidget(self.messageBox)
    	#self.messageBox.setAutoFormatting(QtGui.QTextBrowser.LogText)
        
    def setupFileMenu(self):
        iconfile = os.path.join(self.cwd, 'icons/exit.png')
        exit = QtGui.QAction(QtGui.QIcon(iconfile), '&Quit', self)
        exit.setShortcut('Ctrl+Q')
        exit.setStatusTip('Exit application')
        self.connect(exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
               
        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        
        #TODO: ctrl-W should close the tab
        fileMenu.addAction("&New", self.newFile, QtGui.QKeySequence("Ctrl+N"))
        exampleMenu = fileMenu.addMenu("Examples")
        fileMenu.addAction("&Open", self.openFile, QtGui.QKeySequence("Ctrl+O"))
        fileMenu.addAction("&Save", self.saveFile, QtGui.QKeySequence("Ctrl+S"))
        fileMenu.addAction("Save &As", self.saveFileAs, QtGui.QKeySequence("Shift+Ctrl+S"))
        fileMenu.addAction("&Print", self.printFile, QtGui.QKeySequence("Ctrl+P"))
        fileMenu.addAction(exit)
        
        exampleDir = os.path.join(self.cwd, 'examples/')
        examples = glob.glob(exampleDir + '*.cess')
        #glob examples
        #TODO: glob sub-directories
        for example in examples:
            example = os.path.basename(example)
            example = example.replace('.cess','')
            exMenuItem = exampleMenu.addAction(example)
            receiver = lambda example=example: self.openExample(example)
            self.connect(exMenuItem, QtCore.SIGNAL('triggered()'), receiver)

    def setupToolbar(self):
        toolbar = self.addToolBar('Toolbar')
        iconfile = os.path.join(self.cwd, 'icons/play.png')
        self.playToggle = QtGui.QAction(QtGui.QIcon(iconfile), 'Run Program', toolbar)
        self.connect(self.playToggle, QtCore.SIGNAL('triggered()'), self.togglePlaying)
        toolbar.addAction(self.playToggle)
       
	    #add spacer
        spacer = QtGui.QWidget(self)
        spacer.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
        toolbar.addWidget(spacer)
        
        #add find widget
        self.findEdit = QtGui.QLineEdit(self)
        self.findEdit.setFixedSize(QtCore.QSize(200, self.findEdit.height()))
        toolbar.addWidget(self.findEdit)
        
        #add expandable spacer
        
        # find button
        iconfile = os.path.join(self.cwd, "icons/Edit-find.png")
        self.findbutton = QtGui.QAction(QtGui.QIcon(iconfile), 'Find in Program', toolbar)
        self.connect(self.findbutton, QtCore.SIGNAL('triggered()'), self.findNext)
        toolbar.addAction(self.findbutton)

        #add help browser toggle        
        iconfile = os.path.join(self.cwd, "icons/help-browser.png")
        self.helptoggle = QtGui.QAction(QtGui.QIcon(iconfile), 'Show / Hide Help', toolbar)
        self.connect(self.helptoggle, QtCore.SIGNAL('triggered()'), self.toggleHelpBrowser)
        #help hide / show action
        toolbar.addAction(self.helptoggle)
        
    def setupHelpMenu(self):
        iconfile = os.path.join(self.cwd, 'icons/about.png')
        aboutItem = QtGui.QAction(QtGui.QIcon('icons/about.png'), 'About', self)
        self.connect(aboutItem, QtCore.SIGNAL('triggered()'), self.about)
    
        menubar = self.menuBar()
        helpMenu = menubar.addMenu('&Help')
        helpMenu.addAction(aboutItem)
        
    	self.helpwindow = QtGui.QDockWidget("Doc Browser")
    	self.helpwindow.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
    	self.helpwindow.setMinimumWidth(400)
    	self.helpwindow.setFeatures(QtGui.QDockWidget.DockWidgetClosable | QtGui.QDockWidget.DockWidgetFloatable)
    	helpAction = self.helpwindow.toggleViewAction()
    	helpAction.setText("&Help Browser")
    	helpAction.setShortcut(QtGui.QKeySequence("F1"))
    	helpMenu.addAction(helpAction)
    	self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.helpwindow)
    	
    	helpbrowser = QtGui.QTextBrowser(self.helpwindow)
    	indexpath = os.path.join(self.cwd, "help", "index.html")
    	url = QtCore.QUrl("file://" + indexpath)
    	helpbrowser.setSource(url)
    	helpbrowser.resize(400, self.helpwindow.height())
    	
        
    def setupEditMenu(self):
        menubar = self.menuBar()
        editMenu = menubar.addMenu('&Edit')
        
        editMenu.addAction('&Undo', self.textEdit.undo, QtGui.QKeySequence("Ctrl+Z"))
        editMenu.addAction('&Redo', self.textEdit.redo, QtGui.QKeySequence("Shift+Ctrl+Z"))
        
        editMenu.addSeparator()
        
        editMenu.addAction('Cu&t', self.textEdit.cut, QtGui.QKeySequence("Ctrl+X"))
        editMenu.addAction('&Copy', self.textEdit.copy, QtGui.QKeySequence("Ctrl+C"))
        editMenu.addAction('&Paste', self.textEdit.paste, QtGui.QKeySequence("Ctrl+V"))
        #editMenu.addAction('&Delete', self.textEdit.delete, QtGui.QKeySequence("Del"))
        
        editMenu.addSeparator()
        
        editMenu.addAction('&Indent', self.doIndent, QtGui.QKeySequence("Ctrl+T"))
        editMenu.addAction('&Un-indent', self.doUnIndent, QtGui.QKeySequence("Shift+Ctrl+T"))
        
    def setupProgramMenu(self):
        menubar = self.menuBar()
        progMenu = menubar.addMenu("&Program")
        
        progMenu.addAction('&Run / Stop', self.togglePlaying, QtGui.QKeySequence("Ctrl+R"))
    
    #TODO: catch file exceptions    
    def saveFile(self):
        if not(self.fileName):
            self.saveFileAs()
        fp = open(self.fileName, "w")
        fp.write(self.textEdit.text())
        fp.close()
        
    def saveFileAs(self):
        self.fileName = QtGui.QFileDialog.getSaveFileName(self, "Save File", "", "Pycessing program (*.cess)")
        if not(self.fileName.endsWith(".cess")):
            self.fileName += ".cess"
        self.saveFile()
        shortname = os.path.basename(str(self.fileName))
        self.setWindowTitle('PyCessing - ' + shortname)
        
    def printFile(self):
        pass 
        
    def helpContents(self):
        QtGui.QDesktopServices.openUrl(QtCore.QUrl("help/index.html"))
        
    def doIndent(self):
        (lineFrom, indexFrom, lineTo, indexTo) = self.textEdit.getSelection()
        if(lineFrom == -1):
            line, index = self.textEdit.getCursorPosition()
            self.textEdit.indent(line)
        else:
            for curLine in range(lineFrom, lineTo+1):
                self.textEdit.indent(curLine)
                
    def doUnIndent(self):
        (lineFrom, indexFrom, lineTo, indexTo) = self.textEdit.getSelection()
        if(lineFrom == -1):
            line, index = self.textEdit.getCursorPosition()
            self.textEdit.unindent(line)
        else:
            for curLine in range(lineFrom, lineTo+1):
                self.textEdit.unindent(curLine)
    
    def _saveTmp(self):
        #FIXME: this might be kinda insecure
        if self.textEdit.isModified():
            fp = open(self.tempFile, "w")
            fp.write(self.textEdit.text())
            fp.close()
        
    def togglePlaying(self):
        if(self.running):         
            self.sketchPID.kill()
            
        else:
            self.messageBox.clear()
            self.messageBox.append("starting...")            
            print "starting program"
            
            #fork the child process
            self._saveTmp()
            #app = "/opt/local/bin/python"
            app = sys.executable
            if self.fileName != None:
                progdir = os.path.dirname(str(self.fileName))
            else:
                progdir = os.path.join(self.cwd, 'examples/')
            args = ["-u", "run.py", self.tempFile, progdir]
            self.sketchPID = QtCore.QProcess(self)
                              
            self.connect(self.sketchPID, QtCore.SIGNAL('finished(int,QProcess::ExitStatus)'), self._progStopped)
            self.connect(self.sketchPID, QtCore.SIGNAL('readyReadStandardOutput()'), self._outMessage)
            self.connect(self.sketchPID, QtCore.SIGNAL('readyReadStandardError()'), self._errorMessage)
            self.sketchPID.start(app, args)
            self.sketchPID.waitForStarted()
            
            self.running = True
            iconfile = os.path.join(self.cwd, 'icons/Process-stop.png')
            self.playToggle.setIcon(QtGui.QIcon(iconfile))
            self.playToggle.setToolTip("Stop running program")
            self.statusBar().showMessage("Program Running...")
            
    def toggleHelpBrowser(self):
        self.helpwindow.setVisible(not(self.helpwindow.isVisible()))
        
        
    def findNext(self):
    	searchString = self.findEdit.text()
    	self.textEdit.findFirst(searchString, False, False, False, True)
            
    def _progStopped(self, exitcode, exitstatus):
        print "program stopped:"
        print exitstatus
        #Normal exit actually means python errored out, crash means it was stopped by user
        if exitstatus == QtCore.QProcess.CrashExit:
	        self.statusBar().showMessage("Program Stopped")
        else:
            self.statusBar().showMessage("Oops! Program Crashed")
        self.running = False
        iconfile = os.path.join(self.cwd, 'icons/play.png')
        self.playToggle.setIcon(QtGui.QIcon(iconfile))
        self.playToggle.setToolTip("Run program")
    	
    def _errorMessage(self):
        self.sketchPID.setReadChannel(QtCore.QProcess.StandardError)
        while(self.sketchPID.canReadLine()):
            self.messageBox.append(QtCore.QString(self.sketchPID.readLine()))
    
    def _outMessage(self):
        self.sketchPID.setReadChannel(QtCore.QProcess.StandardOutput)
        while(self.sketchPID.canReadLine()):
            self.messageBox.append(QtCore.QString(self.sketchPID.readLine()))