#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
**editor.py**
**Platform:**
Windows, Linux, Mac Os X.
**Description:**
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 - 2014 - Thomas Mansencal"
__license__ = "GPL V3.0 - http://www.gnu.org/licenses/"
__maintainer__ = "Thomas Mansencal"
__email__ = "[email protected]"
__status__ = "Production"
__all__ = ["LOGGER", "Editor"]
LOGGER = foundations.verbose.installLogger()
#**********************************************************************************************************************
#*** Module classes and definitions.
#**********************************************************************************************************************
[docs]class Editor(CodeEditor_QPlainTextEdit):
"""
Defines the default editor used by
the :class:`umbra.components.factory.scriptEditor.scriptEditor.ScriptEditor` Component Interface class.
"""
__untitledNameId = 1
"""
:param __untitledNameId: Editor untitled name id.
:type __untitledNameId: int
"""
# 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):
"""
Initializes the class.
:param parent: Object parent.
:type parent: QObject
:param file: File path.
:type file: unicode
:param language: Editor language.
:type language: Language
:param \*args: Arguments.
:type \*args: \*
:param \*\*kwargs: Keywords arguments.
:type \*\*kwargs: \*\*
"""
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):
"""
Property for **self.__file** attribute.
:return: self.__file.
:rtype: unicode
"""
return self.__file
@file.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def file(self, value):
"""
Setter for **self.__file** attribute.
:param value: Attribute value.
:type value: unicode
"""
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
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def file(self):
"""
Deleter for **self.__file** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "file"))
@property
def defaultFontsSettings(self):
"""
Property for **self.__defaultFontsSettings** attribute.
:return: self.__defaultFontsSettings.
:rtype: dict
"""
return self.__defaultFontsSettings
@defaultFontsSettings.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def defaultFontsSettings(self, value):
"""
Setter for **self.__defaultFontsSettings** attribute.
:param value: Attribute value.
:type value: dict
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFontsSettings"))
@defaultFontsSettings.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFontsSettings(self):
"""
Deleter for **self.__defaultFontsSettings** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFontsSettings"))
@property
def tabWidth(self):
"""
Property for **self.__tabWidth** attribute.
:return: self.__tabWidth.
:rtype: int
"""
return self.__tabWidth
@tabWidth.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def tabWidth(self, value):
"""
Setter for **self.__tabWidth** attribute.
:param value: Attribute value.
:type value: int
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "tabWidth"))
@tabWidth.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def tabWidth(self):
"""
Deleter for **self.__tabWidth** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "tabWidth"))
@property
def title(self):
"""
Property for **self.__title** attribute.
:return: self.__title.
:rtype: unicode
"""
return self.__title
@title.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def title(self, value):
"""
Setter for **self.__title** attribute.
:param value: Attribute value.
:type value: unicode
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "title"))
@title.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def title(self):
"""
Deleter for **self.__title** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "title"))
@property
def isUntitled(self):
"""
Property for **self.__isUntitled** attribute.
:return: self.__isUntitled.
:rtype: bool
"""
return self.__isUntitled
@isUntitled.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def isUntitled(self, value):
"""
Setter for **self.__isUntitled** attribute.
:param value: Attribute value.
:type value: bool
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "isUntitled"))
@isUntitled.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def isUntitled(self):
"""
Deleter for **self.__isUntitled** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "isUntitled"))
@property
def defaultFileName(self):
"""
Property for **self.__defaultFileName** attribute.
:return: self.__defaultFileName.
:rtype: unicode
"""
return self.__defaultFileName
@defaultFileName.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def defaultFileName(self, value):
"""
Setter for **self.__defaultFileName** attribute.
:param value: Attribute value.
:type value: unicode
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFileName"))
@defaultFileName.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFileName(self):
"""
Deleter for **self.__defaultFileName** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFileName"))
@property
def defaultFileExtension(self):
"""
Property for **self.__defaultFileExtension** attribute.
:return: self.__defaultFileExtension.
:rtype: unicode
"""
return self.__defaultFileExtension
@defaultFileExtension.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def defaultFileExtension(self, value):
"""
Setter for **self.__defaultFileExtension** attribute.
:param value: Attribute value.
:type value: unicode
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "defaultFileExtension"))
@defaultFileExtension.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultFileExtension(self):
"""
Deleter for **self.__defaultFileExtension** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultFileExtension"))
#******************************************************************************************************************
#*** Class methods.
#******************************************************************************************************************
def __initializeUi(self):
"""
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):
"""
Defines the slot triggered by the editor when document content changes.
"""
self.setTitle()
def __document__modificationChanged(self, changed):
"""
Defines the slot triggered by the editor when document is modified.
:param changed: File modification state.
:type changed: bool
"""
self.setTitle()
def __setDocumentSignals(self):
"""
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):
"""
Sets the editor title.
:param title: Editor title.
:type title: unicode
:return: Method success.
:rtype: bool
"""
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):
"""
Sets the editor file.
:param File: File to set.
:type File: unicode
:param isModified: File modified state.
:type isModified: bool
:param isUntitled: File untitled state.
:type isUntitled: bool
:return: Method success.
:rtype: bool
"""
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):
"""
Returns the current editor file short name.
:return: File short name.
:rtype: unicode
"""
if not self.__file:
return ""
return os.path.basename(self.__file)
[docs] def getUntitledFileName(self):
"""
Returns an untitled editor file name.
:return: Untitled file name.
:rtype: unicode
"""
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):
"""
Loads given document into the editor.
:param document: Document to load.
:type document: QTextDocument
:param file: File.
:type file: unicode
:param language: Editor language.
:type language: unicode
:return: Method success.
:rtype: bool
"""
document.setDocumentLayout(QPlainTextDocumentLayout(document))
self.setDocument(document)
self.setFile(file)
self.setLanguage(language)
self.__setDocumentSignals()
self.fileLoaded.emit()
return True
[docs] def newFile(self):
"""
Creates a new editor file.
:return: File name.
:rtype: unicode
"""
file = self.getUntitledFileName()
LOGGER.debug("> Creating '{0}' file.".format(file))
self.setFile(file, isModified=False, isUntitled=True)
self.__setDocumentSignals()
return file
# Oncilla: Statement commented by auto-documentation process:
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.FileExistsError)
[docs] def loadFile(self, file):
"""
Reads and loads given file into the editor.
:param File: File to load.
:type File: unicode
:return: Method success.
:rtype: bool
"""
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
# Oncilla: Statement commented by auto-documentation process:
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.FileExistsError)
[docs] def reloadFile(self, isModified=True):
"""
Reloads the current editor file.
:param isModified: File modified state.
:type isModified: bool
:return: Method success.
:rtype: bool
"""
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):
"""
Saves the editor file content.
:return: Method success.
:rtype: bool
"""
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):
"""
Saves the editor file content either using given file or user chosen file.
:return: Method success.
:rtype: bool
:note: 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):
"""
Writes the editor file content into given file.
:param file: File to write.
:type file: unicode
:return: Method success.
:rtype: bool
"""
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):
"""
Closes the editor file.
:return: Method success.
:rtype: bool
"""
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
else:
return False