Source code for umbra.components.factory.scriptEditor.editor

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
**editor.py**

**Platform:**
	Windows, Linux, Mac Os X.

**Description:**
	This module defines the :class:`Editor` class and others editing helper objects.

**Others:**

"""

#**********************************************************************************************************************
#***	Future imports.
#**********************************************************************************************************************
from __future__ import unicode_literals

#**********************************************************************************************************************
#***	External imports.
#**********************************************************************************************************************
import os
import platform
from PyQt4.QtCore import Qt
from PyQt4.QtCore import pyqtSignal
from PyQt4.QtGui import QFileDialog
from PyQt4.QtGui import QFont
from PyQt4.QtGui import QMessageBox
from PyQt4.QtGui import QPlainTextDocumentLayout
from PyQt4.QtGui import QTextOption

#**********************************************************************************************************************
#***	Internal imports.
#**********************************************************************************************************************
import foundations.exceptions
import foundations.io
import foundations.strings
import foundations.verbose
import umbra.ui.common
import umbra.ui.widgets.messageBox as messageBox
from umbra.ui.languages import PYTHON_LANGUAGE
from umbra.ui.widgets.codeEditor_QPlainTextEdit import CodeEditor_QPlainTextEdit

#**********************************************************************************************************************
#***	Module attributes.
#**********************************************************************************************************************
__author__ = "Thomas Mansencal"
__copyright__ = "Copyright (C) 2008 - 2013 - Thomas Mansencal"
__license__ = "GPL V3.0 - http://www.gnu.org/licenses/"
__maintainer__ = "Thomas Mansencal"
__email__ = "thomas.mansencal@gmail.com"
__status__ = "Production"

__all__ = ["LOGGER", "Editor"]

LOGGER = foundations.verbose.installLogger()

#**********************************************************************************************************************
#***	Module classes and definitions.
#**********************************************************************************************************************
[docs]class Editor(CodeEditor_QPlainTextEdit): """ This class defines the default editor used by the :class:`umbra.components.factory.scriptEditor.scriptEditor.ScriptEditor` Component Interface class. """ __untitledNameId = 1 """Editor untitled name id. ( Integer )""" # Custom signals definitions. titleChanged = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current title is changed. ( pyqtSignal ) """ fileLoaded = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current file is loaded. ( pyqtSignal ) """ fileSaved = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current file is saved. ( pyqtSignal ) """ fileReloaded = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current file is reloaded. ( pyqtSignal ) """ fileClosed = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current file is closed. ( pyqtSignal ) """ contentsChanged = pyqtSignal() """ This signal is emited by the :class:`Editor` class when the current editor document content has changed. ( pyqtSignal ) """ modificationChanged = pyqtSignal(bool) """ This signal is emited by the :class:`Editor` class when the current editor document content has been modified. ( pyqtSignal ) """ def __init__(self, parent=None, file=None, language=PYTHON_LANGUAGE, *args, **kwargs): """ .. Sphinx: Statements updated for auto-documentation purpose. :param parent: Object parent. ( QObject ) :param file: File path. ( String ) :param language: Editor language. ( Language ) :param \*args: Arguments. ( \* ) :param \*\*kwargs: Keywords arguments. ( \*\* ) """ LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__)) CodeEditor_QPlainTextEdit.__init__(self, parent, language, *args, **kwargs) # --- Setting class attributes. --- self.__file = None self.file = file self.__defaultFontsSettings = {"Windows" : ("Consolas", 10), "Darwin" : ("Monaco", 12), "Linux" : ("Monospace", 10)} self.__tabWidth = None self.__title = None self.__isUntitled = True self.__defaultFileName = "Untitled" self.__defaultFileExtension = "py" Editor.__initializeUi(self) file and self.loadFile(file) #****************************************************************************************************************** #*** Attributes properties. #****************************************************************************************************************** @property def file(self): """ This method is the property for **self.__file** attribute. :return: self.__file. ( String ) """ return self.__file @file.setter @foundations.exceptions.handleExceptions(AssertionError) def file(self, value): """ This method is the setter method for **self.__file** attribute. :param value: Attribute value. ( String ) """ if value is not None: assert type(value) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format("file", value) self.__file = value @file.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def file(self): """ This method is the deleter method for **self.__file** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "file"))
@property def defaultFontsSettings(self): """ This method is the property for **self.__defaultFontsSettings** attribute. :return: self.__defaultFontsSettings. ( Dictionary ) """ return self.__defaultFontsSettings @defaultFontsSettings.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def defaultFontsSettings(self, value): """ This method is the setter method for **self.__defaultFontsSettings** attribute. :param value: Attribute value. ( Dictionary ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFontsSettings")) @defaultFontsSettings.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFontsSettings(self): """ This method is the deleter method for **self.__defaultFontsSettings** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFontsSettings"))
@property def tabWidth(self): """ This method is the property for **self.__tabWidth** attribute. :return: self.__tabWidth. ( Integer ) """ return self.__tabWidth @tabWidth.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def tabWidth(self, value): """ This method is the setter method for **self.__tabWidth** attribute. :param value: Attribute value. ( Integer ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "tabWidth")) @tabWidth.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def tabWidth(self): """ This method is the deleter method for **self.__tabWidth** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "tabWidth"))
@property def title(self): """ This method is the property for **self.__title** attribute. :return: self.__title. ( String ) """ return self.__title @title.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def title(self, value): """ This method is the setter method for **self.__title** attribute. :param value: Attribute value. ( String ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "title")) @title.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def title(self): """ This method is the deleter method for **self.__title** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "title"))
@property def isUntitled(self): """ This method is the property for **self.__isUntitled** attribute. :return: self.__isUntitled. ( Boolean ) """ return self.__isUntitled @isUntitled.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def isUntitled(self, value): """ This method is the setter method for **self.__isUntitled** attribute. :param value: Attribute value. ( Boolean ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "isUntitled")) @isUntitled.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def isUntitled(self): """ This method is the deleter method for **self.__isUntitled** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "isUntitled"))
@property def defaultFileName(self): """ This method is the property for **self.__defaultFileName** attribute. :return: self.__defaultFileName. ( String ) """ return self.__defaultFileName @defaultFileName.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def defaultFileName(self, value): """ This method is the setter method for **self.__defaultFileName** attribute. :param value: Attribute value. ( String ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFileName")) @defaultFileName.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFileName(self): """ This method is the deleter method for **self.__defaultFileName** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFileName"))
@property def defaultFileExtension(self): """ This method is the property for **self.__defaultFileExtension** attribute. :return: self.__defaultFileExtension. ( String ) """ return self.__defaultFileExtension @defaultFileExtension.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def defaultFileExtension(self, value): """ This method is the setter method for **self.__defaultFileExtension** attribute. :param value: Attribute value. ( String ) """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFileExtension")) @defaultFileExtension.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFileExtension(self): """ This method is the deleter method for **self.__defaultFileExtension** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFileExtension")) #****************************************************************************************************************** #*** Class methods. #******************************************************************************************************************
def __initializeUi(self): """ This method initializes the Widget ui. """ self.setAttribute(Qt.WA_DeleteOnClose) self.setWordWrapMode(QTextOption.NoWrap) self.setAcceptDrops(True) if platform.system() == "Windows" or platform.system() == "Microsoft": fontFamily, fontSize = self.__defaultFontsSettings["Windows"] elif platform.system() == "Darwin": fontFamily, fontSize = self.__defaultFontsSettings["Darwin"] elif platform.system() == "Linux": fontFamily, fontSize = self.__defaultFontsSettings["Linux"] font = QFont(fontFamily) font.setPointSize(fontSize) self.setFont(font) def __document__contentsChanged(self): """ This method is triggered when the editor document content changes. """ self.setTitle() def __document__modificationChanged(self, changed): """ This method is triggered when the editor document is modified. :param changed: File modification state. ( Boolean ) """ self.setTitle() def __setDocumentSignals(self): """ This method connects the editor document signals. """ # Signals / Slots. self.document().contentsChanged.connect(self.contentsChanged.emit) self.document().contentsChanged.connect(self.__document__contentsChanged) self.document().modificationChanged.connect(self.modificationChanged.emit) self.document().modificationChanged.connect(self.__document__modificationChanged)
[docs] def setTitle(self, title=None): """ This method sets the editor title. :param title: Editor title. ( String ) :return: Method success. ( Boolean ) """ if not title: # TODO: https://bugreports.qt-project.org/browse/QTBUG-27084 # titleTemplate = self.isModified() and "{0} *" or "{0}" # title = titleTemplate.format(self.getFileShortName()) title = self.getFileShortName() LOGGER.debug("> Setting editor title to '{0}'.".format(title)) self.__title = title self.setWindowTitle(title) self.titleChanged.emit() return True
[docs] def setFile(self, file=None, isModified=False, isUntitled=False): """ This method sets the editor file. :param File: File to set. ( String ) :param isModified: File modified state. ( Boolean ) :param isUntitled: File untitled state. ( Boolean ) :return: Method success. ( Boolean ) """ LOGGER.debug("> Setting '{0}' editor file.".format(file)) self.__file = file self.__isUntitled = isUntitled self.setModified(isModified) self.setTitle() return True
[docs] def getFileShortName(self): """ This method returns the current editor file short name. :return: File short name. ( String ) """ if not self.__file: return "" return os.path.basename(self.__file)
[docs] def getUntitledFileName(self): """ This method returns an untitled editor file name. :return: Untitled file name. ( String ) """ name = "{0} {1}.{2}".format(self.__defaultFileName, Editor._Editor__untitledNameId, self.defaultFileExtension) Editor._Editor__untitledNameId += 1 LOGGER.debug("> Next untitled file name: '{0}'.".format(name)) return name
[docs] def loadDocument(self, document, file=None, language=None): """ This method loads given document into the editor. :param document: Document to load. ( QTextDocument ) :param file: File. ( String ) :param language: Editor language. ( String ) :return: Method success. ( Boolean ) """ document.setDocumentLayout(QPlainTextDocumentLayout(document)) self.setDocument(document) self.setFile(file) self.setLanguage(language) self.__setDocumentSignals() self.fileLoaded.emit() return True
[docs] def newFile(self): """ This method creates a new editor file. :return: File name. ( String ) """ file = self.getUntitledFileName() LOGGER.debug("> Creating '{0}' file.".format(file)) self.setFile(file, isModified=False, isUntitled=True) self.__setDocumentSignals() return file
@foundations.exceptions.handleExceptions(foundations.exceptions.FileExistsError)
[docs] def loadFile(self, file): """ This method reads and loads given file into the editor. :param File: File to load. ( String ) :return: Method success. ( Boolean ) """ if not foundations.common.pathExists(file): raise foundations.exceptions.FileExistsError("{0} | '{1}' file doesn't exists!".format(self.__class__.__name__, file)) LOGGER.debug("> Loading '{0}' file.".format(file)) reader = foundations.io.File(file) self.setPlainText(reader.read()) self.setFile(file) self.__setDocumentSignals() self.fileLoaded.emit() return True
@foundations.exceptions.handleExceptions(foundations.exceptions.FileExistsError)
[docs] def reloadFile(self, isModified=True): """ This method reloads the current editor file. :param isModified: File modified state. ( Boolean ) :return: Method success. ( Boolean ) """ if not foundations.common.pathExists(self.__file): raise foundations.exceptions.FileExistsError("{0} | '{1}' file doesn't exists!".format( self.__class__.__name__, self.__file)) LOGGER.debug("> Reloading '{0}' file.".format(self.__file)) reader = foundations.io.File(self.__file) if reader.cache(): self.setContent(reader.content) self.setFile(self.__file, isModified=isModified) self.fileReloaded.emit() return True
[docs] def saveFile(self): """ This method saves the editor file content. :return: Method success. ( Boolean ) """ if not self.__isUntitled and foundations.common.pathExists(self.__file): return self.writeFile(self.__file) else: return self.saveFileAs()
[docs] def saveFileAs(self, file=None): """ This method saves the editor file content either using given file or user chosen file. :return: Method success. ( Boolean ) :note: This method may require user interaction. """ file = file or umbra.ui.common.storeLastBrowsedPath(QFileDialog.getSaveFileName(self, "Save As:", self.__file)) if not file: return False return self.writeFile(foundations.strings.toString(file))
[docs] def writeFile(self, file): """ This method writes the editor file content into given file. :param file: File to write. ( String ) :return: Method success. ( Boolean ) """ LOGGER.debug("> Writing '{0}' file.".format(file)) writer = foundations.io.File(file) writer.content = [self.toPlainText().toUtf8()] if writer.write(): self.setFile(file) self.fileSaved.emit() return True
[docs] def closeFile(self): """ This method closes the editor file. :return: Method success. ( Boolean ) """ if not self.isModified(): LOGGER.debug("> Closing '{0}' file.".format(self.__file)) self.fileClosed.emit() return True choice = messageBox.messageBox("Warning", "Warning", "'{0}' document has been modified!\nWould you like to save your changes?".format(self.getFileShortName()), buttons=QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if choice == QMessageBox.Save: if self.saveFile(): LOGGER.debug("> Closing '{0}' file.".format(self.__file)) return True elif choice == QMessageBox.Discard: LOGGER.debug("> Discarding '{0}' file.".format(self.__file)) self.fileClosed.emit() return True