Exemplo n.º 1
0
class ToolbarTool(ToolInstance):

    SESSION_ENDURING = True
    SESSION_SAVE = False
    PLACEMENT = "top"
    CUSTOM_SCHEME = "toolbar"
    help = "help:user/tools/toolbar.html"  # Let ChimeraX know about our help page

    def __init__(self, session, tool_name):
        super().__init__(session, tool_name)
        self.display_name = "Toolbar"
        global _settings
        if _settings is None:
            _settings = _ToolbarSettings(self.session, "Toolbar")
        from chimerax.ui import MainToolWindow
        self.tool_window = MainToolWindow(self, close_destroys=False, hide_title_bar=True)
        self._build_ui()
        self.tool_window.fill_context_menu = self.fill_context_menu
        session.triggers.add_handler('set right mouse', self._set_right_mouse_button)

    def _build_ui(self):
        from chimerax.ui.widgets.tabbedtoolbar import TabbedToolbar
        from PyQt5.QtWidgets import QVBoxLayout
        layout = QVBoxLayout()
        margins = layout.contentsMargins()
        margins.setTop(0)
        margins.setBottom(0)
        layout.setContentsMargins(margins)
        self.ttb = TabbedToolbar(
            self.tool_window.ui_area, show_section_titles=_settings.show_section_labels,
            show_button_titles=_settings.show_button_labels)
        layout.addWidget(self.ttb)
        self._build_tabs()
        self.tool_window.ui_area.setLayout(layout)
        self.tool_window.manage(self.PLACEMENT)

    def fill_context_menu(self, menu, x, y):
        # avoid having actions destroyed when this routine returns
        # by stowing a reference in the menu itself
        from PyQt5.QtWidgets import QAction
        button_labels = QAction("Show button labels", menu)
        button_labels.setCheckable(True)
        button_labels.setChecked(_settings.show_button_labels)
        button_labels.toggled.connect(lambda arg, f=self._set_button_labels: f(arg))
        menu.addAction(button_labels)
        section_labels = QAction("Show section labels", menu)
        section_labels.setCheckable(True)
        section_labels.setChecked(_settings.show_section_labels)
        section_labels.toggled.connect(lambda arg, f=self._set_section_labels: f(arg))
        menu.addAction(section_labels)
        settings_action = QAction("Settings...", menu)
        settings_action.triggered.connect(lambda arg: self.show_settings())
        menu.addAction(settings_action)

    def show_settings(self):
        if not hasattr(self, "settings_tool"):
            self.settings_tool = ToolbarSettingsTool(
                self.session, self,
                self.tool_window.create_child_window("Toolbar Settings", close_destroys=False))
            self.settings_tool.tool_window.manage(None)
        self.settings_tool.tool_window.shown = True

    def _set_button_labels(self, show_button_labels):
        _settings.show_button_labels = show_button_labels
        self.ttb.set_show_button_titles(show_button_labels)

    def _set_section_labels(self, show_section_labels):
        _settings.show_section_labels = show_section_labels
        self.ttb.set_show_section_titles(show_section_labels)

    def build_home_tab(self):
        # (re)Build Home tab from settings
        from PyQt5.QtGui import QIcon
        self.ttb.clear_tab("Home")
        last_section = None
        for (section, compact, display_name, icon_path, description, link, bundle_info, name, kw) in _home_layout(self.session, _settings.home_tab):
            if section != last_section:
                last_section = section
                if compact:
                    self.ttb.set_section_compact("Home", section, True)
            if icon_path is None:
                icon = None
            else:
                icon = QIcon(icon_path)

            def callback(event, session=self.session, name=name, bundle_info=bundle_info, display_name=display_name):
                bundle_info.run_provider(session, name, session.toolbar, display_name=display_name)
            self.ttb.add_button(
                    "Home", section, display_name, callback,
                    icon, description, **kw)

    def _build_tabs(self):
        # add buttons from toolbar manager
        from PyQt5.QtGui import QIcon
        from .manager import fake_mouse_mode_bundle_info
        self.right_mouse_buttons = {}
        self.current_right_mouse_button = None

        self.build_home_tab()

        # Build other tabs from toolbar manager
        toolbar = self.session.toolbar._toolbar
        last_tab = None
        last_section = None
        for (tab, section, compact, display_name, icon_path, description, bundle_info, name, kw) in _other_layout(self.session, toolbar):
            if tab != last_tab:
                last_tab = tab
                last_section = None
            if section != last_section:
                last_section = section
                if compact:
                    self.ttb.set_section_compact(tab, section, True)
            if bundle_info == fake_mouse_mode_bundle_info:
                kw["vr_mode"] = name  # Allows VR to recognize mouse mode buttons
                rmbs = self.right_mouse_buttons.setdefault(name, [])
                if icon_path is None:
                    m = self.session.ui.mouse_modes.named_mode(name)
                    if m is not None:
                        icon_path = m.icon_path
                rmbs.append((tab, section, display_name, icon_path))
            if icon_path is None:
                icon = None
            else:
                icon = QIcon(icon_path)

            def callback(event, session=self.session, name=name, bundle_info=bundle_info, display_name=display_name):
                bundle_info.run_provider(session, name, session.toolbar, display_name=display_name)
            self.ttb.add_button(
                    tab, section, display_name, callback,
                    icon, description, **kw)
        self.ttb.show_tab('Home')
        self._set_right_mouse_button('init', self.session.ui.mouse_modes.mode("right", exact=True))

    def _set_right_mouse_button(self, trigger_name, mode):
        # highlight current right mouse button
        name = mode.name if mode is not None else None
        if name == self.current_right_mouse_button:
            return

        set_sections = set()
        has_button = name in self.right_mouse_buttons
        if has_button:
            for info in self.right_mouse_buttons[name]:
                tab_title, section_title, _, _ = info
                set_sections.add((tab_title, section_title))

        if self.current_right_mouse_button is not None:
            # remove highlighting
            for info in self.right_mouse_buttons[self.current_right_mouse_button]:
                tab_title, section_title, button_title, icon_path = info
                redo = (tab_title, section_title) not in set_sections
                self.ttb.remove_button_highlight(tab_title, section_title, button_title, redo=redo)
        if not has_button:
            return
        # highlight button(s)
        self.current_right_mouse_button = name
        for info in self.right_mouse_buttons[name]:
            tab_title, section_title, button_title, icon_path = info
            self.ttb.add_button_highlight(tab_title, section_title, button_title)
Exemplo n.º 2
0
class SequenceViewer(ToolInstance):
    """ Viewer displays a multiple sequence alignment """

    help = "help:user/tools/sequenceviewer.html"

    MATCHED_REGION_INFO = ("matched residues", (1, .88, .8), "orange red")

    ERROR_REGION_STRING = "mismatches"
    GAP_REGION_STRING = "missing structure"
    """TODO
    buttons = ('Quit', 'Hide')
    help = "ContributedSoftware/multalignviewer/framemav.html"
    provideStatus = True
    statusWidth = 15
    statusPosition = "left"
    provideSecondaryStatus = True
    secondaryStatusPosition = "left"

    ConsAttr = "svPercentConserved"

    MATCH_REG_NAME_START = "matches"
    ERROR_REG_NAME_START = "mismatches"
    GAP_REG_NAME_START = "missing structure"

    # so Model Loops tool can invoke it...
    MODEL_LOOPS_MENU_TEXT = "Modeller (loops/refinement)..."

    def __init__(self, fileNameOrSeqs, fileType=None, autoAssociate=True,
                title=None, quitCB=None, frame=None, numberingDisplay=None,
                sessionSave=True):
    """
    def __init__(self, session, tool_name, alignment=None):
        """ if 'alignment' is None, then we are being restored from a session and
            _finalize_init will be called later.
        """

        ToolInstance.__init__(self, session, tool_name)
        if alignment is None:
            return
        self._finalize_init(alignment)

    def _finalize_init(self, alignment):
        """TODO
        from chimera import triggerSet
        self.triggers = triggerSet.TriggerSet()
        self.triggers.addTrigger(ADD_ASSOC)
        self.triggers.addTrigger(DEL_ASSOC)
        self.triggers.addTrigger(MOD_ASSOC)
        self.triggers.addHandler(ADD_ASSOC, self._fireModAssoc, None)
        self.triggers.addHandler(DEL_ASSOC, self._fireModAssoc, None)
        self.triggers.addTrigger(ADD_SEQS)
        self.triggers.addTrigger(PRE_DEL_SEQS)
        self.triggers.addTrigger(DEL_SEQS)
        self.triggers.addTrigger(ADDDEL_SEQS)
        self.triggers.addTrigger(SEQ_RENAMED)
        self.triggers.addHandler(ADD_SEQS, self._fireAddDelSeq, None)
        self.triggers.addHandler(DEL_SEQS, self._fireAddDelSeq, None)
        self.triggers.addHandler(ADDDEL_SEQS, self._fireModAlign, None)
        self.triggers.addTrigger(MOD_ALIGN)
        self.associations = {}
        self._resAttrs = {}
        self._edited = False

        from common import getStaticSeqs
        seqs, fileMarkups, fileAttrs = getStaticSeqs(fileNameOrSeqs, fileType=fileType)
        self.seqs = seqs
        """
        self.alignment = alignment
        from . import subcommand_name
        alignment.attach_viewer(self, subcommand_name=subcommand_name)
        from . import settings
        self.settings = settings.init(self.session)
        """
        from SeqCanvas import shouldWrap
        if numberingDisplay:
            defaultNumbering = numberingDisplay
        else:
            defaultNumbering = (True,
                not shouldWrap(len(seqs), self.prefs))
        self.numberingsStripped = False
        if getattr(seqs[0], 'numberingStart', None) is None:
            # see if sequence names imply numbering...
            startInfo = []
            for seq in seqs:
                try:
                    name, numbering = seq.name.rsplit('/',1)
                except ValueError:
                    break
                try:
                    start, end = numbering.split('-')
                except ValueError:
                    start = numbering
                try:
                    startInfo.append((name, int(start)))
                except ValueError:
                    break
            if len(startInfo) == len(seqs):
                self.numberingsStripped = True
                for i, seq in enumerate(seqs):
                    seq.name, seq.numberingStart = \
                                startInfo[i]
            else:
                for seq in seqs:
                    if hasattr(seq, 'residues'):
                        for i, r in enumerate(seq.residues):
                            if r:
                                seq.numberingStart = r.id.position - 1
                                break
                        else:
                            seq.numberingStart = 1
                    else:
                        seq.numberingStart = 1
                if not numberingDisplay:
                    defaultNumbering = (False, False)

        self._defaultNumbering = defaultNumbering
        self.fileAttrs = fileAttrs
        self.fileMarkups = fileMarkups
        if not title:
            if isinstance(fileNameOrSeqs, basestring):
                title = os.path.split(fileNameOrSeqs)[1]
            else:
                title = "Sequence Viewer"
        self.title = title
        self.autoAssociate = autoAssociate
        self.quitCB = quitCB
        self.sessionSave = sessionSave
        self._runModellerWSList = []
        self._runModellerLocalList = []
        self._realignmentWSJobs = {'self': [], 'new': []}
        self._blastAnnotationServices = {}
        ModelessDialog.__init__(self)
        """
        words = self.alignment.description.split()
        capped_words = []
        for word in words:
            if word.islower() and word.isalpha():
                capped_words.append(word.capitalize())
            else:
                capped_words.append(word)
        self.display_name = " ".join(
            capped_words) + " [ID: %s]" % self.alignment.ident
        from chimerax.ui import MainToolWindow
        self.tool_window = MainToolWindow(self,
                                          close_destroys=True,
                                          statusbar=True)
        self.tool_window._dock_widget.setMouseTracking(True)
        self.tool_window.fill_context_menu = self.fill_context_menu
        self.status = self.tool_window.status
        parent = self.tool_window.ui_area
        parent.setMouseTracking(True)
        """TODO
        # SeqCanvas will use these...
        leftNums, rightNums = self._defaultNumbering
        self.leftNumberingVar = Tkinter.IntVar(parent)
        self.leftNumberingVar.set(leftNums)
        self.rightNumberingVar = Tkinter.IntVar(parent)
        self.rightNumberingVar.set(rightNums)

        """
        from .seq_canvas import SeqCanvas
        self.seq_canvas = SeqCanvas(parent, self, self.alignment)
        if self.alignment.associations:
            # There are pre-existing associations, show them
            for aseq in self.alignment.seqs:
                if aseq.match_maps:
                    self.seq_canvas.assoc_mod(aseq)
        from .region_browser import RegionBrowser
        rb_window = self.tool_window.create_child_window("Regions",
                                                         close_destroys=False)
        self.region_browser = RegionBrowser(rb_window, self.seq_canvas)
        self._seq_rename_handlers = {}
        for seq in self.alignment.seqs:
            self._seq_rename_handlers[seq] = seq.triggers.add_handler(
                "rename", self.region_browser._seq_renamed_cb)
            if seq.match_maps:
                self._update_errors_gaps(seq)
        if self.alignment.intrinsic:
            self.show_ss(True)
            self.status("Helices/strands depicted in gold/green")
        """TODO
        if self.fileMarkups:
            from HeaderSequence import FixedHeaderSequence
            headers = []
            for name, val in self.fileMarkups.items():
                headers.append(
                    FixedHeaderSequence(name, self, val))
            self.addHeaders(headers)
        self.prefDialog = PrefDialog(self)
        top = parent.winfo_toplevel()
        cb = lambda e, rb=self.regionBrowser: rb.deleteRegion(
                                rb.curRegion())
        top.bind('<Delete>', cb)
        top.bind('<BackSpace>', cb)
        self.menuBar = Tkinter.Menu(top, type="menubar", tearoff=False)
        top.config(menu=self.menuBar)

        self.fileMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="File", menu=self.fileMenu)
        self.fileMenu.add_command(label="Save As...", command=self.save)
        self.epsDialog = None
        self.fileMenu.add_command(label="Save EPS...",
                        command=self._showEpsDialog)
        self.fileMenu.add_command(label="Save Association Info...",
            state='disabled', command=self._showAssocInfoDialog)
        self.fileMenu.add_separator()
        self.fileMenu.add_command(label="Load SCF/Seqsel File...",
                command=lambda: self.loadScfFile(None))
        self.fileMenu.add_command(label="Load Color Scheme...",
                    command=self._showColorSchemeDialog)
        self.fileMenu.add_separator()
        self.fileMenu.add_command(label="Hide", command=self.Hide)
        self.fileMenu.add_command(label="Quit", command=self.Quit)
        if parent == dialogParent:
            # if we're not part of a custom interface,
            # override the window-close button to quit, not hide
            top.protocol('WM_DELETE_WINDOW', self.Quit)

        self.editMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Edit", menu=self.editMenu)
        self.editMenu.add_command(label="Copy Sequence...",
                    command=self._showCopySeqDialog)
        self.editMenu.add_command(label="Reorder Sequences...",
                    command=self._showReorderDialog)
        self.editMenu.add_command(label="Insert All-Gap Columns...",
                    command=self._showInsertGapDialog)
        self.editMenu.add_command(label="Delete Sequences/Gaps...",
                    command=self._showDelSeqsGapsDialog)
        self.editMenu.add_command(label="Add Sequence...",
                    command=self._showAddSeqDialog)
        self.editMenu.add_command(label="Realign Sequences...",
                    command=self._showRealignmentDialog)
        self.editMenu.add_command(label="Alignment Annotations...",
                    command=self._showAlignAttrDialog)
        self.editMenu.add_command(label="Edit Sequence Name...",
                    command=self._showSeqNameEditDialog)
        self.editMenu.add_command(label="Show Editing Keys...",
                    command=self._showEditKeysDialog)
        self.editMenu.add_command(label=u"Region \N{RIGHTWARDS ARROW} New Window",
                    command=self.exportActiveRegion)
        self.editMenu.add_separator()
        self.editMenu.add_command(label="Find Subsequence...",
                    command=self._showFindDialog)
        self.editMenu.add_command(label="Find Regular Expression...",
                    command=self._showRegexDialog)
        self.editMenu.add_command(label="Find PROSITE Pattern...",
                    command=self._showPrositeDialog)
        self.editMenu.add_command(label="Find Sequence Name...",
                    command=self._showFindSeqNameDialog)

        self.structureMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Structure",
                            menu=self.structureMenu)
        self.structureMenu.add_command(label="Load Structures",
                        command=self._loadStructures)
        """
        """
        self.alignDialog = self.assessDialog = self.findDialog = None
        self.prositeDialog = self.regexDialog = None
        self.associationsDialog = self.findSeqNameDialog = None
        self.saveHeaderDialog = self.alignAttrDialog = None
        self.assocInfoDialog = self.loadHeaderDialog = None
        self.identityDialog = self.colorSchemeDialog = None
        self.modellerHomologyDialog = self.fetchAnnotationsDialog = None
        self.treeDialog = self.reorderDialog = self.blastPdbDialog = None
        self.delSeqsGapsDialog = self.insertGapDialog = None
        self.addSeqDialog = self.numberingsDialog = None
        self.editKeysDialog = self.copySeqDialog = None
        self.modellerLoopsDialog = self.seqNameEditDialog = None
        self.realignDialog = None
        self.structureMenu.add_command(label="Match...",
                state='disabled', command=self._showAlignDialog)
        
        self.structureMenu.add_command(label="Assess Match...",
            state='disabled', command=self._showAssessDialog)

        if len(self.seqs) <= 1:
            state = "disabled"
        else:
            state = "normal"
        self.structureMenu.add_command(label="Modeller (homology)...",
            state=state, command=self._showModellerHomologyDialog)
        self.structureMenu.add_command(label=self.MODEL_LOOPS_MENU_TEXT,
            state="disabled", command=self._showModellerLoopsDialog)

        if chimera.openModels.list(modelTypes=[chimera.Molecule]):
            assocState = 'normal'
        else:
            assocState = 'disabled'
        self.structureMenu.add_command(label="Associations...",
            state=assocState, command=self._showAssociationsDialog)
        self.ssMenu = Tkinter.Menu(self.structureMenu)
        self.structureMenu.add_cascade(label="Secondary Structure",
                            menu=self.ssMenu)
        self.showSSVar = Tkinter.IntVar(parent)
        self.showSSVar.set(False)
        self.showPredictedSSVar = Tkinter.IntVar(parent)
        self.showPredictedSSVar.set(False)
        self.ssMenu.add_checkbutton(label="show actual",
            variable=self.showSSVar, command=lambda s=self: s.showSS(show=None))
        self.ssMenu.add_checkbutton(label="show predicted",
            variable=self.showPredictedSSVar,
            command=lambda s=self: s.showSS(show=None, ssType="predicted"))
        # actual SS part of MOD_ASSOC handler...
        self._predSSHandler = self.triggers.addHandler(ADD_SEQS,
            lambda a1, a2, a3, s=self:
            s.showSS(show=None, ssType="predicted"), None)
        """
        from chimerax.atomic import get_triggers
        self._atomic_changes_handler = get_triggers().add_handler(
            "changes", self._atomic_changes_cb)
        """TODO
        self.structureMenu.add_command(state='disabled',
                label="Select by Conservation...",
                command=lambda: self._doByConsCB("Select"))
        self.structureMenu.add_command(state='disabled',
                label="Render by Conservation...",
                command=lambda: self._doByConsCB("Render"))
        self.structureMenu.add_command(label="Expand Selection to"
                " Columns", state=assocState,
                command=self.expandSelectionByColumns)
        self._modAssocHandlerID = self.triggers.addHandler(
                    MOD_ASSOC, self._modAssocCB, None)

        self.headersMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Headers", menu=self.headersMenu)
        self.headersMenu.add_command(label="Save...",
            command=self._showSaveHeaderDialog)
        self.headersMenu.add_command(label="Load...",
                    command=self._showLoadHeaderDialog)
        self.headersMenu.add_separator()
        for trig in [ADD_HEADERS,DEL_HEADERS,SHOW_HEADERS,HIDE_HEADERS,
                                MOD_ALIGN]:
            self.triggers.addHandler(trig,
                        self._rebuildHeadersMenu, None)
        self._rebuildHeadersMenu()

        self.numberingsMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Numberings",
                        menu=self.numberingsMenu)
        self.showRulerVar = Tkinter.IntVar(self.headersMenu)
        self.showRulerVar.set(
                len(self.seqs) > 1 and self.prefs[SHOW_RULER_AT_STARTUP])
        self.numberingsMenu.add_checkbutton(label="Overall Alignment",
                        selectcolor="black",
                        variable=self.showRulerVar,
                        command=self.setRulerDisplay)
        self.numberingsMenu.add_separator()
        self.numberingsMenu.add_checkbutton(
                    label="Left Sequence",
                    selectcolor="black",
                    variable=self.leftNumberingVar,
                    command=self.setLeftNumberingDisplay)
        self.numberingsMenu.add_checkbutton(
                    label="Right Sequence",
                    selectcolor="black",
                    variable=self.rightNumberingVar,
                    command=self.setRightNumberingDisplay)
        self.numberingsMenu.add_command(
                    label="Adjust Sequence Numberings...",
                    command = self._showNumberingsDialog)

        self.treeMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Tree", menu=self.treeMenu)
        self.treeMenu.add_command(label="Load...",
                    command=self._showTreeDialog)
        self.showTreeVar = Tkinter.IntVar(self.menuBar)
        self.showTreeVar.set(True)
        self.treeMenu.add_checkbutton(label="Show Tree",
            selectcolor="black",
            variable=self.showTreeVar, command=self._showTreeCB,
            state='disabled')
        self.treeMenu.add_separator()
        self.treeMenu.add_command(label="Extract Subalignment",
            state="disabled", command=self.extractSubalignment)

        self.infoMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Info", menu=self.infoMenu)
        if len(self.seqs) == 1:
            state = "disabled"
        else:
            state = "normal"
        self.infoMenu.add_command(label="Percent Identity...",
                state=state, command=self._showIdentityDialog)
        self.infoMenu.add_command(label="Region Browser",
                    command=self.regionBrowser.enter)
        self.infoMenu.add_command(label="Blast Protein...",
                    command=self._showBlastPdbDialog)
        self.infoMenu.add_command(label="UniProt/CDD Annotations...",
                    command=self._showFetchAnnotationsDialog)
        self.preferencesMenu = Tkinter.Menu(self.menuBar)
        self.menuBar.add_cascade(label="Preferences",
                        menu=self.preferencesMenu)

        from chimera.tkgui import aquaMenuBar
        aquaMenuBar(self.menuBar, parent, row = 0, columnspan = 4)

        for tab in self.prefDialog.tabs:
            self.preferencesMenu.add_command(label=tab,
                command=lambda t=tab: [self.prefDialog.enter(),
                self.prefDialog.notebook.selectpage(t)])

        self.status("Mouse drag to create region (replacing current)\n",
            blankAfter=30, followTime=40, followWith=
            "Shift-drag to add to current region\n"
            "Control-drag to add new region")
        self._addHandlerID = chimera.openModels.addAddHandler(
                        self._newModelsCB, None)
        self._removeHandlerID = chimera.openModels.addRemoveHandler(
                        self._closeModelsCB, None)
        self._closeSessionHandlerID = chimera.triggers.addHandler(
            CLOSE_SESSION, lambda t, a1, a2, s=self: s.Quit(), None)
        self._monitorChangesHandlerID = None
        # deregister other handlers on APPQUIT...
        chimera.triggers.addHandler(chimera.APPQUIT, self.destroy, None)
        if self.autoAssociate == None:
            if len(self.seqs) == 1:
                self.intrinsicStructure = True
            else:
                self.autoAssociate = False
                self.associate(None)
        else:
            self._newModelsCB(models=chimera.openModels.list())
        self._makeSequenceRegions()
        if self.prefs[LOAD_PDB_AUTO]:
            # delay calling _loadStructures to give any structures
            # opened along with MAV a chance to load
            parent.after_idle(lambda: self._loadStructures(auto=1))
        """
        self.tool_window.manage(
            'side' if self.seq_canvas.wrap_okay() else 'top')

    def alignment_notification(self, note_name, note_data):
        alignment = self.alignment
        if note_name == alignment.NOTE_MOD_ASSOC:
            assoc_aseqs = set()
            if note_data[0] != alignment.NOTE_DEL_ASSOC:
                match_maps = note_data[1]
            else:
                match_maps = [note_data[1]['match map']]
            for match_map in match_maps:
                aseq = match_map.align_seq
                assoc_aseqs.add(aseq)
            for aseq in assoc_aseqs:
                self.seq_canvas.assoc_mod(aseq)
                self._update_errors_gaps(aseq)
            if self.alignment.intrinsic:
                self.show_ss(True)
            if hasattr(self, 'associations_tool'):
                self.associations_tool._assoc_mod(note_data)
        elif note_name == alignment.NOTE_PRE_DEL_SEQS:
            self.region_browser._pre_remove_lines(note_data)
        elif note_name == alignment.NOTE_DESTROYED:
            self.delete()
        elif note_name == alignment.NOTE_COMMAND:
            from .cmd import run
            run(self.session, self, note_data)
        self.seq_canvas.alignment_notification(note_name, note_data)

    @property
    def consensus_capitalize_theshold(self):
        return self.seq_canvas.consensus_capitalize_theshold

    @consensus_capitalize_theshold.setter
    def consensus_capitalize_theshold(self, capitalize_theshold):
        self.seq_canvas.consensus_capitalize_theshold = capitalize_theshold

    @property
    def consensus_ignores_gaps(self):
        return self.seq_canvas.consensus_ignores_gaps

    @consensus_ignores_gaps.setter
    def consensus_ignores_gaps(self, ignores_gaps):
        self.seq_canvas.consensus_ignores_gaps = ignores_gaps

    @property
    def conservation_style(self):
        return self.seq_canvas.conservation_style

    @conservation_style.setter
    def conservation_style(self, style):
        self.seq_canvas.conservation_style = style

    def delete(self):
        self.region_browser.destroy()
        self.seq_canvas.destroy()
        self.alignment.detach_viewer(self)
        for seq in self.alignment.seqs:
            seq.triggers.remove_handler(self._seq_rename_handlers[seq])
        from chimerax.atomic import get_triggers
        get_triggers().remove_handler(self._atomic_changes_handler)
        ToolInstance.delete(self)

    def fill_context_menu(self, menu, x, y):
        from PyQt5.QtWidgets import QAction
        file_menu = menu.addMenu("File")
        save_as_menu = file_menu.addMenu("Save As")
        from chimerax.core.commands import run, StringArg
        fmts = [
            fmt for fmt in self.session.save_command.save_data_formats
            if fmt.category == "Sequence"
        ]
        fmts.sort(key=lambda fmt: fmt.synopsis.casefold())
        for fmt in fmts:
            action = QAction(fmt.synopsis, save_as_menu)
            action.triggered.connect(lambda arg, fmt=fmt: run(
                self.session, "save browse format %s alignment %s" %
                (fmt.nicknames[0], StringArg.unparse(self.alignment.ident))))
            save_as_menu.addAction(action)
        scf_action = QAction("Load Sequence Coloring File...", file_menu)
        scf_action.triggered.connect(lambda arg: self.load_scf_file(None))
        file_menu.addAction(scf_action)

        structure_menu = menu.addMenu("Structure")
        assoc_action = QAction("Associations...", structure_menu)
        assoc_action.triggered.connect(lambda arg: self.show_associations())
        from chimerax.atomic import AtomicStructure
        for m in self.session.models:
            if isinstance(m, AtomicStructure):
                break
        else:
            assoc_action.setEnabled(False)
        structure_menu.addAction(assoc_action)

        headers_menu = menu.addMenu("Headers")
        headers = self.alignment.headers
        headers.sort(key=lambda hdr: hdr.ident.casefold())
        from chimerax.core.commands import run
        for hdr in headers:
            action = QAction(hdr.name, headers_menu)
            action.setCheckable(True)
            action.setChecked(hdr.shown)
            if not hdr.relevant:
                action.setEnabled(False)
            align_arg = "%s " % self.alignment if len(
                self.session.alignments.alignments) > 1 else ""
            action.triggered.connect(
                lambda checked, hdr=hdr, align_arg=align_arg, self=self: run(
                    self.session, "seq header %s%s %s" %
                    (align_arg, hdr.ident, "show" if checked else "hide")))
            headers_menu.addAction(action)
        headers_menu.addSeparator()
        hdr_save_menu = headers_menu.addMenu("Save")
        for hdr in headers:
            if not hdr.relevant:
                continue
            action = QAction(hdr.name, hdr_save_menu)
            align_arg = "%s " % self.alignment if len(
                self.session.alignments.alignments) > 1 else ""
            action.triggered.connect(
                lambda checked, hdr=hdr, align_arg=align_arg, self=self: run(
                    self.session, "seq header %s%s save browse" %
                    (align_arg, hdr.ident)))
            hdr_save_menu.addAction(action)

        # Whenever Region Browser and UniProt Annotations happen, the thought is to
        # put them in an "Annotations" menu (rather than "Info")

        tools_menu = menu.addMenu("Tools")
        comp_model_action = QAction("Modeller Comparative Modeling...",
                                    tools_menu)
        comp_model_action.triggered.connect(lambda arg: run(
            self.session, "ui tool show 'Modeller Comparative'"))
        if not self.alignment.associations:
            comp_model_action.setEnabled(False)
        tools_menu.addAction(comp_model_action)
        if len(self.alignment.seqs) == 1:
            blast_action = QAction("Blast Protein...", tools_menu)
            blast_action.triggered.connect(lambda arg: run(
                self.session, "blastprotein %s" %
                (StringArg.unparse("%s:1" % self.alignment.ident))))
            tools_menu.addAction(blast_action)
        else:
            blast_menu = tools_menu.addMenu("Blast Protein")
            for i, seq in enumerate(self.alignment.seqs):
                blast_action = QAction(seq.name, blast_menu)
                blast_action.triggered.connect(lambda arg: run(
                    self.session, "blastprotein %s" % (StringArg.unparse(
                        "%s:%d" % (self.alignment.ident, i + 1)))))
                blast_menu.addAction(blast_action)

        settings_action = QAction("Settings...", menu)
        settings_action.triggered.connect(lambda arg: self.show_settings())
        menu.addAction(settings_action)

    def load_scf_file(self, path, color_structures=None):
        """color_structures=None means use user's preference setting"""
        self.region_browser.load_scf_file(path, color_structures)

    def new_region(self, **kw):
        if 'blocks' in kw:
            # interpret numeric values as indices into sequences
            blocks = kw['blocks']
            if blocks and isinstance(blocks[0][0], int):
                blocks = [(self.alignment.seqs[i1], self.alignment.seqs[i2],
                           i3, i4) for i1, i2, i3, i4 in blocks]
                kw['blocks'] = blocks
        if 'columns' in kw:
            # in lieu of specifying blocks, allow list of columns
            # (implicitly all rows); list should already be in order
            left = right = None
            blocks = []
            for col in kw['columns']:
                if left is None:
                    left = right = col
                elif col > right + 1:
                    blocks.append((self.alignment.seqs[0],
                                   self.alignment.seqs[-1], left, right))
                    left = right = col
                else:
                    right = col
            if left is not None:
                blocks.append((self.alignment.seqs[0], self.alignment.seqs[-1],
                               left, right))
            kw['blocks'] = blocks
            del kw['columns']
        return self.region_browser.new_region(**kw)

    def show_associations(self):
        if not hasattr(self, "associations_tool"):
            from .associations_tool import AssociationsTool
            self.associations_tool = AssociationsTool(
                self,
                self.tool_window.create_child_window(
                    "Chain-Sequence Associations", close_destroys=False))
            self.associations_tool.tool_window.manage(None)
        self.associations_tool.tool_window.shown = True

    def show_settings(self):
        if not hasattr(self, "settings_tool"):
            from .settings_tool import SettingsTool
            self.settings_tool = SettingsTool(
                self,
                self.tool_window.create_child_window(
                    "Sequence Viewer Settings", close_destroys=False))
            self.settings_tool.tool_window.manage(None)
        self.settings_tool.tool_window.shown = True

    def show_ss(self, show=True):
        # show == None means don't change show states, but update regions
        # ... not yet implemented, so see if the regions exist and their
        # display is True...
        rb = self.region_browser
        if show == None:
            hreg = rb.get_region(rb.ACTUAL_HELICES_REG_NAME)
            if not hreg:
                return
            show = hreg.shown
        rb.show_ss(show)

    @classmethod
    def restore_snapshot(cls, session, data):
        inst = super().restore_snapshot(session, data['ToolInstance'])
        inst._finalize_init(data['alignment'])
        inst.region_browser.restore_state(data['region browser'])
        if 'seq canvas' in data:
            inst.seq_canvas.restore_state(session, data['seq canvas'])
        return inst

    SESSION_SAVE = True

    def take_snapshot(self, session, flags):
        data = {
            'ToolInstance': ToolInstance.take_snapshot(self, session, flags),
            'alignment': self.alignment,
            'region browser': self.region_browser.save_state(),
            'seq canvas': self.seq_canvas.save_state()
        }
        return data

    def _atomic_changes_cb(self, trig_name, changes):
        if "ss_type changed" in changes.residue_reasons():
            self.show_ss(show=None)

    def _update_errors_gaps(self, aseq):
        if not self.settings.error_region_shown and not self.settings.gap_region_shown:
            return
        a_ref_seq = getattr(aseq, 'residue_sequence', aseq.ungapped())
        errors = [0] * len(a_ref_seq)
        gaps = [0] * len(a_ref_seq)
        from chimerax.atomic import Sequence
        for chain, match_map in aseq.match_maps.items():
            for i, char in enumerate(a_ref_seq):
                try:
                    res = match_map[i]
                except KeyError:
                    gaps[i] += 1
                else:
                    if Sequence.rname3to1(res.name) != char.upper():
                        errors[i] += 1
        partial_error_blocks, full_error_blocks = [], []
        partial_gap_blocks, full_gap_blocks = [], []
        num_assocs = len(aseq.match_maps)
        if num_assocs > 0:
            for partial, full, check in [
                (partial_error_blocks, full_error_blocks, errors),
                (partial_gap_blocks, full_gap_blocks, gaps)
            ]:
                cur_partial_block = cur_full_block = None
                for i, check_num in enumerate(check):
                    gapped_i = aseq.ungapped_to_gapped(i)
                    if check_num == num_assocs:
                        if cur_full_block:
                            cur_full_block[-1] = gapped_i
                        else:
                            cur_full_block = [aseq, aseq, gapped_i, gapped_i]
                            full.append(cur_full_block)
                        if cur_partial_block:
                            cur_partial_block = None
                    else:
                        if cur_full_block:
                            cur_full_block = None
                        if check_num > 0:
                            if cur_partial_block:
                                cur_partial_block[-1] = gapped_i
                            else:
                                cur_partial_block = [
                                    aseq, aseq, gapped_i, gapped_i
                                ]
                                partial.append(cur_partial_block)
                        elif cur_partial_block:
                            cur_partial_block = None

        for shown, region_name_part, partial_blocks, full_blocks, fills, outlines in [
            (self.settings.error_region_shown, self.ERROR_REGION_STRING,
             partial_error_blocks, full_error_blocks,
             self.settings.error_region_interiors,
             self.settings.error_region_borders),
            (self.settings.gap_region_shown, self.GAP_REGION_STRING,
             partial_gap_blocks, full_gap_blocks,
             self.settings.gap_region_interiors,
             self.settings.gap_region_borders)
        ]:
            if not shown:
                continue
            full_fill, partial_fill = fills
            full_outline, partial_outline = outlines
            for region_name_start, blocks, fill, outline in [
                (region_name_part, full_blocks, full_fill, full_outline),
                ("partial " + region_name_part, partial_blocks, partial_fill,
                 partial_outline)
            ]:
                region_name = "%s of %s" % (region_name_start, aseq.name)
                old_reg = self.region_browser.get_region(region_name,
                                                         create=False)
                if old_reg:
                    self.region_browser.delete_region(old_reg)
                if blocks:
                    self.region_browser.new_region(region_name,
                                                   blocks=blocks,
                                                   fill=fill,
                                                   outline=outline,
                                                   sequence=aseq,
                                                   cover_gaps=False)