Source code for schrodinger.ui.sequencealignment.sequence_viewer

"""
This class implements a sequence viewer widget.

The sequence viewer widget is a QSplitter object that includes three
child widgets: tree area, name area and sequence area.

Copyright Schrodinger, LLC. All rights reserved.
"""

# Contributors: Piotr Rotkiewicz

import os
import pickle
from past.utils import old_div

from schrodinger import get_maestro
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtPrintSupport
from schrodinger.Qt import QtWidgets

# These are common dialogs, they can also work in text mode
from . import align
from . import constants
from . import dialogs
from . import fileio
from . import jobs
from . import maestro as maestro_helpers
from . import predictors
from . import prime
from . import sequence
from . import sequence_group
# Import external dialogs and panels
from .align_gui import AlignmentSettingsDialog
from .analyze_gui import analyzeBindingSiteDialog
from .associate_gui import AssociateEntryPanel
from .blast_gui import createBlastResultsDialog
from .blast_gui import createBlastSettingsDialog
from .compare_sequences_gui import CompareSequencesDialog
from .contact_map import ContactMap
from .jobs_gui import createJobProgressDialog
from .jobs_gui import createJobSettingsDialog
from .name_area import NameArea
from .prime_gui import primeValidateBuildMode
from .prime_gui import setPrimeSequenceGroup
from .prime_gui import showPrimeSettingsDialog
from .prime_gui import updatePrimeQueryList
from .sequence_area import SequenceArea
from .sequence_viewer_gui import RemoteQueryDialog
from .sequence_viewer_gui import RemoveRedundancyDialog
from .sequence_viewer_gui import RenumberResiduesDialog
from .sequence_viewer_gui import SequenceEditorDialog
from .sequence_viewer_gui import WeightColorsDialog
from .tree_area import TreeArea
from .undo import UndoStack

maestro = get_maestro()

try:
    from schrodinger.application.prime.packages.antibody import \
        SeqType as PspSeqType
except:
    PspSeqType = None

# The SequenceViewer is a container for the name area, sequence area
# and tree area.


class SequenceViewer(QtWidgets.QSplitter):
    """
    The sequence viewer widget displays and manipulates sequences and
    associated data. The widget is a QSplitter that includes tree coupled
    areas: sequence area, name area and tree area.
    """

    def __init__(self, parent=None):

        QtWidgets.QSplitter.__init__(self, parent)  # Initialize base class.
        self.setFrameStyle(QtWidgets.QFrame.Panel |
                           QtWidgets.QFrame.Sunken)  #of widget.
        self.sequence_group = sequence_group.SequenceGroup()
        self.sequence_group_list = []  #in current project.

        #: Create tree area widget
        self.tree_area = TreeArea()
        self.tree_area.setViewer(self)  #Tree area's parent viewer.
        self.addWidget(self.tree_area)

        # Create name area widget.
        self.name_area = NameArea()
        self.name_area.setViewer(self)  #Name area's parent viewer.
        self.addWidget(self.name_area)

        # Create sequence area widget.
        self.sequence_area = SequenceArea()
        self.sequence_area.setViewer(self)  #Sequence area's parent viewer.
        self.addWidget(self.sequence_area)

        # Initially, tree area not visible (resized to 0).
        self.setSizes([0, 150, 400])  #Of children widgets.
        self.rows = []
        self.background_color = QtGui.QColor(QtCore.Qt.white)
        self.inv_background_color = QtGui.QColor(QtCore.Qt.black)  #inverted
        self.font_size = 12
        self.font_width = 6
        self.font_height = 12
        self.font_xheight = 5
        self.zoom_factor = 1.0  #horizontal zoom factor.
        self.cell_width = 12

        self.wrapped = False  #Wrapping mode.
        self.wrapped_width = None  #Max width of wrapped area.
        self.chain_name_only = False  #sequence name format.
        self.left_column = 0  #position, used in unwrapped mode.
        self.top_row = 0  #position, used in wrapped mode.
        self.max_columns = 0
        self.separator_scale = 100  #of row percentage. (0 disables separator)
        self.crop_image = False  #crop output image to minimize margins.
        self.mode = constants.MODE_GRAB_AND_DRAG  #Current sequence viewer operating mode.
        self.has_tooltips = True  #Tool tips enabled.
        # If this flag is enabled, the sequence right to mouse cursor position
        # will be locked during slide or grab-and-drag operations.
        self.lock_downstream = True  #lock downstream sequences.
        self.sequence_group.color_mode = constants.COLOR_SIDECHAIN_CHEMISTRY
        self.auto_color = False  #switches to white for dark background.
        self.margin = 3  #size
        self.has_ruler = True
        self.average_columns = False  #avg colors in columns
        self.weight_colors = False  #by alignment strength
        self.weight_colors_by_identity = False  #by sequence identity
        self.weight_colors_by_difference = False  #by sequence difference
        #: Pad alignment with gaps. If enabled, all sequences will be padded
        #: with gaps so that they all have identical lengths.
        self.padded = True
        self.use_mouse_across = False  #across rows
        self.name_popup_menu = None
        self.tree_popup_menu = None
        self.sequence_popup_menu = None
        self.mutate = False  #as type
        #: State stack for undo/redo.
        self.undo_stack = UndoStack()

        #: Display percentange identity on a right side of the panel.
        self.display_identity = False
        self.display_similarity = False
        self.display_homology = False
        self.display_score = False
        #: Update flags
        self.contents_changed = False
        self.alignment_changed = False
        self.selection_changed = False
        self.display_dots = False  #If True, identities display as dots.
        # Fonts
        self.original_font = None
        self.header_font = None
        self.ruler_font = None
        self.bold_font = None
        self.italic_font = None
        # Displays
        self.display_boundaries = False
        self.group_annotations = False
        self.min_weight_identity = 20.0
        self.max_weight_identity = 100.0
        self.statistics_status_bar = None
        self.message_status_bar = None
        self.use_colors = True
        self.set_constraints = False
        self.set_query_constraints = False
        # Syncing
        self.auto_synchronize = True  #with Maestro.
        self.auto_profile = True  #automatically calculate internal seq profile.
        self.build_mode = False
        self.last_index = 0

        # Use Courier.
        # @note: this is not working properly on Ubuntu.
        font = QtGui.QFont("Courier", self.font_size)
        font.setStyleHint(QtGui.QFont.Courier)
        if not font.exactMatch():
            # Workaround for Windows.
            font = QtGui.QFont("Courier New", self.font_size)
        if not font.exactMatch():
            # Workaround for Ubuntu.
            font = QtGui.QFont("Courier 10 Pitch", self.font_size)
        self.setFont(font)

        # Use Courier.
        # @note: this is not working properly on Ubuntu.
        self.bold_font = QtGui.QFont("Courier", self.font_size)
        self.bold_font.setStyleHint(QtGui.QFont.Courier)
        if not self.bold_font.exactMatch():
            # Workaround for Windows.
            self.bold_font = QtGui.QFont("Courier New", self.font_size)
        if not self.bold_font.exactMatch():
            # Workaround for Ubuntu.
            self.bold_font = QtGui.QFont("Courier 10 Pitch", self.font_size)
        self.bold_font.setBold(True)

        # Use Courier.
        # @note: this is not working properly on Ubuntu.
        self.italic_font = QtGui.QFont("Courier", self.font_size)
        self.italic_font.setStyleHint(QtGui.QFont.Courier)
        if not self.italic_font.exactMatch():
            # Workaround for Windows.
            self.italic_font = QtGui.QFont("Courier New", self.font_size)
        if not self.italic_font.exactMatch():
            # Workaround for Ubuntu.
            self.italic_font = QtGui.QFont("Courier 10 Pitch", self.font_size)
        self.italic_font.setItalic(True)

        self.job_progress_dialog = createJobProgressDialog("Job Progress",
                                                           parent=self)
        self.remote_query_dialog = RemoteQueryDialog(self)
        self.alignment_settings_dialog = AlignmentSettingsDialog(self)

        self.always_ask_action = None
        self.query_tabs = None
        self.has_header_row = False
        self.last_project_path = ""  #Path to currently open project.
        self.save_state = False  #current state to Maestro proj when MSV closes.

        dialogs.set_parent_widget(self)
        self.updateFontSize(self.font_size)

        self.command_dict = None
        #: Callbacks
        self.cb_residue_selection_changed = None
        self.cb_sequence_selection_changed = None
        self.cb_contents_changed = None

        self.maestro_busy = False
        self.blast_settings_dialog = None
        self.blast_results_dialog = None
        self.use_maestro_entry_title = True  #as seq name
        self.incorporate_scratch_entry = False
        self.update_annotations_menu = False  #parent's annotations menu
        self.feedback_label = None  #QLabel widget
        self.auto_resize = False  #name area on mouse over
        self.associate_dialog = None  #Entry dialog
        self.hide_empty_lines = False

        #: Global job settings
        self.job_settings = {
            'command_line': ['-HOST', 'localhost'],
            'keep_files': False,
            'temporary_dir': True
        }
        self.ready_to_save = True  #Set after updateView, reset by proj export.
        self.compare_sequences_dialog = None

    def setPadded(self, padded):
        """
        Toggles alignment padding.

        :type padded: bool
        :param padded: If True, enable padded mode, False - disable it.
        """
        self.padded = padded
        self.updateView()

    def setMouseAcross(self, enabled):
        """
        Toggles use mouse across rows mode.

        :type enabled: bool
        :param enabled: if True, enable "mouse across rows" mode, otherwise
            disable it
        """
        self.use_mouse_across = enabled

    def setBackgroundColor(self, color=(255, 255, 255)):
        """
        Sets a background color.

        :type color: (int, int, int)
        :param color: Background color RGB tuple.
        """
        r, g, b = color
        self.background_color = QtGui.QColor(r, g, b)
        self.sequence_group.background_color = color
        self.inv_background_color = QtGui.QColor(QtCore.Qt.black)
        if old_div((r + g + b), 3) < 127:
            self.inv_background_color = QtGui.QColor(QtCore.Qt.white)
        self.sequence_group.inv_background_color = (
            self.inv_background_color.red(), self.inv_background_color.green(),
            self.inv_background_color.blue())
        self.sequence_group.updateVariableSequences()
        self.sequence_area.cacheSSAImages()
        self.updateView(update_colors=True)

    def updateFontSize(self, size=None):
        """
        Updates sequence viewer font size in all child widgets. Recomputes
        all font size dependent variables.

        :type size: int
        :param size: new font size
        """
        if size is None:
            size = self.font_size
        else:
            self.font_size = size

        # Use Courier.
        # @note: this is not working properly on Ubuntu.
        font = QtGui.QFont("Courier", self.font_size)
        font.setStyleHint(QtGui.QFont.Courier)
        if not font.exactMatch():
            # Workaround for Windows.
            font = QtGui.QFont("Courier New", self.font_size)
        if not font.exactMatch():
            # Workaround for Ubuntu.
            font = QtGui.QFont("Courier 10 Pitch", self.font_size)
        font.setPointSize(size)
        self.setFont(font)

        # Use Courier.
        # @note: this is not working properly on Ubuntu.
        self.bold_font = QtGui.QFont("Courier", self.font_size)
        self.bold_font.setStyleHint(QtGui.QFont.Courier)
        if not self.bold_font.exactMatch():
            # Workaround for Windows.
            self.bold_font = QtGui.QFont("Courier New", self.font_size)
        if not self.bold_font.exactMatch():
            # Workaround for Ubuntu.
            self.bold_font = QtGui.QFont("Courier 10 Pitch", self.font_size)
        self.bold_font.setBold(True)
        font.setStyleHint(QtGui.QFont.Courier)
        font_metrics = QtGui.QFontMetrics(font)
        self.bold_font.setPointSize(size)

        # Calculate font width.
        self.font_width = font_metrics.horizontalAdvance(' ') + 1
        self.font_height = font_metrics.overlinePos() + \
            font_metrics.underlinePos()
        self.font_vertical_offset = font_metrics.underlinePos() - \
                                                self.font_height - 2
        self.font_xheight = 0.75 * font_metrics.xHeight()
        self.cell_width = self.zoom_factor * self.font_width
        if self.cell_width < 1.0:
            self.cell_width = 1.0
        self.cell_width = int(self.cell_width)

        height = self.height() - self.margin
        if self.has_header_row:
            height -= 20

        self.max_columns = self.maxColumns()
        self.setFont(font)

        # Set new font for all children,
        self.name_area.setFont(font)
        self.sequence_area.setFont(font)
        self.tree_area.setFont(font)

        # Remember the original font size with default charactr spacing.
        self.original_font = QtGui.QFont(self.font())
        self.original_font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, 0.0)
        self.font().setLetterSpacing(QtGui.QFont.AbsoluteSpacing, 1.0)
        self.header_font = QtGui.QFont("Arial")
        self.header_font.setPointSize(12)

        # This is an ugly hack to have the font actually rebuilt. Otherwise,
        # Qt will not honor the letter spacing settings.
        self.original_font.setPointSize(self.original_font.pointSize() - 1)

        # Create a slightly smaller font for the ruler row.
        self.ruler_font = QtGui.QFont(self.font())
        size = self.ruler_font.pointSize() - 3
        if size < 8:
            size = 8
        self.ruler_font.setPointSize(size)
        self.sequence_area.cacheSSAImages()

        self.updateView()

    def setWrapped(self, wrapped):
        """
        This method toggles sequence wrapping mode.

        :type wrapped: bool
        :param wrapped: if True, enable wrapped mode, otherwise - disable it
        """
        self.wrapped = wrapped
        if not self.wrapped:
            self.sequence_area.horizontal_scroll_bar.show()
            self.sequence_area.horizontal_scroll_bar.setValue(0)
            self.sequence_area.vertical_scroll_bar.setValue(0)
            self.left_column = 0
            self.top_row = 0
        else:
            self.sequence_area.horizontal_scroll_bar.hide()
            self.sequence_area.vertical_scroll_bar.setValue(0)
            self.top_row = 0
        self.updateView()

    def hideSelectedSequences(self):
        """
        Hides all selected sequences.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Hide Selected Sequences")
        self.sequence_group.hideSelected()
        self.contents_changed = True
        self.updateView()

    def deleteSelectedSequences(self):
        """
        Deletes all selected sequences.

        :rtype: bool
        :return: True if all sequences were removed
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Delete Selected Sequences")
        self.excludeSelected()
        result = self.sequence_group.deleteSelected()
        self.sequence_area.vertical_scroll_bar.setValue(0)
        self.contents_changed = True
        if not self.sequence_group.sequences:
            self.sequence_group.user_annotations = []
        self.updateView()
        return result

    def showAllSequences(self):
        """
        Shows all sequences.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Show All Sequences")
        self.sequence_group.showAll()
        self.contents_changed = True
        self.updateView()

    def fillGaps(self):
        """
        Fill a selected region with gaps.
        """
        self.sequence_group.fillGaps()
        self.contents_changed = True
        self.updateView()

    def removeGaps(self):
        """
        Removes all gaps.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Remove Gaps")
        self.sequence_group.removeAllGaps()
        self.alignment_changed = True
        self.updateView()

    def deleteSelectedResidues(self):
        """
        Deletes all selected residues.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Delete Selected Residues")
        if self.mutate:
            asl = maestro_helpers.maestroGetSelectedResiduesASL(self)
            if asl:
                try:
                    maestro.command("delete " + asl)
                except:
                    pass
        self.sequence_group.deleteSelectedResidues()
        self.selection_changed = True
        self.updateView()

    def invertSelection(self):
        """
        Inverts residue selection.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Invert Selection")
        self.sequence_group.invertSelection()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def deselectAll(self):
        """
        Deselects all residues.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Deselect All")
        self.sequence_group.deselectAll()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def selectAll(self):
        """
        Selects all residues.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Select All")
        self.sequence_group.selectAll()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def minimizeAlignment(self):
        """
        Removes all gaps.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Remove Empty Columns")
        self.sequence_group.minimizeAlignment()
        self.contents_changed = True
        self.updateView()

    def lockGaps(self):
        """
        Locks gaps.
        """
        self.sequence_group.lockGaps()
        self.update()

    def unlockGaps(self):
        """
        Unlocks gaps.
        """
        self.sequence_group.unlockGaps()
        self.update()

    def selectAlignedBlocks(self):
        """
        Selects aligned blocks (the sequence regions without gaps).
        """
        self.sequence_group.selectAlignedBlocks()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def selectStructureBlocks(self):
        """
        Selects blocks that have structure.
        """
        self.sequence_group.selectStructureBlocks()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def selectIdentities(self):
        """
        Selects identical residues in columns.
        """
        self.sequence_group.selectIdentities()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def setHasTooltips(self, has_tooltips):
        """
        Toggles mouse hover tooltips.

        :type has_tooltips: bool
        :param has_tooltips: if True, enable tooltips, if False -disable them
        """
        self.has_tooltips = has_tooltips

    def setHasHeaderRow(self, has_header_row):
        """
        Toggles header row.

        :type has_tooltips: bool
        :param has_tooltips: If True, enable the header row,
                             if False - disable it
        """
        self.has_header_row = has_header_row
        self.update()

    def setHasRuler(self, has_ruler):
        """
        Toggles the ruler.

        :type has_ruler: bool
        :param has_ruler: if True - enable the ruler, otherwise - disable it.
        """
        self.has_ruler = has_ruler
        self.updateView()

    def deleteAnnotations(self):
        """
        Deletes all annotations.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Delete Annotations")
        self.sequence_group.deleteAnnotations()
        self.contents_changed = True
        self.updateView()

    def deletePredictions(self):
        """
        Deletes all predictions.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Delete Predictions")
        self.sequence_group.deletePredictions()
        self.contents_changed = True
        self.updateView()

    def loadFile(self,
                 file_name,
                 merge=False,
                 replace=False,
                 to_maestro=False,
                 translate=False,
                 new_list=None,
                 maestro_include=True):
        """
        Loads a sequence file and merges the read sequences with current
        sequence viewer contents.

        :note: This is a default behavior. To replace current contents with
            new sequences, clear the sequence viewer contents and then load
            the file.

        :type file_name: str
        :param file_name: name of the file to be read.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Load File")
        if maestro and to_maestro:
            name, ext = os.path.splitext(str(file_name))
            from_maestro = (ext == ".mae") or (ext == ".maegz") \
                or (file_name[-7:] == ".mae.gz")
            if from_maestro or \
               (to_maestro and ((ext == ".pdb") or (ext == ".ent"))):
                if from_maestro:
                    tmp_status = self.auto_synchronize
                    self.auto_synchronize = False
                    current_entries = maestro_helpers.maestroGetListOfEntryIDs()
                    wsreplace = maestro.get_command_option(
                        "entryimport", "wsreplace")
                    wsinclude = maestro.get_command_option(
                        "entryimport", "wsinclude")
                    format = maestro.get_command_option("entryimport", "format")
                    if maestro_include:
                        maestro.command("entryimport "
                                        " wsreplace=false format=maestro " +
                                        " \"" + str(file_name) + "\"")
                    else:
                        maestro.command("entryimport "
                                        " wsreplace=false wsinclude=none "
                                        "format=maestro " + " \"" +
                                        str(file_name) + "\"")
                    # Revert to original Maestro command settings.
                    maestro.command("entryimport" + " wsreplace=" + wsreplace +
                                    " wsinclude=" + wsinclude + " format=" +
                                    format)
                    self.auto_synchronize = True
                    maestro_helpers.maestroIncorporateEntries(
                        self.sequence_group,
                        what="all",
                        ignore=current_entries,
                        include=to_maestro,
                        align_func=jobs.pdb_align_structure_to_sequence,
                        use_title=self.use_maestro_entry_title,
                        viewer=self)
                    self.auto_synchronize = tmp_status
                    result = True
                    if new_list is not None:
                        for seq in self.sequence_group.sequences:
                            if seq.maestro_entry_id and \
                            seq.maestro_entry_id not in current_entries:
                                new_list.append(seq)
                else:
                    result = jobs.incorporatePDB(
                        self.sequence_group,
                        file_name,
                        maestro_include=maestro_include,
                        new_list=new_list,
                        viewer=self)
                self.contents_changed = True
                self.updateView()
                return result

        tmp_group = sequence_group.SequenceGroup()
        result = fileio.load_file(
            tmp_group,
            file_name,
            align_func=jobs.pdb_align_structure_to_sequence)
        if result:
            new_sequence_list = tmp_group.sequences
            if replace:
                new_sequence_list = []
                for seq in tmp_group.sequences:
                    found = False
                    for original in self.sequence_group.sequences:
                        if original.gaplessLength() == seq.gaplessLength() and\
                         original.compare(seq) == 1.0:
                            found = True
                            seq.residues = original.propagateGaps(
                                seq, parent_sequence=seq)
                            seq.propagateGapsToChildren()
                            self.sequence_group.sequences[
                                self.sequence_group.sequences.index(
                                    original)] = seq
                            break
                    if not found:
                        new_sequence_list.append(seq)
            if translate:
                for seq in new_sequence_list:
                    if seq.isDNA():
                        seq.translateDNA()
            self.sequence_group.sequences += new_sequence_list
            if new_list is not None:
                new_list += new_sequence_list
            if merge:
                last_sequence = None
                for seq in self.sequence_group.sequences:
                    if seq.type == constants.SEQ_AMINO_ACIDS:
                        last_sequence = seq
                if last_sequence and self.sequence_group.reference:
                    align.align(self.sequence_group.reference,
                                last_sequence,
                                scoring_matrix=constants.BLOSUM62,
                                gap_open_penalty=-10,
                                gap_extend_penalty=-1,
                                merge=True,
                                sequence_group=self.sequence_group)
        self.contents_changed = True
        self.alignment_changed = True
        self.updateView()
        return result

    def saveFile(self,
                 file_name,
                 save_annotations=False,
                 selected_only=False,
                 save_similarity=False,
                 format="FASTA"):
        """
        Saves sequences to a file.

        :type file_name: str
        :param file_name: name of the output file

        :rtype: bool
        :return: True if file successfully saved, False otherwise.
        """
        try:
            name, ext = os.path.splitext(file_name)
        except:
            ext = ""
        if format.startswith("FAS"):
            if not ext:
                file_name += ".fasta"
            return fileio.save_fasta_file(self.sequence_group,
                                          file_name,
                                          save_annotations=save_annotations,
                                          save_similarity=save_similarity,
                                          selected_only=selected_only)
        elif format.startswith("T"):
            if not ext:
                file_name += ".txt"
            return fileio.save_fasta_file(self.sequence_group,
                                          file_name,
                                          save_annotations=save_annotations,
                                          selected_only=selected_only,
                                          save_similarity=save_similarity,
                                          as_text=True)

    def setColorMode(self, color_mode):
        """
        Colors sequences using a specified color mode.

        :type color_mode: int
        :param color_mode: Color mode used to color the sequences.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Change Color Mode")
        self.sequence_group.color_mode = color_mode
        self.sequence_group.colorSequences(self.sequence_group.color_mode)
        self.updateView(update_colors=True)

    def colorSequences(self, color=None):
        """
        Sets arbitrary color to the sequences.
        """
        self.sequence_group.color_mode = constants.COLOR_CUSTOM
        if color:
            self.sequence_group.custom_color = color
        self.sequence_group.colorSequences(self.sequence_group.color_mode,
                                           color=color)
        self.updateView(update_colors=True)

    def propagateColors(self):
        """
        Propagates colors to Maestro workspace.
        """
        for seq in self.sequence_group.sequences:
            if seq.isValidProtein() and seq.from_maestro:
                maestro_helpers.propagateColorsToMaestro(self, seq)

    def clearSequences(self):
        """
        Removes all sequences and updates the viewer.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="New Project")
        self.sequence_group.clear()
        self.contents_changed = True
        self.updateView()

    def expandAllSequences(self):
        """
        Expands all sequences.
        """
        self.sequence_group.showAllChildren()
        self.updateView()

    def collapseAllSequences(self):
        """
        Collapses all sequences.
        """
        self.sequence_group.hideAllChildren()
        self.updateView()

    def addConsensus(self, toggle=False, update=True):
        """
        Adds a consensus annotation sequence.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Add Consensus Sequence")
        self.sequence_group.addConsensusSequence(toggle=toggle)
        if update:
            self.contents_changed = True
            self.updateView()

    def addSymbols(self, toggle=False, update=True):
        """
        Adds a symbols sequence.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Add Consensus Symbols")
        self.sequence_group.addConsensusSymbols(toggle=toggle)
        if update:
            self.contents_changed = True
            self.updateView()

    def toggleHistory(self):
        """
        Toggles changes tracking feature.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Track Changes")
        self.sequence_group.toggleHistory()
        self.updateView()

    def resetHistory(self):
        """
        Toggles changes tracking feature.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Reset History")
        self.sequence_group.resetHistory()
        self.updateView()

    def addMeanHydrophobicity(self, toggle=False, update=True):
        """
        Adds a mean hydrophobicity annotation.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Add Mean Hydrophobicity")
        self.sequence_group.addMeanHydrophobicity(toggle=toggle)
        if update:
            self.contents_changed = True
            self.updateView()

    def addMeanPI(self, toggle=False, update=True):
        """
        Adds a mean isoelectric point annotation.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Add Mean PI")
        self.sequence_group.addMeanPI(toggle=toggle)
        if update:
            self.contents_changed = True
            self.updateView()

    def addSequenceLogo(self, toggle=False, update=True):
        """
        Adds a sequence logo annotation.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Add Sequence Logo")

        self.sequence_group.addSequenceLogo(toggle=toggle)
        if update:
            self.contents_changed = True
            self.updateView()

    def zoomIn(self):
        """
        Increases zoom factor and updates sequence viewer contents.
        """
        if self.zoom_factor < 1.0:
            self.zoom_factor += 0.2
        else:
            self.zoom_factor += 1.0
        if self.zoom_factor > 4.0:
            self.zoom_factor = 4.0
        self.updateFontSize(self.font().pointSize())
        # self.updateView()

    def zoomOut(self):
        """
        Decreases zoom factor and updates sequence viewer contents.
        """
        if self.zoom_factor > 1.0:
            self.zoom_factor -= 1.0
        else:
            self.zoom_factor -= 0.2
        if self.zoom_factor <= 0.2:
            self.zoom_factor = 0.2
        self.updateFontSize(self.font().pointSize())
        # self.updateView()

    def addAnnotation(self, annotation_type, remove=False):
        """
        Adds a new annotation sequence to selected sequences or to all
        sequences if no sequence is selected.

        :type annotation_type: int
        :param annotation_type: Type of the annotation sequence.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Add Annotation")
        self.sequence_group.addAnnotation(annotation_type, remove=remove)
        self.contents_changed = True
        self.updateView()

    def addAllColorBlocks(self):
        """
        Adds all available color blocks annotations to selected or all
        sequences in the sequence group.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Add All Color Blocks")
        for ann in constants.COLOR_BLOCKS:
            self.sequence_group.addAnnotation(ann)
        self.contents_changed = True
        self.updateView()

    def removeAllColorBlocks(self):
        """
        Removes all color blocks annotations from selected or all
        sequences in the sequence group.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Remove All Color Blocks")
        for ann in constants.COLOR_BLOCKS:
            self.sequence_group.addAnnotation(ann, remove=True)
        self.contents_changed = True
        self.updateView()

    def updateView(self,
                   generate_rows=True,
                   update_colors=False,
                   repaint=True,
                   immediately=False):
        """
        Updates the sequence viewer, re-generates profile and re-colors
        the sequences. This should be called every time sequence group
        contents changes.

        :note: This method may take a long time to execute if there are many
            sequences in the group. Consider making profile generation optional.

        :type generate_rows: bool
        :param generate_rows: Optional parameter. If False, the method will
            not re-generate rows (default=True).

        :type.update: bool
        :param.update: Optional parameter. If False, the method will not
            update the viewer contents (default=True).
        """
        if self.sequence_group:
            if self.has_ruler:
                self.sequence_group.addRuler()
            else:
                self.sequence_group.removeRuler()

            if (self.alignment_changed or self.contents_changed or
                    (self.selection_changed
                     and self.sequence_group.identity_in_columns)) and \
                    self.auto_profile:
                self.sequence_group.removeTerminalGaps()
                if self.padded:
                    self.sequence_group.padAlignment()
                self.sequence_group.calculateProfile()
                self.sequence_group.updateVariableSequences()
                update_colors = True
            if update_colors:
                self.sequence_group.colorSequences()
                if self.average_columns:
                    self.sequence_group.colorAverageColumnColors()
                if self.weight_colors:
                    self.sequence_group.colorWeightByAlignmentStrength(
                        self.min_weight_identity, self.max_weight_identity)
                if self.weight_colors_by_identity:
                    self.sequence_group.colorByIdentity()
                elif self.weight_colors_by_difference:
                    self.sequence_group.colorByDifference()
        if generate_rows:
            self.generateRows()
        if repaint:
            self.update()
        if immediately:
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
        if self.contents_changed:
            self.ready_to_save = True
            primeValidateBuildMode()
            updatePrimeQueryList(refresh_ligands=True)
            self.updateAnnotationsMenu()
            if self.cb_contents_changed:
                self.cb_contents_changed()
        if self.selection_changed:
            self.ready_to_save = True
            maestro_helpers.maestroSelectResiduesInWorkspace(self)
            if self.cb_residue_selection_changed:
                self.cb_residue_selection_changed()
        if self.alignment_changed:
            self.ready_to_save = True
        self.contents_changed = False
        self.alignment_changed = False
        self.selection_changed = False

    def updateStatusBar(self):
        """
        Updates bottom status bar.
        """
        total, selected, maestro, hidden = self.sequence_group.statistics()
        ss = "s"
        if total == 1:
            ss = ""
        status = str(total) + " sequence" + ss + " total, " + \
            str(selected) + " selected, " + \
            str(maestro) + " from Maestro, " + \
            str(hidden) + " hidden.   "
        if self.sequence_group.reference:
            status += "Query sequence: " + \
                self.sequence_group.reference.short_name
        else:
            status += "Query sequence: None"
        if self.statistics_status_bar:
            self.statistics_status_bar.setText(status)

    def setMode(self, mode):
        """
        Sets sequence viewer operation mode.

        :note: See constants.py for more details on what modes are available.

        :type mode: int
        :param mode: Sequence viewer mode.
        """
        self.mode = mode
        self.sequence_area.cursor_enabled = False
        self.update()

    def getMode(self):
        """
        Gets current sequence viewer operational mode.

        :rtype: int
        :return: Current sequence viewer mode.
        """
        return self.mode

    def auxBorderWidth(self):
        """
        Calculates a width of sequence area border that includes auxiliary
        information (sequence similarity, borders, etc).
        """
        aux_right_column_width = 0
        aux_left_column_width = 0
        if self.display_identity or \
           self.display_similarity or \
           self.display_homology:
            aux_right_column_width = 4 * self.font_width
        elif self.display_score:
            aux_right_column_width = 5 * self.font_width
        if self.display_boundaries:
            aux_right_column_width += 7 * self.font_width
            aux_left_column_width = 5 * self.font_width
        return aux_right_column_width + aux_left_column_width

    def maxColumns(self,
                   custom_width=None,
                   custom_height=None,
                   calculate_width=False):
        """
        Calculates a maximum number of sequence columns that can fit
        into the viewer.

        :rtype: int or tuple
        :return: maximum number of columns or max number of columns
                and calculated width when calculate_width == True
        """
        aux_right_column_width = 0
        aux_left_column_width = 0
        if self.display_identity or self.display_similarity or \
                                                        self.display_homology:
            aux_right_column_width = 4 * self.font_width
        elif self.display_score:
            aux_right_column_width = 5 * self.font_width
        if self.display_boundaries:
            aux_right_column_width += 7 * self.font_width
            aux_left_column_width = 5 * self.font_width
        if custom_width is None:
            custom_width = self.sequence_area.width()
        if self.wrapped_width:
            custom_width = self.wrapped_width * self.cell_width \
                + aux_left_column_width + aux_right_column_width
        max_columns = old_div((custom_width - 2 * self.margin - 15 -
                               aux_left_column_width - aux_right_column_width),
                              self.cell_width)
        if max_columns < 1:
            max_columns = 1
        if self.wrapped_width:
            max_columns = self.wrapped_width
        if calculate_width:
            return (max_columns, custom_width)
        else:
            return max_columns

    def generateRows(self,
                     use_max_length=False,
                     custom_width=None,
                     custom_height=None):
        """
        Generate rows that can be  directly displayed by the sequence area
        widget. This method is relatively fast, because it doesn't generate
        actual sequence chunks, but rather calculates pointers.
        """
        self.rows = []
        actual_width = 0
        actual_height = 0

        max_length = self.maxColumns(custom_width=custom_width,
                                     custom_height=custom_height)
        if self.wrapped_width:
            max_length, custom_width = self.maxColumns(
                custom_width=custom_width,
                custom_height=custom_height,
                calculate_width=True)
        else:
            max_length = self.maxColumns(custom_width=custom_width,
                                         custom_height=custom_height)
        annotation_types = [
            [constants.SEQ_SECONDARY, None],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_HELIX_PROPENSITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_STRAND_PROPENSITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_TURN_PROPENSITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_HELIX_TERMINATORS],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_STERIC_GROUP],
            [
                constants.SEQ_ANNOTATION,
                constants.ANNOTATION_SIDECHAIN_CHEMISTRY
            ], [constants.SEQ_ANNOTATION, constants.ANNOTATION_IDENTITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_SIMILARITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_BFACTOR],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_HYDROPHOBICITY],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_PI],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_SSP],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_ACC],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_DIS],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_DOM],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_CCB],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_STRUCTURE],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_PFAM],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_RESNUM],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_LIGAND],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_SSBOND],
            [constants.SEQ_ANNOTATION, constants.ANNOTATION_CUSTOM]
        ]
        empty = None
        if self.wrapped:
            # Wrapped mode.
            actual_width = self.sequence_area.width()
            if custom_width is not None:
                actual_width = custom_width
            if self.sequence_group:
                start = 0
                length = 0
                has_sequences = True
                while has_sequences:
                    has_sequences = False
                    for seq in self.sequence_group.sequences:
                        if seq.residues and seq.visible and \
                           start < len(seq.residues):
                            has_sequences = True
                    if has_sequences:
                        last_hidden = False
                        for seq in self.sequence_group.sequences:
                            if self.hide_empty_lines and \
                                    seq.isValidProtein() and \
                                    start > seq.unpaddedLength():
                                continue
                            parent_seq = seq
                            child_index = 0
                            parent_row = 0
                            while seq:
                                if seq.visible:
                                    length = max_length
                                    if start + length > len(seq.residues):
                                        length = len(seq.residues) - start
                                    if length < 0:
                                        length = 0
                                    height = seq.height
                                    if seq.type == constants.SEQ_RULER:
                                        height = 2
                                    if len(seq.residues) == 0:
                                        length = max_length
                                    row = (seq, start, start + length, height)
                                    if seq.annotation_type == constants.ANNOTATION_RESNUM:
                                        self.rows.insert(parent_row, row)
                                    else:
                                        self.rows.append(row)
                                    actual_height += height * self.font_height
                                    seq.last_hidden = last_hidden
                                    last_hidden = False
                                else:
                                    last_hidden = True

                                if seq == parent_seq:
                                    parent_row = len(self.rows) - 1

                                if not self.group_annotations or \
                                   seq.type != constants.SEQ_AMINO_ACIDS:
                                    seq = None
                                    if parent_seq.visible and \
                                       not parent_seq.collapsed and \
                                       len(parent_seq.children) > 0:
                                        while child_index < \
                                                len(parent_seq.children):
                                            seq = parent_seq.children[
                                                child_index]
                                            child_index += 1
                                            if seq.visible:
                                                break
                                            seq = None
                                else:
                                    seq = None

                        # Sequence group.
                        if self.group_annotations:
                            empty = sequence.Sequence()
                            empty.type = constants.SEQ_EMPTY
                            self.rows.append((empty, start, start + length, 1))
                            actual_height += self.font_height
                            for sequence_type, annotation_type in annotation_types:
                                n_child = 0
                                for seq in self.sequence_group.sequences:
                                    for child in seq.children:
                                        if child.type == sequence_type and \
                                           child.annotation_type == annotation_type:
                                            n_child += 1
                                            if child.visible:
                                                length = max_length
                                                if start + length > len(
                                                        child.residues):
                                                    length = len(
                                                        child.residues) - start
                                                if length < 0:
                                                    length = 0
                                                height = child.height
                                                row = (child, start,
                                                       start + length, height)
                                                self.rows.append(row)
                                                actual_height += height * \
                                                    self.font_height
                                if n_child > 0:
                                    empty = sequence.Sequence()
                                    empty.type = constants.SEQ_EMPTY
                                    self.rows.append(
                                        (empty, start, start + length, 1))
                                    actual_height += self.font_height
                        else:
                            empty = sequence.Sequence()
                            empty.type = constants.SEQ_EMPTY
                            self.rows.append((empty, start, start + length, 1))
                            actual_height += self.font_height
                        if empty:
                            empty.type = constants.SEQ_SEPARATOR
                    start = start + max_length
        else:
            # Unwrapped mode.
            start = self.left_column
            length = 0
            max_sequence_length = 0
            if use_max_length:
                max_length = self.sequence_group.findMaxLength()
            if self.sequence_group:
                last_hidden = False
                for seq in self.sequence_group.sequences:
                    parent_seq = seq
                    child_index = 0
                    parent_row = 0
                    while seq:
                        if seq.visible and \
                           seq.type != constants.SEQ_SEPARATOR:
                            sequence_length = len(seq.residues)
                            if sequence_length == 0 and self.sequence_group.profile:
                                sequence_length = len(
                                    self.sequence_group.profile.residues)
                            if sequence_length > max_sequence_length:
                                max_sequence_length = sequence_length
                            length = max_length
                            if start + length > sequence_length:
                                length = sequence_length - start
                            if length < 0:
                                length = 0
                            height = seq.height
                            if seq.type == constants.SEQ_RULER:
                                height = 2
                                length = max_length
                            row = (seq, start, start + length, height)
                            if seq.annotation_type == constants.ANNOTATION_RESNUM:
                                self.rows.insert(parent_row, row)
                            else:
                                self.rows.append(row)
                            actual_height += height * self.font_height
                            seq.last_hidden = False
                            last_hidden = False
                        elif not seq.visible:
                            last_hidden = True
                        if seq == parent_seq:
                            parent_row = len(self.rows) - 1
                        if not self.group_annotations or \
                           seq.type != constants.SEQ_AMINO_ACIDS:
                            seq = None
                            if parent_seq.visible and \
                               not parent_seq.collapsed and \
                               len(parent_seq.children) > 0:
                                while child_index < len(parent_seq.children):
                                    seq = parent_seq.children[child_index]
                                    child_index += 1
                                    if seq.visible:
                                        break
                                    seq = None
                        else:
                            seq = None
                # Sequence group.
                if self.group_annotations:
                    empty = sequence.Sequence()
                    empty.type = constants.SEQ_EMPTY
                    self.rows.append((empty, start, start + length, 1))
                    actual_height += self.font_height
                    for sequence_type, annotation_type in annotation_types:
                        n_child = 0
                        for seq in self.sequence_group.sequences:
                            for child in seq.children:
                                if child.type == sequence_type and \
                                   child.annotation_type == annotation_type:
                                    n_child += 1
                                    if child.visible:
                                        sequence_length = len(child.residues)
                                        if sequence_length > max_sequence_length:
                                            max_sequence_length = sequence_length
                                        length = max_length
                                        if start + length > sequence_length:
                                            length = sequence_length - start
                                        if length < 0:
                                            length = 0
                                        height = child.height
                                        row = (child, start, start + length,
                                               height)
                                        self.rows.append(row)
                                        actual_height += height * \
                                            self.font_height
                        if n_child > 0:
                            empty = sequence.Sequence()
                            empty.type = constants.SEQ_EMPTY
                            self.rows.append((empty, start, start + length, 1))
                            actual_height += self.font_height
            self.sequence_area.horizontal_scroll_bar.setRange(
                0, max_sequence_length - max_length + 5)
            actual_width = max_length * self.cell_width + self.auxBorderWidth()
        # Make the bottom separator empty.
        if empty:
            empty.type = constants.SEQ_EMPTY
        self.sequence_area.vertical_scroll_bar.setRange(0, len(self.rows))
        actual_height += self.font_height
        return (actual_width, actual_height)

    def runClustal(self, ignore_selection=False):
        """
        Runs Clustal alignment and updates self.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Align Sequences")
        ss_constraints = not self.alignment_settings_dialog.allowSSGaps()
        jobs.run_clustal(self,
                         self.sequence_group,
                         ss_constraints=ss_constraints,
                         progress_dialog=self.job_progress_dialog,
                         global_alignment=False,
                         ignore_selection=ignore_selection,
                         viewer=self)
        self.sequence_group.setReference(self.sequence_group.reference)
        self.alignment_changed = True
        self.updateView()

    def setPopupMenus(self, name_menu=None, sequence_menu=None, tree_menu=None):
        """
        Sets popup menu for name area widget.

        :type menu: QPopupMenu
        :param menu: Name area popup menu.
        """
        self.name_popup_menu = name_menu
        self.sequence_popup_menu = sequence_menu
        self.tree_popup_menu = tree_menu

    def findPattern(self, pattern):
        """
        Finds a specified PROSITE-like pattern in the sequences.

        :type pattern: str
        :param pattern: Pattern to find in the sequence group.
        """
        self.sequence_group.findPattern(pattern)
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def undo(self):
        """
        Undoes the last operation.
        """
        status = self.undo_stack.undo(self)
        self.contents_changed = True
        self.updateView()
        return status

    def redo(self):
        """
        Redoes previously undone operation.
        """
        status = self.undo_stack.redo(self)
        self.contents_changed = True
        self.updateView()
        return status

    def runSSP(self):
        """
        Runs secondary structure prediction and incorporates the results.
        """
        self.runPredictors(["sspro"])

    def runPfam(self):
        """
        Runs Pfam simulation and incorporates the results.
        """
        self.undo_stack.storeStateGroup(self.sequence_group, label="Run Pfam")
        jobs.run_pfam(self.sequence_group,
                      self.job_progress_dialog,
                      job_settings=self.job_settings)
        self.updateView()

    def runBlast(self, failed_callback=None, ok_callback=None):
        """
        Takes a `schrodinger.ui.sequencealignment.sequence.Sequence` and runs a
        Blast simulation to determine the best matches. The status callbacks can
        be set.

        This method will return None if the run dialog was cancelled, 'failed' if
        the job failed and 'ok' if the job succeeded.

        :param ref_sequence: The sequence to run the BLAST search against
        :type ref_sequence: schrodinger.ui.sequencealignment.sequence.Sequence
        :param failed_callback: The callback to call when the job fails
        :type  failed_callback: callable
        :param failed_callback: The callback to call when the job succeeds
        :type  failed_callback: callable

        See also `failedBlastCallback` and `successBlastCallback`.

        """
        if not failed_callback:
            failed_callback = self.blastFailedCallback
        if not ok_callback:
            ok_callback = self.blastOkCallback
        self.undo_stack.storeStateGroup(self.sequence_group, label="Run Blast")
        if not self.blast_settings_dialog:
            self.blast_settings_dialog = createBlastSettingsDialog(
                self, execute=False)
        result = self.blast_settings_dialog.exec()
        if not result:
            return
        settings = self.blast_settings_dialog.getAllSettings()
        if not self.blast_results_dialog:
            self.blast_results_dialog = createBlastResultsDialog(self)
        self.blast_results_dialog.hide()
        status = jobs.blast_run(self.sequence_group,
                                settings,
                                progress_dialog=self.job_progress_dialog,
                                results_dialog=self.blast_results_dialog,
                                remote_query_dialog=self.remote_query_dialog,
                                job_settings=self.job_settings)
        if status == 'failed':
            failed_callback(self.blast_settings_dialog,
                            self.blast_results_dialog, ok_callback)
        elif status == 'ok':
            ok_callback(self.blast_results_dialog)
            if self.cb_contents_changed:
                self.cb_contents_changed()
        return status

    def blastFailedCallback(self, dialog, results_dialog, ok_callback):
        """
        The default callback to call when a BLAST search job fails. This
        will retry the job if it was a locally run job and try to use the
        a remote job to succeed.
        """
        if dialog.remote_button.isChecked() == False:
            question = dialogs.question_dialog(
                "Local BLAST search has failed",
                "Would you like to run a remote BLAST job?",
                buttons=["yes", "no"])
            if question == "yes":
                # Run remote BLAST search
                dialog.remote_button.setChecked(True)
                settings = dialog.getAllSettings()
                status = jobs.blast_run(
                    self.sequence_group,
                    settings,
                    progress_dialog=self.job_progress_dialog,
                    results_dialog=results_dialog,
                    remote_query_dialog=self.remote_query_dialog)
                self.job_progress_dialog.hide()
                if status == "ok":
                    ok_callback(results_dialog)
                else:
                    title = 'BLAST Search Error'
                    msg = 'The BLAST search failed.'
                    dialogs.error_dialog(title, msg)

    def blastOkCallback(self, results_dialog):
        """
        The default callback to call when a BLAST search job succeeds. This
        will show the BLAST results panel which allows users to choose which
        sequences to import into the sequence viewer, with the best 10
        highlighted.

        """
        results_dialog.show()
        results_dialog.updateTableGeometry()
        results_dialog.setTopInput(10)
        results_dialog.raise_()
        results_dialog.exec()

    def fetchSequence(self,
                      ids,
                      replace=None,
                      progress=None,
                      maestro_include=False,
                      maestro_incorporate=True,
                      remote_query=True):
        """
        Fetches sequences from online repositories based on entry ID.
        This method attempts to automatically recognize the repository
        by looking on the specified entry ID format.

        :type entry_id: str
        :param entry_id: Entry ID in the online database.

        :type replace: Sequence
        :param replace: Sequence to be replaced by a matching PDB sequence.

        :rtype: string/bool
        :return: On success "ok" or True, on error "error", "cancelled" or "invalid"
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Fetch Sequence")
        result = False
        ids = ids.replace(',', ' ')
        query_dialog = None
        if remote_query and self.remote_query_dialog:
            query_dialog = self.remote_query_dialog
        for entry_id in ids.split(' '):
            if len(entry_id) >= 4 and \
               len(entry_id) <= 5 and \
               entry_id[0] >= '0' and \
               entry_id[0] <= '9':
                tmp_group = sequence_group.SequenceGroup()
                result = jobs.fetch_pdb(
                    tmp_group,
                    entry_id,
                    maestro_include=maestro_include,
                    progress_dialog=self.job_progress_dialog,
                    progress=progress,
                    maestro_incorporate=maestro_incorporate,
                    remote_query_dialog=query_dialog,
                    viewer=self)
                if result == "ok" and tmp_group.sequences:
                    if replace:
                        replace_index = self.sequence_group.sequences.index(
                            replace)
                        self.sequence_group.sequences.remove(replace)
                    else:
                        replace_index = len(self.sequence_group.sequences)
                    for index, pdb in enumerate(tmp_group.sequences):
                        self.sequence_group.sequences.insert(
                            replace_index + index, pdb)
                        if replace:
                            replace.propagateGaps(pdb,
                                                  parent_sequence=pdb,
                                                  replace=True)
                            pdb.selected = replace.selected
                        pdb.propagateGapsToChildren()
            else:
                result = jobs.fetch_entrez(
                    self.sequence_group,
                    entry_id,
                    progress_dialog=self.job_progress_dialog,
                    remote_query_dialog=query_dialog)
                # FIXME result here be True or False, while result returned by
                # jobs.fetch_pdb() is a string.
        if self.job_progress_dialog:
            self.job_progress_dialog.hide()
        self.contents_changed = True
        self.updateView()
        return result

    def moveUp(self):
        """
        Moves selected sequences one level up.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Move Sequences Up")
        self.sequence_group.moveUp()
        self.sequence_group.setReference(self.sequence_group.reference)
        self.alignment_changed = True
        self.updateView()

    def moveDown(self):
        """
        Moves selected sequences one level down.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Move Sequences Down")
        self.sequence_group.moveDown()
        self.sequence_group.setReference(self.sequence_group.reference)
        self.alignment_changed = True
        self.updateView()

    def moveTop(self):
        """
        Moves selected sequences to the top of the group.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Move Sequences Top")
        self.sequence_group.moveTop()
        self.sequence_group.setReference(self.sequence_group.reference)
        self.alignment_changed = True
        self.updateView()

    def moveBottom(self):
        """
        Moves selected sequences to the bottom of the group.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Move Sequences Bottom")
        self.sequence_group.moveBottom()
        self.sequence_group.setReference(self.sequence_group.reference)
        self.alignment_changed = True
        self.updateView()

    def synchronizeWithMaestro(self):
        """
        Synchronizes sequence viewer contents with Maestro workspace.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Synchronize With Maestro")
        maestro_helpers.maestroSynchronize(self.sequence_group)
        maestro_helpers.synchronizePropertiesWithMaestro(self,
                                                         colors=True,
                                                         selection=True)
        self.updateView()

    def duplicateSequences(self):
        """
        Duplicates selected sequences.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Duplicate Sequences")
        self.sequence_group.duplicateSelectedSequences()
        if self.cb_contents_changed:
            self.cb_contents_changed()
        self.contents_changed = True
        self.updateView()

    def setUndoRedoActions(self, undo_action, redo_action):
        """
        Sets Qt undo/redo actions, so that the undo/redo mechanism can change
        the corresponding menu items appropriately.

        :type undo_action: QAction
        :param undo_action: Qt action for undo operation.

        :type redo_action: QAction
        :param redo_action: Qt action for redo operation.
        """
        self.undo_stack.setActions(undo_action, redo_action)

    def setDisplayIdentity(self, value):
        """
        Turns on and off displaing sequence identity information.

        :type value: bool
        :param value: Display identity value.
        """
        self.display_identity = value
        self.updateView()

    def setDisplaySimilarity(self, value):
        """
        Turns on and off displaing sequence similarity information.

        :type value: bool
        :param value: Display similarity value.
        """
        self.display_similarity = value
        self.updateView()

    def setDisplayHomology(self, value):
        """
        Turns on and off displaing sequence homology information.

        :type value: bool
        :param value: Display homology value.
        """
        self.display_homology = value
        self.updateView()

    def setDisplayScore(self, value):
        """
        Turns on and off displaing sequence score information.

        :type value: bool
        :param value: display score value.
        """
        self.display_score = value
        self.updateView()

    def setBoundaries(self, value):
        """
        Turns on and off displaying sequence boundaries on the alignment.

        :type value: bool
        :param value: if True, display boundaries
        """
        self.display_boundaries = value
        self.updateView()

    def saveProject(self, file_name, auto_save=False):
        """
        Saves current project to an external file.
        """
        if auto_save and not self.ready_to_save:
            return
        try:
            output_file = open(file_name, "wb")
            for group in self.sequence_group_list:
                pickle.dump(group.sequences, output_file, 2)
                pickle.dump(group.profile, output_file, 2)
                pickle.dump(group.tree, output_file, 2)
                pickle.dump(group.name, output_file, 2)
                pickle.dump(group.color_mode, output_file, 2)
                pickle.dump(group.custom_color, output_file, 2)
                pickle.dump(group.background_color, output_file, 2)
                pickle.dump(group.inv_background_color, output_file, 2)
                pickle.dump(group.user_annotations, output_file, 2)
            name = output_file.name
            output_file.close()
        except:
            raise
            name = None
        self.ready_to_save = False
        return name

    def loadProject(self, file_name):
        """
        Loads a MSV project from an external file.
        """
        result = True
        try:
            input_file = open(file_name, "rb")
        except:
            return False
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Load Project")
        try:
            input_file.seek(0, 2)
            file_size = input_file.tell()
            input_file.seek(0, 0)
            group_list = []
            pos = 0
            while pos < file_size:
                try:
                    group = sequence_group.SequenceGroup()
                    group.sequences = pickle.load(input_file)
                    group.profile = pickle.load(input_file)
                    group.tree = pickle.load(input_file)
                    group.name = pickle.load(input_file)
                    group.color_mode = pickle.load(input_file)
                    group.custom_color = pickle.load(input_file)
                    group.background_color = pickle.load(input_file)
                    group.inv_background_color = pickle.load(input_file)
                    group.user_annotations = pickle.load(input_file)
                    group.updateReference()
                    group_list.append(group)
                    pos = input_file.tell()
                except:
                    if group:
                        group_list.append(group)
                    break
            input_file.close()
            if group_list:
                for index, group in enumerate(group_list):
                    if not group.name:
                        group.name = "Query %d" % (index + 1)
                    group.repair()
                self.sequence_group_list = group_list
                self.sequence_group = group_list[0]
                self.makeTabsFromGroupList()
        except:
            raise
            result = False
        self.contents_changed = True
        self.updateView()
        maestro_helpers.maestroSynchronize(self.sequence_group)
        return result

    def removeRedundancy(self):
        """
        Removes redundant sequences.
        """
        dialog = RemoveRedundancyDialog(self, self.sequence_group)
        dialog.exec()
        if dialog.result():
            self.undo_stack.storeStateGroupDeep(
                self.sequence_group, label="Remove Redundant Sequences")
            self.excludeSelected()
            self.sequence_group.deleteSelected()
            self.contents_changed = True
            self.updateView()

    def weightColorsSettings(self):
        """
        Enables weighting by colors.
        """
        dialog = WeightColorsDialog(self, self.sequence_group)
        dialog.exec()

    def pairwiseAlignment(self):
        """
        Performs a pariwise alignment using dynamic programming
        (Smith-Waterman algorithm).
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Align Sequences")
        if self.sequence_group.reference not in self.sequence_group.sequences or \
           self.sequence_group.reference.type != constants.SEQ_AMINO_ACIDS or \
                not self.sequence_group.reference.isValidProtein():
            QtWidgets.QMessageBox.critical(
                self, self.tr("Cannot Align Sequences"),
                self.tr("At least two sequences must be present " +
                        "for pairwise alignment."), QtWidgets.QMessageBox.Ok)
            return False
        target_sequence = None
        for seq in self.sequence_group.sequences:
            if seq != self.sequence_group.reference and \
                    seq.isValidProtein():
                target_sequence = seq
                break
        if target_sequence:
            constraint_list = None
            for seq in self.sequence_group.sequences:
                if seq.type == constants.SEQ_CONSTRAINTS:
                    constraint_list = seq.constraint_list
                    break
            not_selected = not self.sequence_group.hasSelectedSequences()
            for seq in self.sequence_group.sequences:
                if seq.isValidProtein() and \
                   seq != self.sequence_group.reference:
                    if (not_selected and seq == target_sequence) or \
                       seq.selected:
                        go_penalty, ge_penalty = \
                            self.alignment_settings_dialog.gapPenalties()
                        ss_constraints = \
                            not self.alignment_settings_dialog.allowSSGaps()
                        align.align(self.sequence_group.reference,
                                    seq,
                                    gap_open_penalty=go_penalty,
                                    gap_extend_penalty=ge_penalty,
                                    scoring_matrix=constants.SIMILARITY_MATRIX,
                                    constraints=constraint_list,
                                    merge=False,
                                    ss_constraints=ss_constraints)
                        self.updateView()
                        QtCore.QCoreApplication.processEvents(
                            QtCore.QEventLoop.AllEvents, 100)
                        if maestro:
                            maestro.process_pending_events()
            self.sequence_group.minimizeAlignment()
        else:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Cannot Align Sequences"),
                self.tr("At least two sequences must be present " +
                        "for pairwise alignment."), QtWidgets.QMessageBox.Ok)
            return False
        self.alignment_changed = True
        self.updateView()

    def alignByResidueNumbers(self):
        """
        Performs a sequence alignment using residue numbers as IDs.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Align by Residue Numbers")
        self.sequence_group.alignByResidueNumbers()
        self.alignment_changed = True
        self.updateView()

    def alignMerge(self):
        """
        Performs a pariwise alignment using dynamic programming
        (Smith-Waterman algorithm). Sequentially merges the aligned sequences
        with the existing alignment.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Align and Merge Sequences")
        if self.sequence_group.reference not in self.sequence_group.sequences or \
           self.sequence_group.reference.type != constants.SEQ_AMINO_ACIDS or \
           not self.sequence_group.hasSelectedSequences(exclude_reference=True):
            QtWidgets.QMessageBox.critical(
                self, self.tr("Cannot Align Sequences"),
                self.tr(
                    "At least one non-reference sequence must be selected " +
                    "for pairwise alignment merging."),
                QtWidgets.QMessageBox.Ok)
            return False
        target_sequence = None
        for seq in self.sequence_group.sequences:
            if seq != self.sequence_group.reference and \
               seq.type == constants.SEQ_AMINO_ACIDS:
                target_sequence = seq
                break
        if target_sequence:
            constraint_list = None
            for seq in self.sequence_group.sequences:
                if seq.type == constants.SEQ_CONSTRAINTS:
                    constraint_list = seq.constraint_list
                    break
            for seq in self.sequence_group.sequences:
                if seq.type == constants.SEQ_AMINO_ACIDS and \
                   seq != self.sequence_group.reference:
                    if seq.selected:
                        go_penalty, ge_penalty = \
                            self.alignment_settings_dialog.gapPenalties()
                        ss_constraints = \
                            not self.alignment_settings_dialog.allowSSGaps()
                        align.align(self.sequence_group.reference,
                                    seq,
                                    gap_open_penalty=go_penalty,
                                    gap_extend_penalty=ge_penalty,
                                    scoring_matrix=constants.SIMILARITY_MATRIX,
                                    constraints=constraint_list,
                                    merge=True,
                                    merge_selected=False,
                                    ss_constraints=ss_constraints,
                                    sequence_group=self.sequence_group,
                                    last_to_merge=seq)
                        self.updateView()
                        QtCore.QCoreApplication.processEvents(
                            QtCore.QEventLoop.AllEvents, 100)
                        if maestro:
                            maestro.process_pending_events()
            self.sequence_group.minimizeAlignment()
        else:
            QtWidgets.QMessageBox.critical(
                self, self.tr("Cannot Align Sequences"),
                self.tr("At least two sequences must" +
                        "be present for pairwise alignment."),
                QtWidgets.QMessageBox.Ok)
            return False
        self.alignment_changed = True
        self.updateView()

    def editSequence(self):
        """
        Edits a selected sequence.
        """
        any_selected = self.sequence_group.hasSelectedSequences()
        # Just edit the first selected sequence.
        for seq in self.sequence_group.sequences:
            if seq.type == constants.SEQ_AMINO_ACIDS and \
               (seq.selected or not any_selected):
                self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                                    label="Edit Sequence")
                dialog = SequenceEditorDialog(self, seq, self.sequence_group)
                dialog.setModal(False)
                dialog.show()
                dialog.exec()
                self.updateView()
                return

    def createSequence(self):
        """
        Creates a new sequence and adds it to the sequence group.

        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Create New Sequence")
        seq = sequence.Sequence()
        seq.name = "new_sequence"
        dialog = SequenceEditorDialog(self, seq, self.sequence_group)
        dialog.setModal(False)
        dialog.button_add.setText("Add")
        dialog.button_replace.setVisible(False)
        dialog.show()
        dialog.exec()
        self.contents_changed = True
        self.updateView()

    def pasteFasta(self):
        """
        Pastes an alignment in text FASTA format into the MSV window.
        """
        dialog = SequenceEditorDialog(self,
                                      None,
                                      self.sequence_group,
                                      fasta_editor=True)
        dialog.setModal(False)
        dialog.sequence_label.setText("Alignment in FASTA format:")
        dialog.button_add.setText("Add")
        dialog.button_replace.setVisible(False)
        dialog.show()
        dialog.exec()
        self.contents_changed = True
        self.updateView()

    def setAsReference(self):
        """
        Sets a selected sequence as a reference.
        """
        if not self.sequence_group.hasSelectedSequences():
            return
        self.contents_changed = True
        self.sequence_group.setReference()
        self.alignment_changed = True
        self.updateView()

    def selectAllSequences(self):
        """
        Selects all sequences in the group.
        """
        self.sequence_group.selectAllSequences()
        self.name_area.update()
        if self.cb_sequence_selection_changed:
            self.cb_sequence_selection_changed()

    def unselectAllSequences(self):
        """
        Deselects all sequences in the group.
        """
        self.sequence_group.unselectAllSequences()
        self.name_area.update()
        if self.cb_sequence_selection_changed:
            self.cb_sequence_selection_changed()

    def invertSequenceSelection(self):
        """
        Inverts current sequence selection range.
        """
        self.sequence_group.invertSequenceSelection()
        self.name_area.update()
        if self.cb_sequence_selection_changed:
            self.cb_sequence_selection_changed()

    def findPrevious(self):
        """
        Scrolls to a previous occurence of a selected pattern.
        """
        if self.wrapped:
            found = None
            for index in range(self.top_row - 1, 0, -1):
                seq, start, end, height = self.rows[index]
                if not seq.parent_sequence and \
                   start < len(seq.residues) and \
                   end < len(seq.residues):
                    for pos in range(start, end):
                        if seq.residues[pos].selected:
                            found = index
                            break
                if found:
                    self.sequence_area.vertical_scroll_bar.setValue(found)
                    break
        else:
            mid = self.left_column
            found = None
            for pos in range(mid - 1, 0, -1):
                for seq in self.sequence_group.sequences:
                    if pos < seq.length():
                        if seq.residues[pos].selected:
                            found = pos
                            break
                if found:
                    break
            if found:
                self.sequence_area.horizontal_scroll_bar.setValue(found)

    def findNext(self):
        """
        Scrolls to a next occurence of a selected pattern.
        """
        if self.wrapped:
            found = None
            for index in range(self.top_row + 1, len(self.rows)):
                seq, start, end, height = self.rows[index]
                if not seq.parent_sequence and \
                   start < len(seq.residues) and \
                   end < len(seq.residues):
                    for pos in range(start, end):
                        if seq.residues[pos].selected:
                            found = index
                            break
                if found:
                    self.sequence_area.vertical_scroll_bar.setValue(found)
                    break
        else:
            mid = self.left_column
            max = self.sequence_group.findMaxLength()
            found = None
            for pos in range(mid + 1, max):
                for seq in self.sequence_group.sequences:
                    if pos < seq.length():
                        if seq.residues[pos].selected:
                            found = pos
                            break
                if found:
                    self.sequence_area.horizontal_scroll_bar.setValue(found)
                    break

    def anchorSelection(self):
        """
        Freezes the selected part of the alignment so it is not possible
        to move residues outside of the restricted part.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Anchor Unselected Residues")
        self.sequence_group.anchorSelection()
        self.updateView()

    def clearAnchors(self):
        """
        Clears the rectricted part of the alignment.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Clear Anchors")
        self.sequence_group.clearAnchors()
        self.updateView()

    def saveImage(self,
                  file_name,
                  save_all=True,
                  format="PNG",
                  custom_width=None,
                  custom_height=None):
        """
        Saves current view to a PNG image file.
        """
        height = self.sequence_area.height()
        pane_sizes = self.sizes()
        tree_width = pane_sizes[0]
        name_width = pane_sizes[1]
        multiple = 1
        _font_size = self.font_size
        self.updateFontSize(multiple * self.font_size)
        seq_area_width = self.sequence_area.width()
        seq_area_height = self.sequence_area.height()
        aux_right_column_width = 0
        aux_left_column_width = 0
        if self.display_identity or \
           self.display_similarity or \
           self.display_homology:
            aux_right_column_width = 4 * self.font_width
        elif self.display_score:
            aux_right_column_width = 5 * self.font_width
        if self.display_boundaries:
            aux_right_column_width += 15 * self.font_width
            aux_left_column_width = 5 * self.font_width
        if self.wrapped_width:
            custom_width = (self.wrapped_width + 3) * self.font_width \
                + aux_right_column_width + aux_left_column_width
        if custom_width is not None:
            name_width = self.name_area.getMaxWidth()
            custom_width -= name_width
            seq_area_width = custom_width
        if custom_height is not None:
            seq_area_height = custom_height
        if (custom_width is not None and
                custom_width <= 0) or (custom_height is not None and
                                       custom_height <= 0):
            custom_width = None
            custom_height = None
        if save_all:
            sequence_width, sequence_height = self.generateRows(
                use_max_length=True,
                custom_width=custom_width,
                custom_height=custom_height)
            # Save the entire alignment.
            height = sequence_height
            width = tree_width + \
                name_width + \
                sequence_width
        else:
            sequence_width, sequence_height = self.generateRows(
                use_max_length=False,
                custom_width=custom_width,
                custom_height=custom_height)
            # Save only the visible part of the alignment.
            height = seq_area_height
            width = tree_width + \
                name_width + \
                seq_area_width
        if format == "AUTO":
            if file_name.endswith(".png"):
                format = "PNG"
            elif file_name.endswith(".pdf"):
                format = "PDF"
            else:
                format = "PNG"
        if format == "PNG":
            image = QtGui.QImage(multiple * width, multiple * height,
                                 QtGui.QImage.Format_RGB32)
            if image:
                painter = QtGui.QPainter(image)
                painter.setFont(self.original_font)
                painter.fillRect(image.rect(), self.background_color)
                painter.setClipping(True)
                painter.setClipRect(0, 0, multiple * tree_width,
                                    multiple * sequence_height)
                self.tree_area.paintTreeArea(painter, None, clip=False)
                painter.translate(tree_width, 0)
                painter.setClipRect(0, 0, multiple * name_width,
                                    multiple * sequence_height)
                self.name_area.paintNameArea(painter,
                                             None,
                                             clip=False,
                                             custom_width=name_width,
                                             custom_height=custom_height)
                painter.translate(name_width, 0)
                painter.setClipRect(0, 0, multiple * sequence_width,
                                    multiple * sequence_height)
                self.sequence_area.paintSequenceArea(
                    painter,
                    None,
                    clip=False,
                    custom_width=custom_width,
                    custom_height=custom_height)
                painter.end()
                if self.crop_image:
                    # Determine image extends
                    bgpix = image.pixel(0, 0)
                    top = 0
                    bottom = image.height() - 1
                    left = 0
                    right = image.width() - 1
                    found = False
                    for y in range(image.height()):
                        for x in range(image.width()):
                            if image.pixel(x, y) != bgpix:
                                top = y
                                found = True
                                break
                        if found:
                            break
                    found = False
                    for y in reversed(list(range(image.height()))):
                        for x in range(image.width()):
                            if image.pixel(x, y) != bgpix:
                                bottom = y
                                found = True
                                break
                        if found:
                            break
                    found = False
                    for x in range(image.width()):
                        for y in range(image.height()):
                            if image.pixel(x, y) != bgpix:
                                left = x
                                found = True
                                break
                        if found:
                            break
                    found = False
                    for x in reversed(list(range(image.width()))):
                        for y in range(image.height()):
                            if image.pixel(x, y) != bgpix:
                                right = x
                                found = True
                                break
                        if found:
                            break
                    image = image.copy(left, top, right - left, bottom - top)
                image.save(file_name, "PNG")
        elif format == "PDF":
            try:
                printer = QtPrintSupport.QPrinter(
                    QtPrintSupport.QPrinter.ScreenResolution)
            except:
                printer = None
            if printer:
                printer.setOutputFormat(QtPrintSupport.QPrinter.PdfFormat)
                printer.setOutputFileName(file_name)
                printer.setFontEmbeddingEnabled(True)
                printer.setFullPage(False)
                printer.setPaperSize(QtCore.QSizeF(width, height),
                                     QtPrintSupport.QPrinter.DevicePixel)
                printer.setPageMargins(0, 0, 0, 0,
                                       QtPrintSupport.QPrinter.DevicePixel)
                painter = QtGui.QPainter(printer)
                painter.setFont(self.original_font)
                painter.setClipping(True)
                painter.setClipRect(0, 0, tree_width, sequence_height)
                self.tree_area.paintTreeArea(painter, None, clip=False)
                painter.translate(tree_width, 0)
                painter.setClipRect(0, 0, name_width, sequence_height)
                self.name_area.paintNameArea(painter,
                                             None,
                                             clip=False,
                                             custom_width=name_width,
                                             custom_height=custom_height)
                painter.translate(name_width, 0)
                painter.setClipRect(0, 0, sequence_width, sequence_height)
                painter.setClipping(False)
                self.sequence_area.paintSequenceArea(
                    painter,
                    None,
                    clip=False,
                    custom_width=custom_width,
                    custom_height=custom_height)
                painter.end()
        # Regenerate original layout
        self.generateRows()
        self.updateFontSize(_font_size)
        if format == "PNG" and image:
            return (image.width(), image.height())
        else:
            return None

    def displayMessage(self, message):
        """
        Displays a message on a status bar.
        """
        if self.message_status_bar:
            self.message_status_bar.setText(message)

    def setConsiderGaps(self, value):
        """
        Sets value of consider gaps flag. If set to True, gaps will be
        included in calculation of local sequence similarity measures.

        :type value: bool
        :param value: Should we consider gaps for sequence identity
            calculations.
        """
        self.sequence_group.setConsiderGaps(value)

    def getConsiderGaps(self):
        """
        Gets value of consider gaps flag.

        :rtype value: bool
        :return: Should we consider gaps for sequence identity calculations.
        """
        return self.sequence_group.consider_gaps

    def expandSelection(self):
        """
        Expands selection to include entire columns.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Expand Selection")
        self.sequence_group.expandSelection()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def expandSelectionRef(self):
        """
        Expands selection from reference sequence to include entire columns.
        """
        self.undo_stack.storeStateGroupDeep(
            self.sequence_group, label="Expand Selection from Reference")
        self.sequence_group.expandSelectionRef()
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def setUseColors(self, use_colors):
        """
        Sets use colors flag.

        :type use_colors: bool
        :param use_colors: If True, colors will be used to draw the sequences.
        """
        self.use_colors = use_colors
        self.updateView()

    def toggleConstraints(self):
        """
        Toggles a pairwise alignment constraints.
        """
        if not self.set_constraints:
            result = self.sequence_group.addConstraint(None, None, None, None)
            if not result:
                dialogs.error_dialog("Error", "Could not create constraints.")
                return False
        else:
            self.sequence_group.removeConstraints()
        self.set_constraints = not self.set_constraints
        self.updateView()
        return self.set_constraints

    def clearConstraints(self):
        """
        Clears pairwise alignment constraints.
        """
        self.sequence_group.clearConstraints()
        self.updateView()

    def enableQueryConstraints(self):
        """
        Toggles a query sequence constraints.
        """
        self.set_query_constraints = True
        if self.set_query_constraints:
            self.sequence_group.addConstraint(None,
                                              None,
                                              None,
                                              None,
                                              for_prime=True)
        self.updateView()

    def disableQueryConstraints(self, remove_if_empty=True):
        """
        Toggles a query sequence constraints.
        """
        self.set_query_constraints = False
        if remove_if_empty and \
                not self.sequence_group.hasConstraints():
            self.sequence_group.removeConstraints()
        self.updateView()

    def clearQueryConstraints(self):
        """
        Toggles a query sequence constraints.
        """
        self.sequence_group.clearConstraints()
        self.updateView()

    def setIdentityInColumns(self, value):
        """
        Sets "Calculate identity in columns" setting.
        """
        self.sequence_group.identity_in_columns = value
        self.sequence_group.calculateProfile()
        self.sequence_group.updateVariableSequences()
        self.updateView()

    def showBlastResults(self):
        """
        Displays BLAST results dialog.
        """
        if not self.blast_results_dialog:
            self.blast_results_dialog = createBlastResultsDialog()
        self.blast_results_dialog.updateTableGeometry()
        self.blast_results_dialog.exec()

    def addGlobalAnnotations(self):
        """
        Add global annotations action callback.
        """
        self.addConsensus(update=False)
        self.addSymbols(update=False)
        self.addMeanHydrophobicity(update=False)
        self.addMeanPI(update=False)
        self.addSequenceLogo(update=False)
        self.contents_changed = True
        self.updateView()

    def removeGlobalAnnotations(self):
        """
        Removes global annotations.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Delete Global Annotations")
        self.sequence_group.deleteGlobalAnnotations()
        self.contents_changed = True
        self.updateView()

    def hideColumns(self, unselected=False):
        """
        Hides selected columns.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Hide Columns")
        self.sequence_group.hideColumns(unselected=unselected)
        self.contents_changed = True
        self.updateView()

    def showColumns(self):
        """
        Shows all hidden columns.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Show Columns")
        self.sequence_group.showAllResidues()
        self.contents_changed = True
        self.updateView()

    def computeSequenceProfile(self):
        """
        Computes internal sequence profile and updates self.
        """
        self.sequence_group.calculateProfile()
        self.sequence_group.updateVariableSequences()
        self.updateView()

    def alignmentSettingsDialog(self):
        """
        Opens alignment settings dialog.
        """
        self.alignment_settings_dialog.group = self.sequence_group
        self.alignment_settings_dialog.exec()

    def downloadPDB(self,
                    maestro_include=True,
                    maestro_incorporate=True,
                    remote_query=True):
        """
        Downloads a corresponding PDB structure.

        :return: True if the download was successful, otherwise False.
        :rtype: boolean
        """

        self.maestro_busy = True
        result = True
        sequences = self.sequence_group.sequences[:]
        any_selected = self.sequence_group.hasSelectedSequences()
        total = 0
        for seq in sequences:
            if seq.type == constants.SEQ_AMINO_ACIDS and \
               (seq.selected or not any_selected):
                pdb_id = seq.getPDBId()
                if len(pdb_id) >= 3:
                    total += 1
        if total:
            count = 0
            self.job_progress_dialog.updateProgress(0.0)
            for seq in sequences:
                if seq.type == constants.SEQ_AMINO_ACIDS and \
                   (seq.selected or not any_selected):
                    pdb_id = seq.getPDBId()
                    if len(pdb_id) >= 3:
                        progress = old_div(float(count), float(total))
                        count += 1
                        if self.fetchSequence(
                                pdb_id,
                                replace=seq,
                                progress=progress,
                                maestro_include=maestro_include,
                                maestro_incorporate=maestro_incorporate,
                                remote_query=remote_query) == "cancelled":
                            result = False
                            break
        self.maestro_busy = False
        self.job_progress_dialog.hide()
        self.contents_changed = True
        self.updateView()
        return result

    def showJobLog(self):
        """
        Displays a job log window.
        """
        dialog = createJobProgressDialog("Job Log", parent=self)
        dialog.setButtonText("Close")
        dialog.exec()

    def showJobSettings(self):
        """
        Displays a job settings dialog.
        """
        result = createJobSettingsDialog(self)
        if result:
            self.job_settings = result

    def buildModel(self):
        """
        Builds 3D model of reference sequence.
        """
        showPrimeSettingsDialog(self, self.sequence_group, prime.prime_run,
                                self.job_progress_dialog)

    def alignStructures(self):
        """
        Aligns available structures.
        """
        jobs.run_align_structures(self.sequence_group,
                                  self.job_progress_dialog,
                                  viewer=self)

    def markResidues(self, rgb):
        """
        Marks selected residues using a specified RGB color.

        :type rgb: (int, int, int)
        :param rgb: RGB color tuple.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Mark Selected Residues")
        self.sequence_group.markResidues(rgb)
        self.sequence_group.unselectAll()
        self.update()

    def clearMarkedResidues(self):
        """
        Clears residues that are marked by custom color.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Clear Marked Residues")
        self.sequence_group.clearMarkedResidues()
        self.sequence_group.unselectAll()
        self.update()

    def cropSelectedResidues(self):
        """
        Crops residues in a selected area.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Crop Selected Residues")
        self.sequence_group.cropSelectedResidues()
        self.updateView()

    def displayLigands(self):
        """
        Displays Maestro ligand interaction fingerprints.
        """
        none_selected = not self.sequence_group.hasSelectedSequences()
        for seq in self.sequence_group.sequences:
            if seq.isValidProtein() and (none_selected or seq.selected):
                maestro_helpers.maestroGetLigandAnnotations(seq)
        self.contents_changed = True
        self.updateView()

    def newSet(self, name=None):
        """
        Creates a new query set
        """
        self.sequence_group = sequence_group.SequenceGroup()
        self.last_index += 1
        set_name = "Query %d" % (self.last_index)
        if name:
            self.sequence_group.name = name
        else:
            self.sequence_group.name = set_name
        self.sequence_group_list.append(self.sequence_group)
        self.updateView()
        return set_name

    def clearSet(self):
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Clear All")
        name = self.sequence_group.name
        if self.sequence_group in self.sequence_group_list:
            index = self.sequence_group_list.index(self.sequence_group)
        else:
            index = 0
        self.sequence_group = sequence_group.SequenceGroup()
        self.sequence_group.name = name
        self.sequence_group_list[index] = self.sequence_group
        self.updateView()

    def changeQuery(self, index):
        if index in range(len(self.sequence_group_list)):
            group = self.sequence_group_list[index]
            self.sequence_group = group
            setPrimeSequenceGroup(group)
            self.updateView()
            return True
        return False

    def excludeSelected(self):
        if not maestro:
            return
        tmp_synchronize = self.auto_synchronize
        self.auto_synchronize = False
        ids = set()
        for seq in self.sequence_group.sequences:
            if (not seq.selected) and seq.from_maestro:
                ids.add(seq.maestro_entry_id)
        for seq in self.sequence_group.sequences:
            if seq.selected and seq.from_maestro and \
               seq.maestro_entry_id not in ids and \
               seq.maestro_entry_id != "Scratch":
                maestro.command("entrywsexclude entry \"" +
                                str(seq.maestro_entry_id) + "\"")
        self.auto_synchronize = tmp_synchronize

    def excludeQueryEntries(self):
        if not maestro:
            return
        tmp_synchronize = self.auto_synchronize
        self.auto_synchronize = False
        for seq in self.sequence_group.sequences:
            if seq.from_maestro:
                maestro.command("entrywsexclude entry \"" +
                                str(seq.maestro_entry_id) + "\"")
        self.auto_synchronize = tmp_synchronize

    def includeQueryEntries(self):
        if not maestro:
            return
        tmp_synchronize = self.auto_synchronize
        self.auto_synchronize = False
        for seq in self.sequence_group.sequences:
            if seq.from_maestro and seq.maestro_included:
                maestro.command("entrywsinclude entry \"" +
                                str(seq.maestro_entry_id) + "\"")
        maestro.command("fit all")
        self.auto_synchronize = tmp_synchronize
        maestro_helpers.maestroSynchronize(self.sequence_group)

    def deleteSet(self, index):
        """
        Remove a sequence viewer tab.
        """
        if index in range(len(self.sequence_group_list)):
            group = self.sequence_group_list[index]
            self.sequence_group_list.remove(group)
            if index == len(self.sequence_group_list):
                index -= 1
            self.sequence_group = self.sequence_group_list[index]
            group = self.sequence_group
            self.contents_changed = True
            self.updateView()
            return True
        return False

    def emphasizeArea(self, area):
        if area == 0:
            self.name_area.emphasized = False
            self.sequence_area.emphasized = False
        elif area == 1:
            self.name_area.emphasized = True
            self.sequence_area.emphasized = False
        elif area == 2:
            self.name_area.emphasized = False
            self.sequence_area.emphasized = True
        self.update()

    def colorSequenceNames(self, color):
        self.sequence_group.colorSequenceNames(color)
        self.update()

    def incorporateIncludedEntries(self, incorporate_scratch_entry=None):
        """
        Incorporates included entries into the SV.
        """
        if incorporate_scratch_entry is not None:
            scratch_entry = incorporate_scratch_entry
        else:
            scratch_entry = self.incorporate_scratch_entry
        maestro_helpers.maestroIncorporateEntries(
            self.sequence_group,
            incorporate_scratch_entry=scratch_entry,
            align_func=jobs.pdb_align_structure_to_sequence,
            use_title=self.use_maestro_entry_title,
            viewer=self)
        self.contents_changed = True
        self.updateView()

    def incorporateSelectedEntries(self):
        """
        Incorporates selected entries into the SV.
        """
        maestro_helpers.maestroIncorporateEntries(
            self.sequence_group,
            what="selected",
            incorporate_scratch_entry=self.incorporate_scratch_entry,
            align_func=jobs.pdb_align_structure_to_sequence,
            use_title=self.use_maestro_entry_title,
            viewer=self)
        self.contents_changed = True
        self.updateView()

    def colorEntrySurface(self):
        any_selected = self.sequence_group.hasSelectedSequences()
        for seq in self.sequence_group.sequences:
            if seq.from_maestro and (seq.selected or not any_selected):
                if not maestro_helpers.maestroColorEntrySurface(self, seq):
                    dialogs.error_dialog(
                        "Error",
                        "Could not color surface for selected sequence(s).")
                    return

    def runPredictors(self, predictor_list):
        """
        Runs a specified residue-level property predictors.
        """
        self.undo_stack.storeStateGroup(self.sequence_group,
                                        label="Run Prediction")
        for pred in predictor_list:
            if not predictors.has_predictor(pred):
                dialogs.error_dialog(
                    "Predictor not available",
                    pred + " is not available. Do you have Prime installed?")
                return False
        any_selected = self.sequence_group.hasSelectedSequences()
        for seq in self.sequence_group.sequences:
            if seq.selected or not any_selected:
                results = predictors.run(
                    seq,
                    predictor_list,
                    progress_dialog=self.job_progress_dialog,
                    remote_query_dialog=self.remote_query_dialog)
                if results == "cancelled":
                    break
                elif results != "failed":
                    for pred_seq in results:
                        pred_seq.parent_sequence = seq
                        seq.children.append(pred_seq)
                    seq.propagateGapsToChildren()
                if hasattr(seq, "beta_map") and \
                        "betapro" in predictor_list:
                    beta_map = ContactMap(
                        self,
                        seq.short_name + " Beta Strand Contacts Prediction")
                    length = seq.gaplessLength()
                    beta_map.setMap(seq.beta_map, length, length)
        self.updateView()
        return True

    def renameSequence(self):
        """
        Renames a selected sequence.
        """
        for seq in self.sequence_group.sequences:
            if seq.selected:
                name, ok = dialogs.string_input_dialog(
                    "Rename Sequence", "Enter new sequence name",
                    seq.short_name)
                if ok and name:
                    seq.short_name = str(name)
                    self.sequence_group.unselectAllSequences()
                    self.updateView()

    def copySequences(self, group):
        """
        Copies all or selected sequences from group to self.sequence_group.
        """
        self.sequence_group.copySequences(group)
        self.contents_changed = True
        self.updateView()

    def makeTabsFromGroupList(self):
        """
        Create named query tabs based on self sequence group list.
        """
        if not self.query_tabs:
            return
        for index in range(self.query_tabs.count()):
            self.query_tabs.removeTab(0)
        for group in self.sequence_group_list:
            self.query_tabs.addTab(group.name)

    def passSelectionToMaestro(self):
        tmp = self.auto_synchronize
        self.auto_synchronize = True
        maestro_helpers.maestroSelectResiduesInWorkspace(self)
        self.auto_synchronize = tmp

    def closeEvent(self, event):
        """
        Called on window close request.
        Will attempt to save MSV state in current Maestro project directory.
        """
        if not self.save_state:
            return
        project_path = maestro_helpers.maestroGetProjectPath()
        if project_path:
            self.saveProject(project_path + "project.msv")

    def showEvent(self, event):
        """
        Called on window show request.
        Will attempt to restore MSV state from current Maestro project directory.
        """
        if not self.save_state:
            return
        # At this point, we know we are running in standalone MSV inside
        # Maestro
        project_path = maestro_helpers.maestroGetProjectPath()
        if project_path and not self.last_project_path:
            if self.loadProject(project_path + "project.msv"):
                self.last_project_path = project_path
            else:
                project_path = maestro_helpers.maestroGetProjectPath(old=True)
                if self.loadProject(project_path + "project.msv"):
                    self.last_project_path = project_path

    def resizeEvent(self, event):
        """
        Resize event handler.
        Update font face and size here.
        """
        self.updateFontSize(self.font_size)

    def createCommandDict(self):
        """
        Creates an external command dictionary.
        """
        if self.command_dict:
            return
        self.command_dict = {}
        self.command_dict["update"] = self.update
        self.command_dict["update_view"] = self.updateView
        self.command_dict["update_font_size"] = self.updateFontSize
        self.command_dict[
            "hide_selected_sequences"] = self.hideSelectedSequences
        self.command_dict[
            "delete_selected_sequences"] = self.deleteSelectedSequences
        self.command_dict["show_all_sequences"] = self.showAllSequences
        self.command_dict["fill_gaps"] = self.fillGaps
        self.command_dict["remove_gaps"] = self.removeGaps
        self.command_dict[
            "delete_selected_residues"] = self.deleteSelectedResidues
        self.command_dict["invert_selection"] = self.invertSelection
        self.command_dict["expand_selection"] = self.expandSelection
        self.command_dict["deselect_all"] = self.deselectAll
        self.command_dict["select_all"] = self.selectAll
        self.command_dict["remove_empty_columns"] = self.minimizeAlignment
        self.command_dict["select_aligned_blocks"] = self.selectAlignedBlocks
        self.command_dict[
            "select_structure_blocks"] = self.selectStructureBlocks
        self.command_dict["select_identities"] = self.selectIdentities
        self.command_dict["delete_annotations"] = self.deleteAnnotations
        self.command_dict["delete_predictions"] = self.deletePredictions
        self.command_dict["propagate_colors"] = self.propagateColors
        self.command_dict["delete_all"] = self.clearSequences
        self.command_dict["add_consensus"] = self.addConsensus
        self.command_dict["multiple_sequence_alignment"] = self.runClustal
        self.command_dict["run_ssp"] = self.runSSP
        self.command_dict["run_pfam"] = self.runPfam
        self.command_dict["run_blast"] = self.runBlast
        self.command_dict["synchronize"] = self.synchronizeWithMaestro
        self.command_dict["pairwise_alignment"] = self.pairwiseAlignment
        self.command_dict["select_all_sequences"] = self.selectAllSequences
        self.command_dict["deselect_all_sequences"] = self.unselectAllSequences
        self.command_dict[
            "invert_sequence_selection"] = self.invertSequenceSelection
        self.command_dict["remove_redundant_sequences"] = self.removeRedundancy
        self.command_dict["quit"] = quit
        self.command_dict["align_structures"] = self.alignStructures
        self.command_dict["collapse_all"] = self.collapseAllSequences
        self.command_dict["expand_all"] = self.expandAllSequences
        self.command_dict["sequence_logo"] = self.addSequenceLogo
        self.command_dict["send_to_knime"] = self.parent().parent().backToKnime

    def colorToRGB(self, color):
        """
        Converts a color name string to RGB tuple.
        """
        colors = {
            'white': (255, 255, 255),
            'black': (0, 0, 0),
            'red': (255, 0, 0),
            'green': (0, 255, 0),
            'blue': (0, 0, 255),
            'yellow': (255, 255, 0),
            'magenta': (255, 0, 255),
            'cyan': (0, 255, 255),
            'gray': (127, 127, 127),
            'orange': (255, 127, 0),
            'teal': (0, 127, 255)
        }
        if color in colors:
            return colors[color]
        return (255, 255, 255)

    def selectSequencesByName(self, name_list):
        for seq in self.sequence_group.sequences:
            if seq.short_name in name_list \
                    or seq.name in name_list:
                seq.selected = True

    def executeCommandFile(self, cmd_file_name):
        """
        Executes an external command file.
        """
        try:
            cmd_file = open(cmd_file_name, "r")
            commands = cmd_file.readlines()
            cmd_file.close()
        except:
            print("Cannot execute command file ", cmd_file_name)
            return False
        progress_dialog = self.job_progress_dialog
        self.job_progress_dialog = None
        self.createCommandDict()
        for cmd in commands:
            if cmd[0] == '#':
                continue
            cmd_list = cmd.split()
            if not cmd_list:
                continue
            if cmd_list[0] in list(self.command_dict):
                command = self.command_dict[cmd_list[0]]
                command()
            elif cmd_list[0] == "load_file":
                arg_list = ''.join(cmd_list[1:]).split(',')
                if not arg_list:
                    continue
                inp_name = arg_list[0]
                if inp_name.startswith('"') and inp_name.endswith('"'):
                    inp_name = inp_name[1:-1]
                self.loadFile(inp_name)
            elif cmd_list[0] == "save_image":
                arg_list = ''.join(cmd_list[1:]).split(',')
                if len(arg_list) < 3:
                    continue
                out_name = arg_list[0]
                if out_name.startswith('"') and out_name.endswith('"'):
                    out_name = out_name[1:-1]
                try:
                    out_width = int(arg_list[1])
                    out_height = int(arg_list[2])
                except:
                    out_width = None
                    out_height = None
                self.saveImage(out_name,
                               format="AUTO",
                               custom_width=out_width,
                               custom_height=out_height)
            elif cmd_list[0] == "annotate":
                arg_list = ''.join(cmd_list[1:]).split(',')
                ann_type = arg_list[0]
                if ann_type not in constants.ANNOTATION_NAMES_DICT:
                    continue
                if len(arg_list) > 1:
                    self.selectSequencesByName(arg_list[1:])
                self.addAnnotation(constants.ANNOTATION_NAMES_DICT[ann_type])
            elif cmd_list[0] == "color":
                ann_type = cmd_list[1]
                if cmd_list[1] not in constants.COLOR_NAMES_DICT:
                    continue
                self.setColorMode(constants.COLOR_NAMES_DICT[cmd_list[1]])
            elif cmd_list[0] == "background":
                arg_list = ''.join(cmd_list[1:]).split(',')
                color = self.colorToRGB(arg_list[0])
                self.setBackgroundColor(color)
            elif cmd_list[0] == "color_name":
                arg_list = ''.join(cmd_list[1:]).split(',')
                color = self.colorToRGB(arg_list[0])
                if len(arg_list) > 1:
                    self.selectSequencesByName(arg_list[1:])
                self.colorSequenceNames(color)
            elif cmd_list[0] == "select_columns":
                arg = ''.join(cmd_list[1:])
                res_range = [
                    c for c in arg if c in [
                        "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ",",
                        "-"
                    ]
                ]
                res_list = res_range.split(',')
                for res in res_list:
                    try:
                        col = int(res)
                        self.sequence_group.selectColumns(col - 1,
                                                          col - 1,
                                                          update_viewer=False)
                        res = None
                    except:
                        pass
                    if res and '-' in res:
                        col = res.split('-')

                        if len(col) > 1:
                            try:
                                col0 = int(col[0])
                                col1 = int(col[1])
                                self.sequence_group.selectColumns(
                                    col0 - 1, col1 - 1, update_viewer=False)
                            except:
                                pass
            elif cmd_list[0] == "mark_residues":
                arg = ''.join(cmd_list[1:])
                color = self.colorToRGB(arg)
                self.markResidues(color)
            elif cmd_list[0][:3] == "set":
                set_list = ''.join(cmd_list[1:]).split('=')
                if len(set_list) < 2:
                    continue
                toggle = False
                if set_list[1].lower() == "true":
                    toggle = True
                if not hasattr(self, set_list[0]):
                    continue
                if set_list[0] == "separator_scale":
                    toggle = int(set_list[1])
                if set_list[0] == "wrapped_width":
                    toggle = int(set_list[1])
                if set_list[0] == "font_size":
                    toggle = int(set_list[1])
                setattr(self, set_list[0], toggle)
                if set_list[0] == "font_size":
                    self.updateFontSize()
        self.update()
        self.job_progress_dialog = progress_dialog
        return True

    def setCallback(self, callback, event_type="residue_selection_changed"):
        """
        Sets a sequence viewer callback.

        :type event_type: string
        :param event_type: Type of callback event.

        :param callback: Callback function to be called.
        """
        if event_type == "residue_selection_changed":
            self.cb_residue_selection_changed = callback
        elif event_type == "sequence_selection_changed":
            self.cb_sequence_selection_changed = callback
        elif event_type == "contents_changed":
            self.cb_contents_changed = callback

    def getContents(self, all_sequences=False):
        """
        Returns contents of the current sequence group (tab).

        :type all_sequences: boolean
        :param all_sequences: If False (default), return only protein sequences.
                              If True, return all sequences including ruler,
                              annotations, spacers, etc.

        :rtype: list of tuples
        :return: Returns a list of following tuples: (sequence_index,
                sequence_short_name, sequence_full_name, maestro_entry_id,
                maestro_chain_name, sequence_string)
        """
        out_list = []
        for index, seq in enumerate(self.sequence_group.sequences):
            if all_sequences or seq.isValidProtein():
                out_list.append(
                    (index, seq.short_name, seq.name, seq.maestro_entry_id,
                     seq.maestro_chain_name, seq.text()))
        return out_list

    def selectColumns(self, column_list):
        """
        Selects specified columns in the viewer.

        First, the function deselects all contents, then selects
        columns specified by the provided list of alignment indices.

        :type column_list: list of integers
        :param column_list: List of colums to select.
        """
        self.sequence_group.unselectAll()
        for index in column_list:
            self.sequence_group.selectColumns(index, index, update_viewer=False)
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def selectResidues(self, sequence_list):
        """
        Selects specified residues in the viewer.

        The function deselects all contents, then selects residues
        specified by the alignment indices for each sequence.

        :type sequence_list: list of tuples
        :param sequence_list: Each tuple includes (sequence_index,
                list_of_alignment_indices)
        """
        self.sequence_group.unselectAll()
        for seq_index, residue_list in sequence_list:
            if seq_index >= 0 and seq_index < len(
                    self.sequence_group.sequences):
                seq = self.sequence_group.sequences[seq_index]
                for res_index in residue_list:
                    if res_index >= 0 and res_index < seq.length():
                        seq.residues[res_index].selected = True
        self.selection_changed = True
        self.updateView(generate_rows=False, update_colors=False)

    def getGlobalAnnotations(
            self,
            annotation_types=["all"],  # noqa: M511
            ignore_query=False,
            ignore_gaps=False):
        """
        Returns global annotations for calculate for each position
        of the alignment.

        :type annotation_types: list of strings
        :param annotation_types: List of global annotation types
            to be calculated. The following types are allowed:

        "all" (default value): All available annotations.

        "variability_percentage": sequence variability (normalized
        Shannon entropy) calculated for each alignment position.

        "variability_count": Number of different residue types for
        each alignment position.

        "group_conservation": Classification based on pre-defined conservation
        groups: 'strong' or 'weak' conservations.

        The following numerical values are possible:

        3: identity
        2: strong conservation
        1: weak conservation
        0: no conservation
        -1: gap in query

        "query_match_percentage": Percentage of sequences that match
        the corresponding residue of the query sequence.

        "sasa": Solvent-accessible surface area. It is not used by default and
        it has to be explicitly specified.

        "sasa_percentage": Percentage of solvent-accessible surface area.
        It is not used by default and it has to be explicitly specified.

        :type ignore_query: boolean
        :param ignore query: Determines if query (parent) sequence should be
            included in calculations.

        :type ignore_gaps: boolean
        :param ignore_gaps: Determines if gaps in query sequence should be
            included in calculations. If False, only values calculated for ungapped
            positions will be returned.

        :rtype: dictionary of lists of floats
        :return: Returns a dictionary with annotation names as keys and lists of
            numbers including calculated annotations for each alignment position.
        """
        # Make sure the annotations are current
        self.sequence_group.calculateProfile(ignore_query=ignore_query)
        known_annotations = [
            "variability_percentage", "variability_count", "group_conservation",
            "query_match_percentage"
        ]
        annotation_dict = {}
        if "sasa" in annotation_types:
            annotation_types.append("sasa")
        if "all" in annotation_types:
            annotation_types += known_annotations
        for annotation in annotation_types:
            if annotation == "variability_percentage":
                ann = []
                for index, r in enumerate(self.sequence_group.profile.residues):
                    if ignore_gaps and \
                            (index < len(self.sequence_group.reference)) and \
                            self.sequence_group.reference.residues[index].is_gap:
                        continue
                    ann.append(100.0 * r.entropy)
                annotation_dict[annotation] = ann
            elif annotation == "variability_count":
                ann = []
                for index, r in enumerate(self.sequence_group.profile.residues):
                    if ignore_gaps and \
                            (index < len(self.sequence_group.reference)) and \
                            self.sequence_group.reference.residues[index].is_gap:
                        continue
                    ann.append(len(r.composition))
                annotation_dict[annotation] = ann
            elif annotation == "group_conservation":
                has_symbols = False
                for seq in self.sequence_group.sequences:
                    if seq.type == constants.SEQ_SYMBOLS:
                        has_symbols = True
                symbols = self.sequence_group.addConsensusSymbols()
                ann = []
                if symbols:
                    self.sequence_group.updateSymbols(symbols)
                    for index, r in enumerate(symbols.residues):
                        if ignore_gaps and \
                                (index < len(self.sequence_group.reference)) and \
                                self.sequence_group.reference.residues[index].is_gap:
                            continue
                        if r.code == '*':
                            ann.append(3)
                        elif r.code == ':':
                            ann.append(2)
                        elif r.code == '.':
                            ann.append(1)
                        else:
                            ann.append(0)
                    if not has_symbols:
                        self.sequence_group.sequences.remove(symbols)
                annotation_dict[annotation] = ann
            elif annotation == "query_match_percentage":
                ann = []
                for index, r in enumerate(self.sequence_group.profile.residues):
                    if ignore_gaps and \
                            (index < len(self.sequence_group.reference)) and \
                            self.sequence_group.reference.residues[index].is_gap:
                        continue
                    ann.append(100.0 * r.query_fraction)
                annotation_dict[annotation] = ann
            elif annotation == "sasa":
                sasa_list = self.getSASA(
                    sequences=[self.sequence_group.reference],
                    ignore_gaps=ignore_gaps)
                if sasa_list and sasa_list[0]:
                    sequence_index, list_residue_sasa = sasa_list[0]
                    annotation_dict[annotation] = list_residue_sasa
            elif annotation == "sasa_percentage":
                sasa_list = self.getSASA(
                    sequences=[self.sequence_group.reference],
                    ignore_gaps=ignore_gaps,
                    percentage=True)
                if sasa_list and sasa_list[0]:
                    sequence_index, list_residue_sasa = sasa_list[0]
                    annotation_dict[annotation] = list_residue_sasa
            elif annotation == "variations":
                ann = []
                for index, r in enumerate(self.sequence_group.profile.residues):
                    if ignore_gaps and \
                            (index < len(self.sequence_group.reference)) and \
                            self.sequence_group.reference.residues[index].is_gap:
                        continue
                    ann.append(list(r.composition))
                annotation_dict[annotation] = ann
        # Recalculate profile using default settings
        self.sequence_group.calculateProfile()
        return annotation_dict

    def getSASA(
            self,
            sequences=[],  # noqa: M511
            selected_only=False,
            ignore_gaps=False,
            normalize=True,
            percentage=False):
        """
        Calculates residue sequence accessible surface area for each sequence.
        The calculated area is not normalized.

        :type sequences: list of Sequences
        :param: List of sequences to calculate SASA. When multiple sequences
            come from a single entry, the calculation will be optimized (performed
            once per entry).

        :type: ignore_gaps
        :param: If True the gaps will not be included in the output list.

        :type normalize: bool
        :param normalize: Should we normalize the SASA area by area of amino acid
            in default conformation.

        :type percentage: bool
        :param percentage: If True return percentage SASA instead of absolute
            values.

        :rtype: list of tuples
        :return: Each tuple includes (sequence_index, list_residue_sasa)
            If SASA could not be calculated (e.g. no structural information),
            the list_residue_sasa is empty.
        """

        maestro_helpers.maestroCalculateSASA(self.sequence_group,
                                             sequences=sequences,
                                             selected_only=selected_only,
                                             normalize=normalize,
                                             percentage=percentage)
        group = sequences if sequences else self.sequence_group.sequences
        out_list = []
        for index, seq in enumerate(group):
            sasa_list = []
            out_list.append((index, sasa_list))
            if selected_only and not seq.selected:
                continue
            if not seq.isValidProtein():
                continue
            for res in seq.residues:
                if ignore_gaps and res.is_gap:
                    continue
                sasa_list.append(res.area)
        return out_list

    def assignAntibodyScheme(self,
                             scheme='Chothia',
                             display_annotation=True,
                             annotation_color=(255, 0, 0),
                             remove=False,
                             renumber_entry=True,
                             renumber=True,
                             annotate=True,
                             select=False):
        """
        Assigns a specified antibody numbering scheme.

        :param scheme: Numbering scheme type. 'Chothia', 'EnhancedChothia'
            'IMGT', 'AHo' or 'Kabat' are valid choices. Please refer to psp.antibody docs
            for more details.

        :param display_annotation: Displays a custom annotation for
            the assigned loops (default: True)

        :param annotation_color: R,G,B color uses for annotation
            (default: red)

        :type remove: bool
        :param remove: If True, instead of creating new annotations,
            the function removes any existing CDR annotations and quits.

        :type renumber: bool
        :param renumber: When True, the function will assign new residue
            numbers according to the selected scheme.

        :type renumber_entry: bool
        :param renumber_entry: When True the function will renumber
            corresponding Maestro entry.

        :type annotate: bool
        :param annotate: When True, the function will create new CDR
            annotations (note: the 'remove' paarameter will override this).

        :type select: bool
        :param select: When True, the function will select residues
            belonging to the CDRs.

        :rtype: int
        :return: Number of successfully re-numbered sequences.
        """
        if not PspSeqType:
            return 0
        if select:
            self.sequence_group.unselectAll()
        ok = 0
        for seq in self.sequence_group.sequences:
            if seq.isValidProtein():
                if remove:
                    for child in seq.children:
                        if child.annotation_type == constants.ANNOTATION_CUSTOM and \
                                child.name == "CDR":
                            seq.children.remove(child)
                    continue
                seq_text = seq.toString()
                seq_type = PspSeqType(seq_text, scheme=scheme)
                resid_list = seq_type.resid_list
                cdr_index = seq_type.cdr_index
                cdr_label = seq_type.cdr_label
                region_list = []
                if seq_type and cdr_index:
                    for index, pos in enumerate(cdr_index):
                        start, end = pos
                        region = (start, end, cdr_label[index],
                                  annotation_color)
                        region_list.append(region)
                    if annotate:
                        self.sequence_group.addCustomAnnotation(
                            sequence=seq,
                            title="CDR",
                            name="CDR",
                            region_list=region_list)
                    if select:
                        self.sequence_group.selectRegions(seq, region_list)
                        self.selection_changed = True
                residues = seq.gaplessResidues()
                if renumber and resid_list and \
                        len(resid_list) <= len(residues):
                    for index, resid in enumerate(resid_list):
                        if resid[-1] < '0' or resid[-1] > '9':
                            residues[index].num = int(resid[1:-1])
                            residues[index].icode = resid[-1]
                        else:
                            residues[index].num = int(resid[1:])
                            residues[index].icode = ' '
                    # Renumber the remanining part of the sequence
                    if len(residues) > index:
                        num = residues[index].num + 1
                        for res in residues[index + 1:]:
                            res.num = num
                            res.icode = ' '
                            num += 1
                    ok += 1
                    if renumber_entry:
                        maestro_helpers.renumberMaestroEntry(self, seq)
        self.contents_changed = True
        self.updateView()
        return ok

    def analyzeBindingSite(self):
        """
        Performs binding site analysis.
        """
        dialog = analyzeBindingSiteDialog(self)
        dialog.setSequence(None, None)
        # if self.sequence_group.reference and
        # self.sequence_group.reference.from_maestro:
        dialog.setSequence(self.sequence_group, self.sequence_group.reference)
        dialog.show()
        # else:
        #    dialog.error_dialog("Analyze Binding Site",
        #                 "No structure available in the sequence viewer.")

    def maestroWorkspaceChanged(self, what_changed):
        """
        This function is invoked whenever Maestro colors change. It updates colors
        and selection state of the sequences associated with Maestro.
        """
        if self.maestro_busy:
            return
        if not self.auto_synchronize:
            return
        if not self.isVisible():
            return
        if what_changed == "color":
            maestro_helpers.synchronizePropertiesWithMaestro(self, colors=True)
        elif what_changed == "selection":
            maestro_helpers.synchronizePropertiesWithMaestro(self,
                                                             selection=True)
        elif what_changed == "geometry" or \
                what_changed == "representation" or \
                what_changed == "coordinates" or \
                what_changed == "unknown":
            pass
        else:
            # Everything changed, update project (slow!)
            self.maestroProjectOpen()
            maestro_helpers.maestroSynchronize(self.sequence_group)
            self.repaint()

    def maestroProjectChanged(self):
        """
        This method is invoked whenever Maestro project changes.
        """
        if self.maestro_busy:
            return
        if not self.auto_synchronize:
            return
        if not self.isVisible():
            return
        maestro_helpers.maestroSynchronize(self.sequence_group)
        # Update MSV window.
        self.updateView()

    def maestroProjectClose(self):
        """
        This function is invoked whenever Maestro project is about
        to be closed.
        """
        if self.maestro_busy or not self.save_state:
            return
        project_path = maestro_helpers.maestroGetProjectPath()
        if project_path:
            self.last_project_path = ""
            self.saveProject(project_path + "project.msv")
            self.sequence_group.removeMaestroSequences()
            self.contents_changed = True
            self.updateView()

    def maestroProjectOpen(self):
        """
        This function is invoked whenever Maestro project is opened.
        """
        if self.maestro_busy:
            return
        project_path = maestro_helpers.maestroGetProjectPath()
        if project_path and not self.last_project_path:
            if self.loadProject(project_path + "project.msv"):
                self.last_project_path = project_path
            else:
                project_path = maestro_helpers.maestroGetProjectPath(old=True)
                if self.loadProject(project_path + "project.msv"):
                    self.last_project_path = project_path

    def initMaestro(self):
        """
        Initializes Maestro callbacks.
        """
        if maestro_helpers.hasMaestro():
            if not maestro.is_function_registered('workspace_changed',
                                                  self.maestroWorkspaceChanged):
                maestro.workspace_changed_function_add(
                    self.maestroWorkspaceChanged)
            if not maestro.is_function_registered('project_update',
                                                  self.maestroProjectChanged):
                maestro.project_update_callback_add(self.maestroProjectChanged)
            if not maestro.is_function_registered('project_close',
                                                  self.maestroProjectClose):
                maestro.project_close_callback_add(self.maestroProjectClose)
            if not maestro.is_function_registered('command',
                                                  self.maestroCommandCallback):
                maestro.command_callback_add(self.maestroCommandCallback)

    def removeMaestroCallbacks(self):
        """
        Removes registered Maestro callbacks.
        """
        if maestro_helpers.hasMaestro():
            if maestro.is_function_registered('workspace_changed',
                                              self.maestroWorkspaceChanged):
                maestro.workspace_changed_function_remove(
                    self.maestroWorkspaceChanged)
            if maestro.is_function_registered('project_update',
                                              self.maestroProjectChanged):
                maestro.project_update_callback_remove(
                    self.maestroProjectChanged)
            if maestro.is_function_registered('project_close',
                                              self.maestroProjectClose):
                maestro.project_close_callback_remove(self.maestroProjectClose)
            if maestro.is_function_registered('command',
                                              self.maestroCommandCallback):
                maestro.command_callback_remove(self.maestroCommandCallback)

    def maestroCommandCallback(self, command):
        """
        Called when Maestro executes a command.
        """
        if (command and command.startswith("projectcopy") and
                self.ready_to_save and self.save_state):
            project_path = maestro_helpers.maestroGetProjectPath()
            if project_path:
                self.saveProject(project_path + "project.msv", auto_save=True)

    def translate(self):
        """
        Translates DNA / RNA to amino acids.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Translate Sequence")
        for seq in self.sequence_group.sequences:
            if seq.selected:
                if seq.isDNA():
                    seq.translateDNA()
                elif seq.isRNA():
                    seq.translateRNA()
        self.contents_changed = True
        self.updateView()

    def addUserAnnotation(self, annotation_type="region"):
        if annotation_type == "custom_sequence":
            self.sequence_group.addCustomSequence()
            self.contents_changed = True
            self.updateView()
            return
        if not self.sequence_group.hasSelectedResidues():
            return
        # Determine selected region boundaries
        start = end = -1
        for col in range(self.sequence_group.findMaxLength() + 1):
            if self.sequence_group.isColumnSelected(col, weak=True):
                if start < 0:
                    start = col
            else:
                if start >= 0:
                    end = col - 1
            if start >= 0 and end >= 0:
                if annotation_type == "region":
                    self.sequence_group.user_annotations.append(
                        (annotation_type, start, end, "", (255, 127, 0)))
                elif annotation_type == "rectangle":
                    first = last = None
                    for seq in self.sequence_group.sequences:
                        if start < seq.length():
                            if seq.residues[start].selected:
                                first = seq
                                break
                    for seq in reversed(self.sequence_group.sequences):
                        if end < seq.length():
                            if seq.residues[end].selected:
                                last = seq
                                break
                    self.sequence_group.user_annotations.append(
                        (annotation_type, (first, start), (last, end), "",
                         (240, 0, 0)))
                start = end = -1
        self.updateView()

    def removeUserAnnotations(self):
        self.sequence_group.user_annotations = []
        self.updateView()

    def updateAnnotationsMenu(self):
        if not self.update_annotations_menu:
            return
        if self.parent() and self.parent().parent():
            if hasattr(self.parent().parent(), "annotationsMenu"):
                ann_states = {}
                for ann in constants.LOCAL_ANNOTATIONS:
                    ann_states[ann] = False
                ann_states['secondary'] = False
                has_cdr = False
                has_consensus = False
                has_mean_hydro = False
                has_mean_pi = False
                has_logo = False
                has_symbols = False
                gparent = self.parent().parent()
                for seq in self.sequence_group.sequences:
                    if seq.type == constants.SEQ_CONSENSUS:
                        has_consensus = True
                    elif seq.type == constants.SEQ_LOGO:
                        has_logo = True
                    elif seq.type == constants.SEQ_SYMBOLS:
                        has_symbols = True
                    if seq.annotation_type == constants.ANNOTATION_MEAN_HYDRO:
                        has_mean_hydro = True
                    if seq.annotation_type == constants.ANNOTATION_MEAN_PI:
                        has_mean_pi = True
                    for child in seq.children:
                        if child.type == constants.SEQ_SECONDARY:
                            ann_states['secondary'] = True
                        else:
                            ann_states[child.annotation_type] = True
                        if child.annotation_type == constants.ANNOTATION_CUSTOM and \
                                child.name == "CDR":
                            has_cdr = True
                gparent.addSSAAct.setChecked(ann_states['secondary'])
                gparent.addResnumAct.setChecked(
                    ann_states[constants.ANNOTATION_RESNUM])
                gparent.addBFactorAct.setChecked(
                    ann_states[constants.ANNOTATION_BFACTOR])
                gparent.addHydrophobicityAct.setChecked(
                    ann_states[constants.ANNOTATION_HYDROPHOBICITY])
                gparent.addPIAct.setChecked(ann_states[constants.ANNOTATION_PI])
                gparent.addSSBondAct.setChecked(
                    ann_states[constants.ANNOTATION_SSBOND])
                gparent.sidechainChemistryAct.setChecked(
                    ann_states[constants.ANNOTATION_SIDECHAIN_CHEMISTRY])
                gparent.helixPropensityAct.setChecked(
                    ann_states[constants.ANNOTATION_HELIX_PROPENSITY])
                gparent.strandPropensityAct.setChecked(
                    ann_states[constants.ANNOTATION_STRAND_PROPENSITY])
                gparent.turnPropensityAct.setChecked(
                    ann_states[constants.ANNOTATION_TURN_PROPENSITY])
                gparent.stericGroupAct.setChecked(
                    ann_states[constants.ANNOTATION_STERIC_GROUP])
                gparent.exposureTendencyAct.setChecked(
                    ann_states[constants.ANNOTATION_EXPOSURE_TENDENCY])
                gparent.helixTerminatorsAct.setChecked(
                    ann_states[constants.ANNOTATION_HELIX_TERMINATORS])
                gparent.renumberToAntibodyAct.setChecked(has_cdr)
                gparent.addSequenceLogoAct.setChecked(has_logo)
                gparent.addConsensusAct.setChecked(has_consensus)
                gparent.addSymbolsAct.setChecked(has_symbols)
                gparent.addMeanHydrophobicityAct.setChecked(has_mean_hydro)
                gparent.addMeanPIAct.setChecked(has_mean_pi)
                gparent.showLigandsAct.setChecked(
                    ann_states[constants.ANNOTATION_LIGAND])

    def renumberResidues(self):
        """
        Executes 'Renumber Residues' command.
        """
        self.undo_stack.storeStateGroupDeep(self.sequence_group,
                                            label="Renumber Residues")
        dialog = RenumberResiduesDialog(self, self.sequence_group)
        dialog.show()
        dialog.exec()
        self.updateView()

    def getStructureAlignment(self):
        maestro_helpers.maestroGetStructureAlignment(self, self.sequence_group)
        self.contents_changed = True
        self.updateView()

    def associateMaestroEntries(self):
        """
        Displays 'Associate Maestro Entries' dialog.
        """
        if not self.associate_dialog:
            self.associate_dialog = AssociateEntryPanel(self)
        self.associate_dialog.show()
        self.associate_dialog.raise_()

    def selectLigandContacts(self, ligand=None):
        """
        Selects residues in ligand proximity in selected ligand sequence.
        """
        if ligand:
            parent = ligand.parent_sequence
            for idx, res in enumerate(ligand.residues):
                if res.value > 0.0:
                    parent.residues[idx].selected = True
        else:
            for seq in self.sequence_group.sequences:
                for child in seq.children:
                    if child.selected and \
                            child.annotation_type == constants.ANNOTATION_LIGAND:
                        for idx, res in enumerate(child.residues):
                            if res.value > 0.0:
                                seq.residues[idx].selected = True
        self.selection_changed = True
        self.updateView()

    def incorporateStructure(self, st):
        """
        Incorporates a structure passed as a Structure object.
        """
        maestro_helpers.maestroIncorporateEntries(
            self.sequence_group,
            ct=st,
            use_title=self.use_maestro_entry_title,
            viewer=self)
        self.contents_changed = True
        self.updateView()

    def showCompareSequencesDialog(self):
        """
        Create an instance of 'Compare sequences' dialog (if necessary)
        and open the dialog.
        """
        if not self.compare_sequences_dialog:
            self.compare_sequences_dialog = CompareSequencesDialog(self)
        self.compare_sequences_dialog.show()
        self.compare_sequences_dialog.raise_()