Example #1
0
    def loadSequenceTraces(self):
        self.numseqs = self.cons.getNumSeqs()
        self.seqt1 = self.cons.getSequenceTrace(0)
        if self.numseqs == 2:
            self.seqt2 = self.cons.getSequenceTrace(1)
        else:
            self.seqt2 = None

        viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt1))
        if self.numseqs == 2:
            viewer = FwdRevSTVDecorator(viewer)
        self.viewers = [ viewer ]
        if self.seqt2 != None:
            viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt2))
            self.viewers.append(FwdRevSTVDecorator(viewer))

        self.consview = ScrolledConsensusSequenceViewer(self.cons)

        # add the sequence trace layout to the window
        self.stlayout = SequenceTraceLayout(self.consview, self.viewers)
        self.vbox.pack_start(self.stlayout, expand=True, fill=True)

        # Register callbacks for the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().registerObserver('consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().registerObserver('selection_state', self.selectStateChange)

        # register callbacks for the consensus sequence model
        self.cons.registerObserver('undo_state_changed', self.undoStateChanged)
        self.cons.registerObserver('redo_state_changed', self.redoStateChanged)

        title = 'Trace View: ' + self.seqt1.getFileName()
        if self.numseqs == 2:
            title += ', ' + self.seqt2.getFileName()
        self.set_title(title)
Example #2
0
    def loadSequenceTraces(self):
        self.numseqs = self.cons.getNumSeqs()
        self.seqt1 = self.cons.getSequenceTrace(0)
        if self.numseqs == 2:
            self.seqt2 = self.cons.getSequenceTrace(1)
        else:
            self.seqt2 = None

        viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt1))
        if self.numseqs == 2:
            viewer = FwdRevSTVDecorator(viewer)
        self.viewers = [viewer]
        if self.seqt2 != None:
            viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt2))
            self.viewers.append(FwdRevSTVDecorator(viewer))

        self.consview = ScrolledConsensusSequenceViewer(self.cons)

        # add the sequence trace layout to the window
        self.stlayout = SequenceTraceLayout(self.consview, self.viewers)
        self.vbox.pack_start(self.stlayout, expand=True, fill=True)

        # Register callbacks for the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().registerObserver(
            'consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().registerObserver(
            'selection_state', self.selectStateChange)

        # register callbacks for the consensus sequence model
        self.cons.registerObserver('undo_state_changed', self.undoStateChanged)
        self.cons.registerObserver('redo_state_changed', self.redoStateChanged)

        title = 'Trace View: ' + self.seqt1.getFileName()
        if self.numseqs == 2:
            title += ', ' + self.seqt2.getFileName()
        self.set_title(title)
Example #3
0
class TraceWindow(gtk.Window, CommonDialogs, Observable):
    def __init__(self, mod_consseq_builder, is_mainwindow=False, id_num=-1):
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)

        self.is_mainwindow = is_mainwindow
        self.id_num = id_num
        self.cons = mod_consseq_builder
        self.infowin = None

        # initialize the observable events for this class
        self.defineObservableEvents(['consensus_saved'])

        # initialize the window GUI elements and event handlers
        self.connect('destroy', self.destroyWindow)

        self.vbox = gtk.VBox(False, 0)
        self.add(self.vbox)

        # create the menus and toolbar
        menuxml = '''<menubar name="menubar">
        <menu action="File">
            <menuitem action="Save_Consens" />
            <separator />
            <menuitem action="Export_Alignment" />
            <menuitem action="Export_Consensus" />
            <menuitem action="Export_Raw" />
            <separator />
            <menuitem action="File_Info" />
            <separator />
            <menuitem action="Close" />
        </menu>
        <menu action="Edit">
            <menuitem action="Undo" />
            <menuitem action="Redo" />
            <separator />
            <menuitem action="Copy" />
            <menuitem action="Delete" />
            <menuitem action="Modify" />
            <separator />
            <menuitem action="Recalc_Consens" />
        </menu>
        <menu action="View">
            <menuitem action="Scroll_Lock" />
        </menu>
        </menubar>
        <popup name="editpopup">
            <menuitem action="Copy" />
            <menuitem action="Delete" />
            <menuitem action="Modify" />
            <separator />
            <menuitem action="Recalc_Consens" />
        </popup>
        <toolbar name="toolbar">
            <toolitem action="File_Info" />
            <separator />
            <toolitem action="Save_Consens" />
            <separator />
            <toolitem action="Undo" />
            <toolitem action="Redo" />
            <separator />
            <toolitem action="Copy" />
            <toolitem action="Delete" />
            <toolitem action="Modify" />
        </toolbar>'''

        # These actions are (usually) always enabled.
        self.main_ag = gtk.ActionGroup('main_actions')
        self.main_ag.add_actions([
            ('File', None, '_File'),
            ('Save_Consens', gtk.STOCK_SAVE, '_Save working sequence to project', None, 'Save the working sequence to the project', self.saveConsensus),
            ('Export_Consensus', None, 'Export w_orking sequence...', None, 'Export the working sequence to a file', self.exportConsensus),
            ('Export_Raw', None, 'Export _raw sequence(s)...', None, 'Export the un-edited sequence(s) to a file', self.exportRawSequence),
            ('File_Info', gtk.STOCK_INFO, '_Information...', None, 'View detailed information about the file(s)', self.fileInfo),
            ('Close', gtk.STOCK_CLOSE, '_Close', None, 'Close this window', self.closeWindow),
            ('Edit', None, '_Edit'),
            ('Recalc_Consens', None, '_Recalculate working seq.', None, 'Recalculate the working sequence', self.recalcConsensus),
            ('View', None, '_View')])

        # These actions are enabled only when there are two sequencing traces in the window.
        self.twotrace_ag = gtk.ActionGroup('twotrace_actions')
        self.twotrace_ag.add_actions([
            ('Export_Alignment', None, 'Export _alignment...', None, 'Export the aligned forward and reverse sequences', self.exportAlignment)])
        self.twotrace_ag.add_toggle_actions([
            ('Scroll_Lock', None, '_Synchronize trace scrolling', None, 'Synchronizes the scrolling of the forward and reverse traces', self.lockScrolling, True)])

        # these actions are for common edit commands
        self.edit_ag = gtk.ActionGroup('edite_actions')
        self.edit_ag.add_actions([
            ('Undo', gtk.STOCK_UNDO, '_Undo', '<ctl>z', 'Undo the last change to the working sequence', self.undoConsChange),
            ('Redo', gtk.STOCK_REDO, '_Redo', '<ctl>y', 'Redo the last change to the working sequence', self.redoConsChange)])

        # these actions are only enabled when there is an active selection
        self.sel_edit_ag = gtk.ActionGroup('selected_edit_actions')
        self.sel_edit_ag.add_actions([
            ('Copy', gtk.STOCK_COPY, '_Copy selected base(s) to clipboard', '<ctl>c', 'Copy the selected base(s) to the system clipboard', self.copyConsBases),
            ('Delete', gtk.STOCK_DELETE, '_Delete selected base(s)', None, 'Delete the selected base(s) from the working sequence', self.deleteConsBases),
            ('Modify', gtk.STOCK_EDIT, '_Modify selected base(s)...', None, 'Edit the selected base(s)', self.editConsBases)])

        self.uim = gtk.UIManager()
        self.add_accel_group(self.uim.get_accel_group())
        self.uim.insert_action_group(self.main_ag, 0)
        self.uim.insert_action_group(self.twotrace_ag, 0)
        self.uim.insert_action_group(self.edit_ag, 0)
        self.uim.insert_action_group(self.sel_edit_ag, 0)
        self.uim.add_ui_from_string(menuxml)
        self.vbox.pack_start(self.uim.get_widget('/menubar'), expand=False, fill=False)

        toolbar_hbox = gtk.HBox()
        self.uim.get_widget('/toolbar').set_show_arrow(False)
        self.uim.get_widget('/toolbar').set_icon_size(gtk.ICON_SIZE_LARGE_TOOLBAR)
        self.uim.get_widget('/toolbar').set_style(gtk.TOOLBAR_ICONS)
        toolbar_hbox.pack_start(self.uim.get_widget('/toolbar'), expand=False, fill=False)
        self.vbox.pack_start(toolbar_hbox, expand=False, fill=False)

        # disable some menus/toolbar buttons by default
        self.edit_ag.get_action('Undo').set_sensitive(False)
        self.edit_ag.get_action('Redo').set_sensitive(False)
        self.sel_edit_ag.set_sensitive(False)

        self.loadSequenceTraces()

        # If there is only one trace file, disable the actions that require two traces.
        if self.numseqs < 2:
            self.twotrace_ag.set_sensitive(False)

        # get the trace file(s) toolbar
        trace_tb = self.stlayout.getTraceToolBar()
        for cnt in range(0, 2):
            blank = gtk.SeparatorToolItem()
            blank.set_draw(False)
            #self.uim.get_widget('/toolbar').insert(blank, 0)
            trace_tb.insert(blank, 0)
        toolbar_hbox.pack_start(trace_tb, True, True)

        # add a consensus sequence status bar at the bottom of the window
        sbar = ConsensSeqStatusBar(self.cons)
        self.vbox.pack_start(sbar, False)

        self.vbox.show_all()
        self.vbox.show()

        # by default, do not show the "Save_Consens" action UI elements
        self.main_ag.get_action('Save_Consens').set_visible(False)

        self.setDefaultGeometry()

        self.show()
        self.set_focus(None)

    def setDefaultGeometry(self):
        #print self.get_size()
        width, height = self.get_size()

        screen = self.get_screen()

        # calculate the default width based on the screen size
        new_width = (screen.get_width() * 5) / 6

        # calculate the default height based on the preferred size of the sequence trace viewers
        v_height = self.viewers[0].getDefaultHeight()
        v_pref_height = self.viewers[0].getPreferredHeight()
        diff = v_pref_height - v_height

        new_height = height + self.numseqs * diff

        #print new_width, new_height
        self.set_default_size(new_width, new_height)
        self.set_position(gtk.WIN_POS_CENTER)

    def setSaveEnabled(self, state):
        self.main_ag.get_action('Save_Consens').set_sensitive(state)
        
    def registerObserver(self, event_name, handler):
        """
        Extends the registerObserver() method in Observable to allow GUI elements to respond
        to observer registration.
        """
        Observable.registerObserver(self, event_name, handler)

        if event_name == 'consensus_saved':
            # Show the "Save_Consens" action UI elements since someone's actually listening for this signal.
            self.main_ag.get_action('Save_Consens').set_visible(True)

    def saveConsensus(self, widget):
        self.notifyObservers('consensus_saved',
                (self, self.cons.getCompactConsensus(), self.cons.getConsensus()))

    def getIdNum(self):
        return self.id_num

    def loadSequenceTraces(self):
        self.numseqs = self.cons.getNumSeqs()
        self.seqt1 = self.cons.getSequenceTrace(0)
        if self.numseqs == 2:
            self.seqt2 = self.cons.getSequenceTrace(1)
        else:
            self.seqt2 = None

        viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt1))
        if self.numseqs == 2:
            viewer = FwdRevSTVDecorator(viewer)
        self.viewers = [ viewer ]
        if self.seqt2 != None:
            viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt2))
            self.viewers.append(FwdRevSTVDecorator(viewer))

        self.consview = ScrolledConsensusSequenceViewer(self.cons)

        # add the sequence trace layout to the window
        self.stlayout = SequenceTraceLayout(self.consview, self.viewers)
        self.vbox.pack_start(self.stlayout, expand=True, fill=True)

        # Register callbacks for the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().registerObserver('consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().registerObserver('selection_state', self.selectStateChange)

        # register callbacks for the consensus sequence model
        self.cons.registerObserver('undo_state_changed', self.undoStateChanged)
        self.cons.registerObserver('redo_state_changed', self.redoStateChanged)

        title = 'Trace View: ' + self.seqt1.getFileName()
        if self.numseqs == 2:
            title += ', ' + self.seqt2.getFileName()
        self.set_title(title)

    def lockScrolling(self, widget):
        """
        Responds to clicks on the synchronize scrolling checkbox menu item.
        """
        if widget.get_active():
            self.stlayout.lockScrolling()
        else:
            self.stlayout.unlockScrolling()

    def consensusSeqClicked(self, select_start, select_end, event):
        if event.button == 3:
            self.uim.get_widget('/editpopup').popup(None, None, None, event.button, event.time)

    def selectStateChange(self, selection_exists):
        if selection_exists:
            self.sel_edit_ag.set_sensitive(True)
        else:
            self.sel_edit_ag.set_sensitive(False)

    def undoStateChanged(self, has_undo):
        if has_undo:
            self.edit_ag.get_action('Undo').set_sensitive(True)
        else:
            self.edit_ag.get_action('Undo').set_sensitive(False)

    def redoStateChanged(self, has_redo):
        if has_redo:
            self.edit_ag.get_action('Redo').set_sensitive(True)
        else:
            self.edit_ag.get_action('Redo').set_sensitive(False)

    def undoConsChange(self, widget):
        self.cons.undo()
        self.setSaveEnabled(True)

    def redoConsChange(self, widget):
        self.cons.redo()
        self.setSaveEnabled(True)

    def copyConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        seq = self.cons.getConsensus(sel[0], sel[1])

        pyperclip.copy(seq)

    def deleteConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        self.cons.deleteBases(sel[0], sel[1])
        self.setSaveEnabled(True)

    def editConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        seq = self.cons.getConsensus(sel[0], sel[1])

        # Create a text-entry dialog to allow the user to modify the selected base(s).
        diag = EntryDialog(self, 'Edit Sequence',
                'You may make changes to the selected base(s) below.\n\nOriginal sequence: {0} ({1} bases)\n'.format(seq, len(seq)),
                seq, 50)

        # Build a regular expression object for checking if the characters in the new
        # string are all valid bases.
        reo = re.compile('[' + ''.join(ConsensSeqBuilder.allbases) + ' ]*')

        # Display the dialog until the user presses cancel, closes the dialog, or submits
        # an acceptable string.
        stringok = False
        while not(stringok):
            response = diag.run()
            if (response == gtk.RESPONSE_CANCEL) or (response == gtk.RESPONSE_DELETE_EVENT):
                break

            newseq = diag.get_text().upper()
            match = reo.match(newseq)

            # Verify that newseq is of the correct length and only contains valid bases.
            if len(newseq) != len(seq):
                self.showMessage('The number of bases in the edited sequence must match the the selection.')
            elif match.end() - match.start() != len(newseq):
                self.showMessage('The edited sequence contains invalid characters.  You may only use IUPAC nucleotide codes or spaces.')
            else:
                stringok = True

        diag.destroy()
        if response == gtk.RESPONSE_OK:
            self.cons.modifyBases(sel[0], sel[1], newseq)

        self.setSaveEnabled(True)

    def recalcConsensus(self, widget):
        response = self.showYesNoDialog('Are you sure you want to recalculate the working sequence?  This will overwrite any edits you have made.')
        if response != gtk.RESPONSE_YES:
            return

        self.cons.recalcConsensusSequence()
        self.setSaveEnabled(True)

    def fileInfo(self, widget):
        seqtraces = list()

        self.numseqs = self.cons.getNumSeqs()
        seqtraces.append(self.cons.getSequenceTrace(0))
        if self.numseqs == 2:
            seqtraces.append(self.cons.getSequenceTrace(1))

        if self.infowin != None:
            self.infowin.present()
        else:
            self.infowin = TraceFileInfoWin(seqtraces)
            self.infowin.connect('destroy', self.fileInfoDestroyed)

    def fileInfoDestroyed(self, win):
        self.infowin = None

    def exportConsensus(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Consensus Sequence')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequence
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage('The file "' + fname + '" could not be opened for writing.  ' +
                    'Verify that you have permission to write to the specified file and directory.')
            return

        desc = 'consensus'
        seqfname = self.cons.getSequenceTrace(0).getFileName()
        if self.numseqs == 2:
            seqfname += ', ' + self.cons.getSequenceTrace(1).getFileName()
        sw.addUnalignedSequence(self.cons.getCompactConsensus(), seqfname, desc)

        sw.write()

    def exportRawSequence(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Raw Sequence(s)')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequence
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage('The file "' + fname + '" could not be opened for writing.  ' +
                    'Verify that you have permission to write to the specified file and directory.')
            return

        for cnt in range(0, self.numseqs):
            seqfname = self.cons.getSequenceTrace(cnt).getFileName()
            desc = 'raw sequence'
            if self.cons.getSequenceTrace(cnt).isReverseComplemented():
                desc += ' (reverse complemented)'
            sw.addUnalignedSequence(self.cons.getSequenceTrace(cnt).getBaseCalls(), seqfname, desc)

        sw.write()

    def exportAlignment(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Alignment')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequences
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage('The file "' + fname + '" could not be opened for writing.  ' +
                    'Verify that you have permission to write to the specified file and directory.')
            return

        for cnt in range(0, self.numseqs):
            seqfname = self.cons.getSequenceTrace(cnt).getFileName()
            desc = 'sequence ' + str(cnt)
            if self.cons.getSequenceTrace(cnt).isReverseComplemented():
                desc += ' (reverse complemented)'
            sw.addAlignedSequence(self.cons.getAlignedSequence(cnt), seqfname, desc)

        sw.write()

    def closeWindow(self, widget):
        self.destroy()

    def destroyWindow(self, widget, data=None):
        # close the file information window, if it exists
        if self.infowin != None:
            self.infowin.destroy()

        # Unregister this window as an observer of the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().unregisterObserver('consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().unregisterObserver('selection_state', self.selectStateChange)

        # Unregister this window as an observer of the consensus sequence.
        self.cons.unregisterObserver('undo_state_changed', self.undoStateChanged)
        self.cons.unregisterObserver('redo_state_changed', self.redoStateChanged)

        if self.is_mainwindow:
            gtk.main_quit()
Example #4
0
class TraceWindow(gtk.Window, CommonDialogs, Observable):
    def __init__(self, mod_consseq_builder, is_mainwindow=False, id_num=-1):
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)

        self.is_mainwindow = is_mainwindow
        self.id_num = id_num
        self.cons = mod_consseq_builder
        self.infowin = None

        # initialize the observable events for this class
        self.defineObservableEvents(['consensus_saved'])

        # initialize the window GUI elements and event handlers
        self.connect('destroy', self.destroyWindow)

        self.vbox = gtk.VBox(False, 0)
        self.add(self.vbox)

        # create the menus and toolbar
        menuxml = '''<menubar name="menubar">
        <menu action="File">
            <menuitem action="Save_Consens" />
            <separator />
            <menuitem action="Export_Alignment" />
            <menuitem action="Export_Consensus" />
            <menuitem action="Export_Raw" />
            <separator />
            <menuitem action="File_Info" />
            <separator />
            <menuitem action="Close" />
        </menu>
        <menu action="Edit">
            <menuitem action="Undo" />
            <menuitem action="Redo" />
            <separator />
            <menuitem action="Copy" />
            <menuitem action="Delete" />
            <menuitem action="Modify" />
            <separator />
            <menuitem action="Recalc_Consens" />
        </menu>
        <menu action="View">
            <menuitem action="Scroll_Lock" />
        </menu>
        </menubar>
        <popup name="editpopup">
            <menuitem action="Copy" />
            <menuitem action="Delete" />
            <menuitem action="Modify" />
            <separator />
            <menuitem action="Recalc_Consens" />
        </popup>
        <toolbar name="toolbar">
            <toolitem action="File_Info" />
            <separator />
            <toolitem action="Save_Consens" />
            <separator />
            <toolitem action="Undo" />
            <toolitem action="Redo" />
            <separator />
            <toolitem action="Copy" />
            <toolitem action="Delete" />
            <toolitem action="Modify" />
        </toolbar>'''

        # These actions are (usually) always enabled.
        self.main_ag = gtk.ActionGroup('main_actions')
        self.main_ag.add_actions([
            ('File', None, '_File'),
            ('Save_Consens', gtk.STOCK_SAVE,
             '_Save working sequence to project', None,
             'Save the working sequence to the project', self.saveConsensus),
            ('Export_Consensus', None, 'Export w_orking sequence...', None,
             'Export the working sequence to a file', self.exportConsensus),
            ('Export_Raw', None, 'Export _raw sequence(s)...', None,
             'Export the un-edited sequence(s) to a file',
             self.exportRawSequence),
            ('File_Info', gtk.STOCK_INFO, '_Information...', None,
             'View detailed information about the file(s)', self.fileInfo),
            ('Close', gtk.STOCK_CLOSE, '_Close', None, 'Close this window',
             self.closeWindow), ('Edit', None, '_Edit'),
            ('Recalc_Consens', None, '_Recalculate working seq.', None,
             'Recalculate the working sequence', self.recalcConsensus),
            ('View', None, '_View')
        ])

        # These actions are enabled only when there are two sequencing traces in the window.
        self.twotrace_ag = gtk.ActionGroup('twotrace_actions')
        self.twotrace_ag.add_actions([
            ('Export_Alignment', None, 'Export _alignment...', None,
             'Export the aligned forward and reverse sequences',
             self.exportAlignment)
        ])
        self.twotrace_ag.add_toggle_actions([
            ('Scroll_Lock', None, '_Synchronize trace scrolling', None,
             'Synchronizes the scrolling of the forward and reverse traces',
             self.lockScrolling, True)
        ])

        # these actions are for common edit commands
        self.edit_ag = gtk.ActionGroup('edite_actions')
        self.edit_ag.add_actions([
            ('Undo', gtk.STOCK_UNDO, '_Undo', '<ctl>z',
             'Undo the last change to the working sequence',
             self.undoConsChange),
            ('Redo', gtk.STOCK_REDO, '_Redo', '<ctl>y',
             'Redo the last change to the working sequence',
             self.redoConsChange)
        ])

        # these actions are only enabled when there is an active selection
        self.sel_edit_ag = gtk.ActionGroup('selected_edit_actions')
        self.sel_edit_ag.add_actions([
            ('Copy', gtk.STOCK_COPY, '_Copy selected base(s) to clipboard',
             '<ctl>c', 'Copy the selected base(s) to the system clipboard',
             self.copyConsBases),
            ('Delete', gtk.STOCK_DELETE, '_Delete selected base(s)', None,
             'Delete the selected base(s) from the working sequence',
             self.deleteConsBases),
            ('Modify', gtk.STOCK_EDIT, '_Modify selected base(s)...', None,
             'Edit the selected base(s)', self.editConsBases)
        ])

        self.uim = gtk.UIManager()
        self.add_accel_group(self.uim.get_accel_group())
        self.uim.insert_action_group(self.main_ag, 0)
        self.uim.insert_action_group(self.twotrace_ag, 0)
        self.uim.insert_action_group(self.edit_ag, 0)
        self.uim.insert_action_group(self.sel_edit_ag, 0)
        self.uim.add_ui_from_string(menuxml)
        self.vbox.pack_start(self.uim.get_widget('/menubar'),
                             expand=False,
                             fill=False)

        toolbar_hbox = gtk.HBox()
        self.uim.get_widget('/toolbar').set_show_arrow(False)
        self.uim.get_widget('/toolbar').set_icon_size(
            gtk.ICON_SIZE_LARGE_TOOLBAR)
        self.uim.get_widget('/toolbar').set_style(gtk.TOOLBAR_ICONS)
        toolbar_hbox.pack_start(self.uim.get_widget('/toolbar'),
                                expand=False,
                                fill=False)
        self.vbox.pack_start(toolbar_hbox, expand=False, fill=False)

        # disable some menus/toolbar buttons by default
        self.edit_ag.get_action('Undo').set_sensitive(False)
        self.edit_ag.get_action('Redo').set_sensitive(False)
        self.sel_edit_ag.set_sensitive(False)

        self.loadSequenceTraces()

        # If there is only one trace file, disable the actions that require two traces.
        if self.numseqs < 2:
            self.twotrace_ag.set_sensitive(False)

        # get the trace file(s) toolbar
        trace_tb = self.stlayout.getTraceToolBar()
        for cnt in range(0, 2):
            blank = gtk.SeparatorToolItem()
            blank.set_draw(False)
            #self.uim.get_widget('/toolbar').insert(blank, 0)
            trace_tb.insert(blank, 0)
        toolbar_hbox.pack_start(trace_tb, True, True)

        # add a consensus sequence status bar at the bottom of the window
        sbar = ConsensSeqStatusBar(self.cons)
        self.vbox.pack_start(sbar, False)

        self.vbox.show_all()
        self.vbox.show()

        # by default, do not show the "Save_Consens" action UI elements
        self.main_ag.get_action('Save_Consens').set_visible(False)

        self.setDefaultGeometry()

        self.show()
        self.set_focus(None)

    def setDefaultGeometry(self):
        #print self.get_size()
        width, height = self.get_size()

        screen = self.get_screen()

        # calculate the default width based on the screen size
        new_width = (screen.get_width() * 5) / 6

        # calculate the default height based on the preferred size of the sequence trace viewers
        v_height = self.viewers[0].getDefaultHeight()
        v_pref_height = self.viewers[0].getPreferredHeight()
        diff = v_pref_height - v_height

        new_height = height + self.numseqs * diff

        #print new_width, new_height
        self.set_default_size(new_width, new_height)
        self.set_position(gtk.WIN_POS_CENTER)

    def setSaveEnabled(self, state):
        self.main_ag.get_action('Save_Consens').set_sensitive(state)

    def registerObserver(self, event_name, handler):
        """
        Extends the registerObserver() method in Observable to allow GUI elements to respond
        to observer registration.
        """
        Observable.registerObserver(self, event_name, handler)

        if event_name == 'consensus_saved':
            # Show the "Save_Consens" action UI elements since someone's actually listening for this signal.
            self.main_ag.get_action('Save_Consens').set_visible(True)

    def saveConsensus(self, widget):
        self.notifyObservers(
            'consensus_saved',
            (self, self.cons.getCompactConsensus(), self.cons.getConsensus()))

    def getIdNum(self):
        return self.id_num

    def loadSequenceTraces(self):
        self.numseqs = self.cons.getNumSeqs()
        self.seqt1 = self.cons.getSequenceTrace(0)
        if self.numseqs == 2:
            self.seqt2 = self.cons.getSequenceTrace(1)
        else:
            self.seqt2 = None

        viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt1))
        if self.numseqs == 2:
            viewer = FwdRevSTVDecorator(viewer)
        self.viewers = [viewer]
        if self.seqt2 != None:
            viewer = ScrollAndZoomSTVDecorator(SequenceTraceViewer(self.seqt2))
            self.viewers.append(FwdRevSTVDecorator(viewer))

        self.consview = ScrolledConsensusSequenceViewer(self.cons)

        # add the sequence trace layout to the window
        self.stlayout = SequenceTraceLayout(self.consview, self.viewers)
        self.vbox.pack_start(self.stlayout, expand=True, fill=True)

        # Register callbacks for the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().registerObserver(
            'consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().registerObserver(
            'selection_state', self.selectStateChange)

        # register callbacks for the consensus sequence model
        self.cons.registerObserver('undo_state_changed', self.undoStateChanged)
        self.cons.registerObserver('redo_state_changed', self.redoStateChanged)

        title = 'Trace View: ' + self.seqt1.getFileName()
        if self.numseqs == 2:
            title += ', ' + self.seqt2.getFileName()
        self.set_title(title)

    def lockScrolling(self, widget):
        """
        Responds to clicks on the synchronize scrolling checkbox menu item.
        """
        if widget.get_active():
            self.stlayout.lockScrolling()
        else:
            self.stlayout.unlockScrolling()

    def consensusSeqClicked(self, select_start, select_end, event):
        if event.button == 3:
            self.uim.get_widget('/editpopup').popup(None, None, None,
                                                    event.button, event.time)

    def selectStateChange(self, selection_exists):
        if selection_exists:
            self.sel_edit_ag.set_sensitive(True)
        else:
            self.sel_edit_ag.set_sensitive(False)

    def undoStateChanged(self, has_undo):
        if has_undo:
            self.edit_ag.get_action('Undo').set_sensitive(True)
        else:
            self.edit_ag.get_action('Undo').set_sensitive(False)

    def redoStateChanged(self, has_redo):
        if has_redo:
            self.edit_ag.get_action('Redo').set_sensitive(True)
        else:
            self.edit_ag.get_action('Redo').set_sensitive(False)

    def undoConsChange(self, widget):
        self.cons.undo()
        self.setSaveEnabled(True)

    def redoConsChange(self, widget):
        self.cons.redo()
        self.setSaveEnabled(True)

    def copyConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        seq = self.cons.getConsensus(sel[0], sel[1])

        pyperclip.copy(seq)

    def deleteConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        self.cons.deleteBases(sel[0], sel[1])
        self.setSaveEnabled(True)

    def editConsBases(self, widget):
        csv = self.consview.getConsensusSequenceViewer()
        sel = csv.getSelection()
        seq = self.cons.getConsensus(sel[0], sel[1])

        # Create a text-entry dialog to allow the user to modify the selected base(s).
        diag = EntryDialog(
            self, 'Edit Sequence',
            'You may make changes to the selected base(s) below.\n\nOriginal sequence: {0} ({1} bases)\n'
            .format(seq, len(seq)), seq, 50)

        # Build a regular expression object for checking if the characters in the new
        # string are all valid bases.
        reo = re.compile('[' + ''.join(ConsensSeqBuilder.allbases) + ' ]*')

        # Display the dialog until the user presses cancel, closes the dialog, or submits
        # an acceptable string.
        stringok = False
        while not (stringok):
            response = diag.run()
            if (response
                    == gtk.RESPONSE_CANCEL) or (response
                                                == gtk.RESPONSE_DELETE_EVENT):
                break

            newseq = diag.get_text().upper()
            match = reo.match(newseq)

            # Verify that newseq is of the correct length and only contains valid bases.
            if len(newseq) != len(seq):
                self.showMessage(
                    'The number of bases in the edited sequence must match the the selection.'
                )
            elif match.end() - match.start() != len(newseq):
                self.showMessage(
                    'The edited sequence contains invalid characters.  You may only use IUPAC nucleotide codes or spaces.'
                )
            else:
                stringok = True

        diag.destroy()
        if response == gtk.RESPONSE_OK:
            self.cons.modifyBases(sel[0], sel[1], newseq)

        self.setSaveEnabled(True)

    def recalcConsensus(self, widget):
        response = self.showYesNoDialog(
            'Are you sure you want to recalculate the working sequence?  This will overwrite any edits you have made.'
        )
        if response != gtk.RESPONSE_YES:
            return

        self.cons.recalcConsensusSequence()
        self.setSaveEnabled(True)

    def fileInfo(self, widget):
        seqtraces = list()

        self.numseqs = self.cons.getNumSeqs()
        seqtraces.append(self.cons.getSequenceTrace(0))
        if self.numseqs == 2:
            seqtraces.append(self.cons.getSequenceTrace(1))

        if self.infowin != None:
            self.infowin.present()
        else:
            self.infowin = TraceFileInfoWin(seqtraces)
            self.infowin.connect('destroy', self.fileInfoDestroyed)

    def fileInfoDestroyed(self, win):
        self.infowin = None

    def exportConsensus(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Consensus Sequence')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequence
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage(
                'The file "' + fname + '" could not be opened for writing.  ' +
                'Verify that you have permission to write to the specified file and directory.'
            )
            return

        desc = 'consensus'
        seqfname = self.cons.getSequenceTrace(0).getFileName()
        if self.numseqs == 2:
            seqfname += ', ' + self.cons.getSequenceTrace(1).getFileName()
        sw.addUnalignedSequence(self.cons.getCompactConsensus(), seqfname,
                                desc)

        sw.write()

    def exportRawSequence(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Raw Sequence(s)')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequence
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage(
                'The file "' + fname + '" could not be opened for writing.  ' +
                'Verify that you have permission to write to the specified file and directory.'
            )
            return

        for cnt in range(0, self.numseqs):
            seqfname = self.cons.getSequenceTrace(cnt).getFileName()
            desc = 'raw sequence'
            if self.cons.getSequenceTrace(cnt).isReverseComplemented():
                desc += ' (reverse complemented)'
            sw.addUnalignedSequence(
                self.cons.getSequenceTrace(cnt).getBaseCalls(), seqfname, desc)

        sw.write()

    def exportAlignment(self, widget):
        # create a file chooser dialog to get a file name and format from the user
        fc = seqwriter.SeqWriterFileDialog(self, 'Export Alignment')
        fc.set_current_folder(os.getcwd())
        response = fc.run()
        fname = fc.get_filename()
        fformat = fc.getFileFormat()
        fc.destroy()
        if response != gtk.RESPONSE_OK:
            return

        # write out the sequences
        sw = seqwriter.SequenceWriterFactory.getSequenceWriter(fformat)
        try:
            sw.open(fname)
        except IOError:
            self.showMessage(
                'The file "' + fname + '" could not be opened for writing.  ' +
                'Verify that you have permission to write to the specified file and directory.'
            )
            return

        for cnt in range(0, self.numseqs):
            seqfname = self.cons.getSequenceTrace(cnt).getFileName()
            desc = 'sequence ' + str(cnt)
            if self.cons.getSequenceTrace(cnt).isReverseComplemented():
                desc += ' (reverse complemented)'
            sw.addAlignedSequence(self.cons.getAlignedSequence(cnt), seqfname,
                                  desc)

        sw.write()

    def closeWindow(self, widget):
        self.destroy()

    def destroyWindow(self, widget, data=None):
        # close the file information window, if it exists
        if self.infowin != None:
            self.infowin.destroy()

        # Unregister this window as an observer of the consensus sequence viewer.
        self.consview.getConsensusSequenceViewer().unregisterObserver(
            'consensus_clicked', self.consensusSeqClicked)
        self.consview.getConsensusSequenceViewer().unregisterObserver(
            'selection_state', self.selectStateChange)

        # Unregister this window as an observer of the consensus sequence.
        self.cons.unregisterObserver('undo_state_changed',
                                     self.undoStateChanged)
        self.cons.unregisterObserver('redo_state_changed',
                                     self.redoStateChanged)

        if self.is_mainwindow:
            gtk.main_quit()