character; whitespace
# is truncated from the end of HTML by default.
# Also, many symbol characters must be substituted
# because they confuse the HTML syntax.
#if str( outSequence[basePosition] ) in substituteDict:
if outSequence[basePosition] in substituteDict:
#theTag = substituteDict[theSeqChar]
theTag = substituteDict[ outSequence[basePosition] ]
outSequence = outSequence[:basePosition] \
+ theTag \
+ outSequence[basePosition + 1:]
basePosition += len(theTag) - 1
else:
# The sequence character is invalid (but permissible).
# Tags (e.g., and ) must be inserted at both the
# beginning and end of a segment of invalid characters.
if invalidSequence == False:
outSequence = outSequence[:basePosition] \
+ invalidStartTag \
+ outSequence[basePosition:]
basePosition += len(invalidStartTag)
invalidSequence = True
basePosition += 1
previousChar = theSeqChar
#basePosition += 1
# Specify that theSequence is definitely HTML format, because
# Qt can get confused between HTML and Plain Text.
#The 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.
outSequence = self._fixedPitchSequence(outSequence)
return outSequence
def _updateSequenceAndItsComplement(self,
inSequence,
inRestoreCursor = True):
"""
Update the main strand sequence and its complement. (private method)
Updating the complement sequence is done as explaned in the method
docstring of self._detemine_complementSequence()
Note that the callers outside the class must call self.updateSequence(),
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()
"""
#Find out complement sequence
complementSequence = self._determine_complementSequence(inSequence)
htmlSequence = self._colorExtraSequenceCharacters(inSequence)
htmlSequence = self._fixedPitchSequence(htmlSequence)
complementSequence = self._fixedPitchSequence(complementSequence)
# Get current cursor position before inserting inSequence.
if inRestoreCursor:
cursorPos = self.getCursorPosition()
else:
cursorPos = 0
# Specify that theSequence is definitely HTML format, because
# Qt can get confused between HTML and Plain Text.
self.sequenceTextEdit.insertHtml( htmlSequence ) #@@@ Generates signal???
self.sequenceTextEdit_mate.insertHtml(complementSequence)
self.setCursorPosition(inCursorPos = cursorPos)
return
def _determine_complementSequence(self, inSequence):
"""
Determine the complementary sequence based on the main sequence.
It does lots of thing than just obtaining the information by using
'getComplementSequence'.
Examples of what it does:
1. Suppose you have a duplex with 10 basepairs.
You are editing strand A and you lengthen it to create a sticky end.
Lets assume that it is lengthened by 5 bases. Since there will be no
complementary strand baseatoms for these, the sequence editor will show
an asterisk('*'), indicating that its missing the strand mate base atom.
Sequence Editor itself doesn't check each time if the strand mate is
missing. Rather, it relies on what the caller supplied as the initial
complement sequence. (@see: self._setComplementSequence) . The caller
determines the sequence of the strand being edited and also its complement.
If the complement doesn't exist, it replace the complement with a '*'
and passes this information to the sequence editor. Everytime sequence
editor is updating its sequnece, it updates the mate sequence and skips
the positions marked '*' (by using self._initial_complementSequence),
and thus those remain unaltered.
If user enters a sequence which has more characters than the original
sequence, then it doesn't update the complement portion of that
extra portion of the sequence. This gives a visual indication of
where the sequence ends. (see NFR bug 2787).
Reversing the sequence also reverses the complement (including the '*'
positions)
@see Bug 2787 for details of the implementation.
@see: self._setComplementSequence()
@see: self._updateSequenceAndItsComplement()
@see: self._setSequence()
@see: DnaStrand_PropertyManager.updateSequence() (the caller)
@see: Dna_Constants.MISSING_COMPLEMENTARY_STRAND_ATOM_SYMBOL
@see: DnaStrand.getStrandSequenceAndItsComplement()
"""
if not self._initial_complementSequence:
#This is unlikely. Do nothing in this case.
return ''
complementSequence = ''
#Make sure that the insequence is a string object
inSequence = str(inSequence)
#Do the following only when the length of sequence (inSequence) is
#greater than or equal to length of original complement sequence
#REVIEW This could be SLOW, need to think of a better way to do this.
if len(inSequence) >= len(self._initial_complementSequence):
for i in range(len(self._initial_complementSequence)):
if self._initial_complementSequence[i] == \
MISSING_COMPLEMENTARY_STRAND_ATOM_SYMBOL:
complementSequence += self._initial_complementSequence[i]
else:
complementSequence += getComplementSequence(inSequence[i])
else:
#Use complementSequence as a list object as we will need to modify
#some of the charactes within it. We can't do for example
#string[i] = 'X' as it is not permitted in python. So we will first
#treat the complementSequence as a list, do necessary modifications
#to it and then convert is back to a string.
#TODO: see if re.sub or re.subn can be used directly to replace
#some characters (the reason re.suib is not used here is that
#it does find and repalce for all matching patterns, which we don't
# want here )
complementSequence = list(self._initial_complementSequence)
for i in range(len(inSequence)):
if complementSequence[i] != MISSING_COMPLEMENTARY_STRAND_ATOM_SYMBOL:
complementSequence[i] = getComplementSequence(inSequence[i])
#Now there is additinal complementary sequence (because lenght of the
#main sequence provided is less than the 'original complementary
#sequence' (or the 'original main strand sequence) . In this case,
#for the remaining complementary sequence, we will use unassigned
#base symbol 'X' .
#Example: If user starts editing a strand,
#the initial strand sequence and its complement are shown in the
#sequence editor. Now, the user deletes some characters from the
#main sequence (using , e.g. backspace in sequence text edit to
#delete those), here, we will assume that this deletion of a
#character is as good as making it an unassigned base 'X',
#so its new complement will be of course 'X' --Ninad 2008-04-10
extra_length = len(complementSequence) - len(inSequence)
#do the above mentioned replacement
count = extra_length
while count > 0:
if complementSequence[-count] != MISSING_COMPLEMENTARY_STRAND_ATOM_SYMBOL:
complementSequence[-count] = 'X'
count -= 1
#convert the complement sequence back to a string
complementSequence = ''.join(complementSequence)
return complementSequence
def _setComplementSequence(self, complementSequence):
"""
Set the complement sequence field to I{complementSequence}. (private)
This is typically called immediately before or after calling
self._setSequence().
@param complementSequence: the complementary sequence determined by the
caller. This string may contain characters '*' which indicate
that there is a missing strand mate atom for the strand you are
editing. The Sequence Editorkeeps the '*' characters while
determining the compelment sequence (if the main sequence is
changed by the user)
@type complementSequence: str
See method docstring self._detemine_complementSequence() for more
information.
@see: self.updateSequence()
@see: self._setSequence()
@see: self._updateSequenceAndItsComplement()
@see: self._determine_complementSequence()
@see: DnaStrand_PropertyManager.updateSequence()
"""
self._initial_complementSequence = complementSequence
complementSequence = self._fixedPitchSequence(complementSequence)
self.sequenceTextEdit_mate.insertHtml(complementSequence)
return
def _setSequence( self,
inSequence,
inStylize = True,
inRestoreCursor = True
):
"""
Replace the current strand sequence with the new sequence text.
(private method)
This is typically called immediately before or after calling
self._setComplementSequence().
@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: Restores cursor to previous position.
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.updateSequence()
@see: self._setComplementSequence()
@see: self._updateSequenceAndItsComplement()
@see: self._determine_complementSequence()
"""
if inStylize:
#Temporary fix for bug 2604--
#Temporarily disabling the code that 'stylizes the sequence'
#that code is too slow and takes a long time for a file to load.
# Example: NE1 hangs while loading M13 sequence (8kb file) if we
#stylize the sequence .-- Ninad 2008-01-22
##inSequence = self.stylizeSequence( inSequence )
# Color any overhang sequence characters gray.
htmlSequence = self._colorExtraSequenceCharacters(inSequence)
#Make the sequence 'Fixed pitch'.
htmlSequence = self._fixedPitchSequence(htmlSequence)
# Specify that theSequence is definitely HTML format, because
# Qt can get confused between HTML and Plain Text.
self._suppress_textChanged_signal = True
self.sequenceTextEdit.insertHtml( htmlSequence )
self._suppress_textChanged_signal = False
self.setCursorPosition(0)
return
def clear(self):
"""
Clear the sequence and mate fields.
"""
if 0:
print "Cleared"
self.sequenceTextEdit.insertHtml("")
self.sequenceTextEdit_mate.insertHtml("")
return
def _colorExtraSequenceCharacters(self, inSequence):
"""
Returns I{inSequence} with html tags that color any extra overhang
characters gray.
@param inSequence: The sequence.
@type inSequence: QString
@return: inSequence with the html tags to color any overhang characters.
@rtype: string
"""
strandLength = self.current_strand.getNumberOfBases()
if len(inSequence) <= strandLength:
return inSequence
sequence = inSequence[:strandLength]
overhang = inSequence[strandLength:]
return sequence + "" + overhang + ""
def _fixedPitchSequence(self, sequence):
"""
Make the sequence 'fixed-pitched' i.e. width of all characters
should be constance
"""
#The 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 updateSequence(self, strand = None, cursorPos = -1):
"""
Updates and shows the sequence editor with the sequence of I{strand}.
This is the main (public) method to call to update the sequence editor.
@param strand: the strand. If strand is None (default), update the
sequence of the current strand (i.e. self.current_strand).
@type strand: DnaStrand
@param cursorPos: the position in the sequence in which to place the
cursor. If cursorPos is negative, the cursor position
is placed at the end of the sequence (default).
@type cursorPos: int
"""
if strand == " ":
self.current_strand = None
self.clear()
return
if strand:
assert isinstance(strand, self.win.assy.DnaStrand)
self.current_strand = strand
else:
# Use self.current_strand. Make sure it's not None (as a precaution).
assert isinstance(self.current_strand, self.win.assy.DnaStrand)
sequence, complementSequence = \
self.current_strand.getStrandSequenceAndItsComplement()
if sequence:
sequence = QString(sequence)
sequence = sequence.toUpper()
#Set the initial sequence (read in from the file)
self._setSequence(sequence)
#Set the initial complement sequence for DnaSequence editor.
#do this independently because 'complementSequenceString' may have
#some characters (such as * ) that denote a missing base on the
#complementary strand. This information is used by the sequence
#editor. See DnaSequenceEditor._determine_complementSequence()
#for more details. See also bug 2787
self._setComplementSequence(complementSequence)
else:
msg = "DnaStrand '%s' has no sequence." % self.current_strand.name
print_compact_traceback(msg)
self._setSequence(msg)
self._setComplementSequence("")
# Set cursor position.
self.setCursorPosition(cursorPos)
# Update the bg color to white.
self._sequence_changed = False
self._previousSequence = sequence
self._updateSequenceBgColor()
# Update window title with name of current protein.
titleString = 'Sequence Editor for ' + self.current_strand.name
self.setWindowTitle(titleString)
if not self.isVisible():
#Show the sequence editor if it isn't visible.
#ATTENTION: the sequence editor will (temporarily) close the
#Reports dockwidget (if it is visible). The Reports dockwidget
#is restored when the sequence Editor is closed.
self.show()
return
def setCursorPosition(self, inCursorPos = -1):
"""
Set the cursor position to I{inCursorPos} in the sequence textedit widget.
@param inCursorPos: the position in the sequence in which to place the
cursor. If cursorPos is negative, the cursor position
is placed at the end of the sequence (default).
@type inCursorPos: int
"""
# Make sure cursorPos is in the valid range.
if inCursorPos < 0:
cursorPos = self.getSequenceLength()
anchorPos = self.getSequenceLength()
elif inCursorPos >= self.getSequenceLength():
cursorPos = self.getSequenceLength()
anchorPos = self.getSequenceLength()
else:
cursorPos = inCursorPos
anchorPos = inCursorPos
# Useful print statements for debugging.
#print "setCursorPosition(): Sequence=", self.getPlainSequence()
#print "setCursorPosition(): Final inCursorPos=%d\ncursorPos=%d, anchorPos=%d" % (inCursorPos, cursorPos, anchorPos)
# Finally, set the cursor position in the sequence.
cursor = self.sequenceTextEdit.textCursor()
cursor.setPosition(anchorPos, QTextCursor.MoveAnchor)
cursor.setPosition(cursorPos, QTextCursor.KeepAnchor)
self.sequenceTextEdit.setTextCursor( cursor )
return
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()
strandSequence_mate = self.sequenceTextEdit_mate.toPlainText()
#The cursorChanged signal is emitted even before the program enters
#_setSequence() (or before the 'textChanged' signal is emitted)
#So, simply return if the 'Mate' doesn't have same number of characters
#as the 'Strand text edit' (otherwise it will print warning message
# while setting the cursor_mate position later in the method.
if strandSequence.length() != strandSequence_mate.length():
return
cursor = self.sequenceTextEdit.textCursor()
cursor_mate = self.sequenceTextEdit_mate.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.sequenceTextEdit_mate.setTextCursor(cursor_mate)
return
def synchronizeLengths( self ):
"""
Guarantees the values of the duplex length and strand length
spinboxes agree with the strand sequence (textedit).
@note: 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.
"""
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)
return
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()
return
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()))
return
# ==== 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()
return
def findPrevious(self):
"""
Find the previous occurence of the search string in the sequence
"""
self._findNextOrPrevious(findPrevious = True)
return
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)
return
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)
return
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()
return
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()
return