# Copyright 2008 Nanorex, Inc. See LICENSE file for details.
"""
ProteinSequenceEditor.py
@copyright: 2008 Nanorex, Inc. See LICENSE file for details.
@version:$Id$
@author: Urmi
History:
Urmi copied this from DnaSequenceEditor.py and modified it to suit the
requirements of a protein sequence editor
"""
import foundation.env as env
import os
import re
import string
from PyQt4.Qt import SIGNAL
from PyQt4.Qt import QTextCursor, QRegExp
from PyQt4.Qt import QString
from PyQt4.Qt import QFileDialog
from PyQt4.Qt import QMessageBox
from PyQt4.Qt import QTextCharFormat, QBrush
from PyQt4.Qt import QRegExp
from PyQt4.Qt import QTextDocument
from dna.model.Dna_Constants import basesDict
from dna.model.Dna_Constants import getComplementSequence
from dna.model.Dna_Constants import getReverseSequence
from PM.PM_Colors import pmMessageBoxColor
from utilities.prefs_constants import workingDirectory_prefs_key
from protein.ProteinSequenceEditor.Ui_ProteinSequenceEditor import Ui_ProteinSequenceEditor
from utilities import debug_flags
from utilities.debug import print_compact_stack
from dna.model.Dna_Constants import MISSING_COMPLEMENTARY_STRAND_ATOM_SYMBOL
from utilities.constants import black, blue, darkblue, aqua, orange, darkorange
from utilities.constants import red, lightred_1, yellow, green, lightgray, olive
from utilities.constants import lightgreen_2, darkgreen, magenta, cyan, gray
from utilities.constants import navy, violet, pink, copper, brass, mustard, banana
class ProteinSequenceEditor(Ui_ProteinSequenceEditor):
"""
Creates a dockable sequence editor.
"""
validSymbols = QString(' <>~!@#%&_+`=$*()[]{}|^\'"\\.;:,/?')
sequenceFileName = None
currentPosition = 0
startPosition = 0
endPosition = 0
def __init__(self, win):
"""
Creates a dockable sequence editor
"""
Ui_ProteinSequenceEditor.__init__(self, win)
self.isAlreadyConnected = False
self.isAlreadyDisconnected = False
self._supress_textChanged_signal = False
self.connect_or_disconnect_signals(isConnect = True)
def connect_or_disconnect_signals(self, isConnect):
"""
Connect or disconnect widget signals sent to their slot methods.
This can be overridden in subclasses. By default it does nothing.
@param isConnect: If True the widget will send the signals to the slot
method.
@type isConnect: boolean
"""
#@see: BuildDna_PropertyManager.connect_or_disconnect_signals
#for a comment about these flags.
if isConnect and self.isAlreadyConnected:
if debug_flags.atom_debug:
print_compact_stack("warning: attempt to connect widgets"\
"in this PM that are already connected." )
return
if not isConnect and self.isAlreadyDisconnected:
if debug_flags.atom_debug:
print_compact_stack("warning: attempt to disconnect widgets"\
"in this PM that are already disconnected.")
return
self.isAlreadyConnected = isConnect
self.isAlreadyDisconnected = not isConnect
if isConnect:
change_connect = self.win.connect
else:
change_connect = self.win.disconnect
change_connect(self.loadSequenceButton,
SIGNAL("clicked()"),
self.openStrandSequenceFile)
change_connect(self.saveSequenceButton,
SIGNAL("clicked()"),
self.saveStrandSequence)
change_connect( self.sequenceTextEdit,
SIGNAL("textChanged()"),
self.sequenceChanged )
change_connect( self.sequenceTextEdit,
SIGNAL("cursorPositionChanged()"),
self.cursorPosChanged)
change_connect( self.findLineEdit,
SIGNAL("textEdited(const QString&)"),
self.findLineEdit_textEdited)
change_connect( self.findNextToolButton,
SIGNAL("clicked()"),
self.findNext)
change_connect( self.findPreviousToolButton,
SIGNAL("clicked()"),
self.findPrevious)
change_connect( self.replacePushButton,
SIGNAL("clicked()"),
self.replace)
def update_state(self, bool_enable = True):
"""
Update the state of this widget by enabling or disabling it depending
upon the flag bool_enable.
@param bool_enable: If True , enables the widgets inside the sequence
editor
@type bool_enable: boolean
"""
for widget in self.children():
if hasattr(widget, 'setEnabled'):
#The following check ensures that even when all widgets in the
#Sequence Editor docWidget are disabled, the 'close' ('x')
#and undock button in the top right corner are still accessible
#for the user. Using self.setEnabled(False) disables
#all the widgets including the corner buttons so that method
#is not used -- Ninad 2008-01-17
if widget.__class__.__name__ != 'QAbstractButton':
widget.setEnabled(bool_enable)
def sequenceChanged( self ):
"""
Slot for the Strand Sequence textedit widget.
Assumes the sequence changed directly by user's keystroke in the
textedit. Other methods...
"""
if self._supress_textChanged_signal:
return
self._supress_textChanged_signal = True
cursorPosition = self.getCursorPosition()
theSequence = self.getPlainSequence()
### Disconnect while we edit the sequence.
##self.disconnect( self.sequenceTextEdit,
##SIGNAL("textChanged()"),
##self.sequenceChanged )
# How has the text changed?
if theSequence.length() != 0:
self._updateSequenceAndItsComplement(theSequence)
### Reconnect to respond when the sequence is changed.
##self.connect( self.sequenceTextEdit,
##SIGNAL("textChanged()"),
##self.sequenceChanged )
#Urmi 20080715: need to update the sec structure text edit as well
secStrucSeq = self.secStrucTextEdit.toPlainText()
fixedPitchSequence = self.getFormattedSequence(str(secStrucSeq))
self.secStrucTextEdit.insertHtml(fixedPitchSequence)
self.synchronizeLengths()
self._supress_textChanged_signal = False
def getPlainSequence( self, inOmitSymbols = False ):
"""
Returns a plain text QString (without HTML stylization)
of the current sequence. All characters are preserved (unless
specified explicitly), including valid base letters, punctuation
symbols, whitespace and invalid letters.
@param inOmitSymbols: Omits characters listed in self.validSymbols.
@type inOmitSymbols: bool
@return: The current Protein sequence in the PM.
@rtype: QString
"""
outSequence = self.sequenceTextEdit.toPlainText()
outSequence = outSequence.toUpper()
if inOmitSymbols:
# This may look like a sloppy piece of code, but Qt's QRegExp
# class makes it pretty tricky to remove all punctuation.
theString = '[<>' \
+ str( QRegExp.escape(self.validSymbols) ) \
+ ']|-'
outSequence.remove(QRegExp( theString ))
return outSequence
def _updateSequenceAndItsComplement(self,
inSequence,
inRestoreCursor = True):
"""
Update the main strand sequence and its complement. (pribvate method)
Updating the complement sequence is done as explaned in the method
docstring of self._detemine_complementSequence()
Note that the callers outside the class call self.setSequence and
self.setComplementsequence but never call this method.
@see: self.setsequence() -- most portion (except for calling
self._determine_complementSequence() is copied over from
setSequence.
@see: self._updateSequenceAndItsComplement()
@see: self._determine_complementSequence()
"""
#@BUG: This method was mostly copied from self.setSequence, which in turn
#was copied over from old DnaGenerator. (so both methods have similar
#issues mentioned below)
#Apparently PM_TextEdit.insertHtml replaces the the whole
#sequence each time. This needs to be cleaned up. - Ninad 2007-04-10
cursor = self.sequenceTextEdit.textCursor()
cursorMate = self.secStrucTextEdit.textCursor()
selectionStart = cursor.selectionStart()
selectionEnd = cursor.selectionEnd()
seq = str(inSequence)
inSequence1 = self.convertProteinSequenceToColoredSequence(seq)
inSequence1 = self._fixedPitchSequence(inSequence1)
# Specify that theSequence is definitely HTML format, because
# Qt can get confused between HTML and Plain Text.
self.sequenceTextEdit.insertHtml( inSequence1 )
if inRestoreCursor:
cursor.setPosition(selectionStart, QTextCursor.MoveAnchor)
cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor)
self.sequenceTextEdit.setTextCursor( cursor )
cursorMate.setPosition( selectionStart, QTextCursor.MoveAnchor )
cursorMate.setPosition(selectionEnd, QTextCursor.KeepAnchor)
self.secStrucTextEdit.setTextCursor( cursorMate )
def setSequence( self,
inSequence,
inStylize = True,
inRestoreCursor = True
):
"""
Replace the current strand sequence with the new sequence text.
@param inSequence: The new sequence.
@type inSequence: QString
@param inStylize: If True, inSequence will be converted from a plain
text string (including optional symbols) to an HTML
rich text string.
@type inStylize: bool
@param inRestoreCursor: Not implemented yet.
@type inRestoreCursor: bool
@attention: Signals/slots must be managed before calling this method.
The textChanged() signal will be sent to any connected widgets.
@see: self.setsequence()
@see: self._updateSequenceAndItsComplement()
@see: self._determine_complementSequence()
"""
#@BUG: This method was mostly copied from old DnaGenerator
#Apparently PM_TextEdit.insertHtml replaces the the whole
#sequence each time. This needs to be cleaned up. - Ninad 2007-11-27
cursor = self.sequenceTextEdit.textCursor()
cursorMate = self.secStrucTextEdit.textCursor()
selectionStart = cursor.selectionStart()
selectionEnd = cursor.selectionEnd()
seq = str(inSequence)
inSequence1 = self.convertProteinSequenceToColoredSequence(seq)
#inSequence = "" + inSequence + ""
if inStylize:
inSequence1 = self._fixedPitchSequence(inSequence1)
#Urmi 20080714: Convert inSequence to colored text, each amino acid is
# shown by its own color
# Specify that theSequence is definitely HTML format, because
# Qt can get confused between HTML and Plain Text.
self.sequenceTextEdit.insertHtml( inSequence1)
if inRestoreCursor:
cursor.setPosition(selectionStart, QTextCursor.MoveAnchor)
cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor)
self.sequenceTextEdit.setTextCursor( cursor )
cursorMate.setPosition(selectionStart, QTextCursor.MoveAnchor)
cursorMate.setPosition(selectionEnd, QTextCursor.KeepAnchor)
self.secStrucTextEdit.setTextCursor( cursorMate )
return
def getFormattedSequence(self, inSequence):
colorList = ['Red','Blue', 'Green']
secStrucList = ['H','E', '-']
secStrucDict = dict(zip(secStrucList, colorList))
outSequence = ""
for i in range(len(inSequence)):
currentAA = inSequence[i]
color = secStrucDict[currentAA]
outSequence = outSequence + ""
outSequence = outSequence + currentAA + ""
#Now put html tags and make everything bold
fixedPitchSequence = "
tag is important to keep the fonts 'fixed pitch' i.e. #all the characters occupy the same size. This is important because #we have two text edits. (strand and Mate) the 'Mate' edit gets updated #as you type in letters in the 'StrandEdit' and these two should #appear to user as having equal lengths. fixedPitchSequence = "" + sequence fixedPitchSequence += " " return fixedPitchSequence def getSequenceLength( self ): """ Returns the number of characters in the strand sequence textedit widget. """ theSequence = self.getPlainSequence( inOmitSymbols = True ) outLength = theSequence.length() return outLength def getCursorPosition( self ): """ Returns the cursor position in the strand sequence textedit widget. """ cursor = self.sequenceTextEdit.textCursor() return cursor.position() def cursorPosChanged( self ): """ Slot called when the cursor position of the strand textEdit changes. When this happens, this method also changes the cursor position of the 'Mate' text edit. Because of this, both the text edit widgets in the Sequence Editor scroll 'in sync'. """ strandSequence = self.sequenceTextEdit.toPlainText() cursor = self.sequenceTextEdit.textCursor() cursor_mate = self.secStrucTextEdit.textCursor() if cursor_mate.position() != cursor.position(): cursor_mate.setPosition( cursor.position(), QTextCursor.MoveAnchor ) #After setting position, it is important to do setTextCursor #otherwise no effect will be observed. self.secStrucTextEdit.setTextCursor(cursor_mate) def synchronizeLengths( self ): """ Guarantees the values of the duplex length and strand length spinboxes agree with the strand sequence (textedit). @TODO: synchronizeLengths doesn't do anything for now """ ##self.updateStrandLength() ##self.updateDuplexLength() return def openStrandSequenceFile(self): """ Open (read) the user specified Strand sequence file and enter the sequence in the Strand sequence Text edit. Note that it ONLY reads the FIRST line of the file. @TODO: It only reads in the first line of the file. Also, it doesn't handle any special cases. (Once the special cases are clearly defined, that functionality will be added. """ #Urmi 20080714: should not this be only fasta file, for both load and save if self.parentWidget.assy.filename: odir = os.path.dirname(self.parentWidget.assy.filename) else: odir = env.prefs[workingDirectory_prefs_key] self.sequenceFileName = \ str(QFileDialog.getOpenFileName( self, "Load Strand Sequence", odir, "Strand Sequnce file (*.txt);;All Files (*.*);;")) lines = self.sequenceFileName try: lines = open(self.sequenceFileName, "rU").readlines() except: print "Exception occurred to open file: ", self.sequenceFileName return sequence = lines[0] sequence = QString(sequence) sequence = sequence.toUpper() self._updateSequenceAndItsComplement(sequence) def _writeStrandSequenceFile(self, fileName, strandSequence): """ Writes out the strand sequence (in the Strand Sequence editor) into a file. """ try: f = open(fileName, "w") except: print "Exception occurred to open file %s to write: " % fileName return None f.write(str(strandSequence)) f.close() def saveStrandSequence(self): """ Save the strand sequence entered in the Strand text edit in the specified file. """ if not self.sequenceFileName: sdir = env.prefs[workingDirectory_prefs_key] else: sdir = self.sequenceFileName fileName = QFileDialog.getSaveFileName( self, "Save Strand Sequence As ...", sdir, "Strand Sequence File (*.txt)" ) if fileName: fileName = str(fileName) if fileName[-4] != '.': fileName += '.txt' if os.path.exists(fileName): # ...and if the "Save As" file exists... # ... confirm overwrite of the existing file. ret = QMessageBox.warning( self, "Save Strand Sequence...", "The file \"" + fileName + "\" already exists.\n"\ "Do you want to overwrite the existing file or cancel?", "&Overwrite", "&Cancel", "", 0, # Enter == button 0 1 ) # Escape == button 1 if ret == 1: # The user cancelled return # write the current set of element colors into a file self._writeStrandSequenceFile( fileName, str(self.sequenceTextEdit.toPlainText())) # ==== Methods to support find and replace. # Should this (find and replace) be in its own class? -- Ninad 2007-11-28 def findNext(self): """ Find the next occurence of the search string in the sequence """ self._findNextOrPrevious() def findPrevious(self): """ Find the previous occurence of the search string in the sequence """ self._findNextOrPrevious(findPrevious = True) def _findNextOrPrevious(self, findPrevious = False): """ Find the next or previous matching string depending on the findPrevious flag. It also considers into account various findFlags user might have set (e.g. case sensitive search) @param findPrevious: If true, this method will find the previous occurance of the search string. @type findPrevious: boolean """ findFlags = QTextDocument.FindFlags() if findPrevious: findFlags |= QTextDocument.FindBackward if self.caseSensitiveFindAction.isChecked(): findFlags |= QTextDocument.FindCaseSensitively if not self.sequenceTextEdit.hasFocus(): self.sequenceTextEdit.setFocus() searchString = self.findLineEdit.text() cursor = self.sequenceTextEdit.textCursor() found = self.sequenceTextEdit.find(searchString, findFlags) #May be the cursor reached the end of the document, set it at position 0 #to redo the search. This makes sure that the search loops over as #user executes findNext multiple times. if not found: if findPrevious: sequence_QString = self.sequenceTextEdit.toPlainText() newCursorStartPosition = sequence_QString.length() else: newCursorStartPosition = 0 cursor.setPosition( newCursorStartPosition, QTextCursor.MoveAnchor) self.sequenceTextEdit.setTextCursor(cursor) found = self.sequenceTextEdit.find(searchString, findFlags) #Display or hide the warning widgets (that say 'sequence not found' #based on the boolean 'found' self._toggleWarningWidgets(found) def _toggleWarningWidgets(self, found): """ If the given searchString is not found in the sequence string, toggle the display of the 'sequence not found' warning widgets. Also enable or disable the 'Replace' button accordingly @param found: Flag that decides whether to sho or hide warning @type found: boolean @see: self.findNext, self.findPrevious """ if not found: self.findLineEdit.setStyleSheet(self._getFindLineEditStyleSheet()) self.phraseNotFoundLabel.show() self.warningSign.show() self.replacePushButton.setEnabled(False) else: self.findLineEdit.setStyleSheet("") self.phraseNotFoundLabel.hide() self.warningSign.hide() self.replacePushButton.setEnabled(True) def findLineEdit_textEdited(self, searchString): """ Slot method called whenever the text in the findLineEdit is edited *by the user* (and not by the setText calls). This is useful in dynamically searching the string as it gets typed in the findLineedit. """ self.findNext() #findNext sets the focus inside the sequenceTextEdit. So set it back to #to the findLineEdit to permit entering more characters. if not self.findLineEdit.hasFocus(): self.findLineEdit.setFocus() def replace(self): """ Find a string matching the searchString given in the findLineEdit and replace it with the string given in the replaceLineEdit. """ searchString = self.findLineEdit.text() replaceString = self.replaceLineEdit.text() sequence = self.sequenceTextEdit.toPlainText() #Its important to set focus on the sequenceTextEdit otherwise, #cursor.setPosition and setTextCursor won't have any effect if not self.sequenceTextEdit.hasFocus(): self.sequenceTextEdit.setFocus() cursor = self.sequenceTextEdit.textCursor() selectionStart = cursor.selectionStart() selectionEnd = cursor.selectionEnd() sequence.replace( selectionStart, (selectionEnd - selectionStart), replaceString ) #Move the cursor position one step back. This is important to do. #Example: Let the sequence be 'AAZAAA' and assume that the #'replaceString' is empty. Now user hits 'replace' , it deletes first #'A' . Thus the new sequence starts with the second A i.e. 'AZAAA' . # Note that the cursor position is still 'selectionEnd' i.e. cursor # position index is 1. #Now you do 'self.findNext' -- so it starts with cursor position 1 #onwards, thus missing the 'A' before the character Z. That's why #the following is done. cursor.setPosition((selectionEnd -1), QTextCursor.MoveAnchor) self.sequenceTextEdit.setTextCursor(cursor) #Set the sequence in the text edit. This could be slow. See comments #in self._updateSequenceAndItsComplement for more info. self._updateSequenceAndItsComplement(sequence) #Find the next occurance of the 'seqrchString' in the sequence. self.findNext()