Esempio n. 1
0
 def __init__(self):
     "Parameter is an instance of VisController to use for sending signals."
     super(VisQtMainWindow, self).__init__()  # required for signals
     self.ui = Ui_MainWindow()
     self.ui.setupUi(self)
     self._tool_import()
     self.show()
     # Hold a lists of checkboxes, "Edit" buttons, and layouts that represent
     # the parts in a piece for the "assemble" panel
     self.part_checkboxes = None
     self.edit_buttons = None
     self.part_layouts = None
     # Setup GUI-only Signals
     mapper = [
         # Workflow management
         (self.ui.btn_step1.clicked, self._check_for_pieces),
         (self.ui.btn_step2.clicked, self._start_the_analysis),
         # To operate the GUI
         (self.ui.btn_choose_files.clicked, self._tool_import),
         (self.ui.btn_about.clicked, self._tool_about),
         (self.ui.btn_analyze.clicked, self._tool_analyze),
         (self.ui.btn_experiment.clicked, self._tool_experiment),
         (self.ui.btn_file_add.clicked, self._add_files),
         (self.ui.btn_file_remove.clicked, self._remove_files),
         (self.ui.btn_show_results.clicked,
          self._prepare_experiment_submission),
         (self.ui.line_piece_title.editingFinished,
          self._update_piece_title),
         (self.ui.line_offset_interval.editingFinished,
          self._update_offset_interval),
         (self.ui.btn_choose_note.clicked, self._launch_offset_selection),
         (self.ui.chk_repeat_identical.stateChanged,
          self._update_repeat_identical),
         (self.ui.btn_cancel_operation.clicked, self._cancel_operation),
         # About part combinations
         (self.ui.line_compare_these_parts.editingFinished,
          self._add_parts_combo_by_line_edit),
         (self.ui.btn_add_check_combo.clicked, self._add_parts_combination),
         (self.ui.gui_pieces_list.selection_changed,
          self._update_pieces_selection),
         (self.ui.chk_all_voices.clicked, self._all_voices),
         (self.ui.chk_all_voice_combos.clicked, self._all_voice_pairs),
         # Things causing GUI adjustment
         (self.ui.rdo_consider_interval_ngrams.clicked,
          self._update_experiment_from_object),
         (self.ui.rdo_consider_intervals.clicked,
          self._update_experiment_from_object),
         (self.ui.rdo_consider_score.clicked,
          self._update_experiment_from_object),
         # Clicked an output format radio button (on the GUI: "How to Show Results")
         (self.ui.rdo_table.clicked, self._output_format_changed),
         (self.ui.rdo_chart.clicked, self._output_format_changed),
         (self.ui.rdo_score.clicked, self._output_format_changed),
         # "Shared Settings" from the "Analyzer" panel
         (self.ui.rdo_heedQuality.clicked, self._update_quality),
         (self.ui.rdo_noHeedQuality.clicked, self._update_quality),
         (self.ui.rdo_simple.clicked, self._update_octaves),
         (self.ui.rdo_compound.clicked, self._update_octaves),
     ]
     for signal, slot in mapper:
         signal.connect(slot)
     # Setup the progress bar
     self.ui.progress_bar.setMinimum(0)
     self.ui.progress_bar.setMaximum(100)
     self.ui.progress_bar.setValue(42)
     # set "Ctrl + q" to close the window
     QtGui.QShortcut(QtGui.QKeySequence(Qt.CTRL + Qt.Key_Q),
                     self,
                     self.close,
                     context=Qt.WindowShortcut)
     # visX setup
     self._list_of_ips = None  # holds the IndexedPiece instances
     self._list_of_files = ListOfFiles()
     self.ui.gui_file_list.setModel(self._list_of_files)
     self._work_wrap = WorkflowWrapper()
     self.ui.gui_pieces_list.setModel(self._work_wrap)
     self._prev_setts = None  # hold settings of previous experiment
Esempio n. 2
0
 def __init__(self):
     "Parameter is an instance of VisController to use for sending signals."
     super(VisQtMainWindow, self).__init__()  # required for signals
     self.ui = Ui_MainWindow()
     self.ui.setupUi(self)
     self._tool_import()
     self.show()
     # Hold a lists of checkboxes, "Edit" buttons, and layouts that represent
     # the parts in a piece for the "assemble" panel
     self.part_checkboxes = None
     self.edit_buttons = None
     self.part_layouts = None
     # Setup GUI-only Signals
     mapper = [
         # Workflow management
         (self.ui.btn_step1.clicked, self._check_for_pieces),
         (self.ui.btn_step2.clicked, self._start_the_analysis),
         # To operate the GUI
         (self.ui.btn_choose_files.clicked, self._tool_import),
         (self.ui.btn_about.clicked, self._tool_about),
         (self.ui.btn_analyze.clicked, self._tool_analyze),
         (self.ui.btn_experiment.clicked, self._tool_experiment),
         (self.ui.btn_file_add.clicked, self._add_files),
         (self.ui.btn_file_remove.clicked, self._remove_files),
         (self.ui.btn_show_results.clicked, self._prepare_experiment_submission),
         (self.ui.line_piece_title.editingFinished, self._update_piece_title),
         (self.ui.line_offset_interval.editingFinished, self._update_offset_interval),
         (self.ui.btn_choose_note.clicked, self._launch_offset_selection),
         (self.ui.chk_repeat_identical.stateChanged, self._update_repeat_identical),
         (self.ui.btn_cancel_operation.clicked, self._cancel_operation),
         # About part combinations
         (self.ui.line_compare_these_parts.editingFinished, self._add_parts_combo_by_line_edit),
         (self.ui.btn_add_check_combo.clicked, self._add_parts_combination),
         (self.ui.gui_pieces_list.selection_changed, self._update_pieces_selection),
         (self.ui.chk_all_voices.clicked, self._all_voices),
         (self.ui.chk_all_voice_combos.clicked, self._all_voice_pairs),
         # Things causing GUI adjustment
         (self.ui.rdo_consider_interval_ngrams.clicked, self._update_experiment_from_object),
         (self.ui.rdo_consider_intervals.clicked, self._update_experiment_from_object),
         (self.ui.rdo_consider_score.clicked, self._update_experiment_from_object),
         # Clicked an output format radio button (on the GUI: "How to Show Results")
         (self.ui.rdo_table.clicked, self._output_format_changed),
         (self.ui.rdo_chart.clicked, self._output_format_changed),
         (self.ui.rdo_score.clicked, self._output_format_changed),
         # "Shared Settings" from the "Analyzer" panel
         (self.ui.rdo_heedQuality.clicked, self._update_quality),
         (self.ui.rdo_noHeedQuality.clicked, self._update_quality),
         (self.ui.rdo_simple.clicked, self._update_octaves),
         (self.ui.rdo_compound.clicked, self._update_octaves),
     ]
     for signal, slot in mapper:
         signal.connect(slot)
     # Setup the progress bar
     self.ui.progress_bar.setMinimum(0)
     self.ui.progress_bar.setMaximum(100)
     self.ui.progress_bar.setValue(42)
     # set "Ctrl + q" to close the window
     QtGui.QShortcut(QtGui.QKeySequence(Qt.CTRL + Qt.Key_Q),
                     self,
                     self.close,
                     context=Qt.WindowShortcut)
     # visX setup
     self._list_of_ips = None  # holds the IndexedPiece instances
     self._list_of_files = ListOfFiles()
     self.ui.gui_file_list.setModel(self._list_of_files)
     self._work_wrap = WorkflowWrapper()
     self.ui.gui_pieces_list.setModel(self._work_wrap)
     self._prev_setts = None  # hold settings of previous experiment
Esempio n. 3
0
class VisQtMainWindow(QtGui.QMainWindow, QtCore.QObject):
    "This class makes the GUI-controlling objects for vis' PyQt4 interface."

    def __init__(self):
        "Parameter is an instance of VisController to use for sending signals."
        super(VisQtMainWindow, self).__init__()  # required for signals
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self._tool_import()
        self.show()
        # Hold a lists of checkboxes, "Edit" buttons, and layouts that represent
        # the parts in a piece for the "assemble" panel
        self.part_checkboxes = None
        self.edit_buttons = None
        self.part_layouts = None
        # Setup GUI-only Signals
        mapper = [
            # Workflow management
            (self.ui.btn_step1.clicked, self._check_for_pieces),
            (self.ui.btn_step2.clicked, self._start_the_analysis),
            # To operate the GUI
            (self.ui.btn_choose_files.clicked, self._tool_import),
            (self.ui.btn_about.clicked, self._tool_about),
            (self.ui.btn_analyze.clicked, self._tool_analyze),
            (self.ui.btn_experiment.clicked, self._tool_experiment),
            (self.ui.btn_file_add.clicked, self._add_files),
            (self.ui.btn_file_remove.clicked, self._remove_files),
            (self.ui.btn_show_results.clicked,
             self._prepare_experiment_submission),
            (self.ui.line_piece_title.editingFinished,
             self._update_piece_title),
            (self.ui.line_offset_interval.editingFinished,
             self._update_offset_interval),
            (self.ui.btn_choose_note.clicked, self._launch_offset_selection),
            (self.ui.chk_repeat_identical.stateChanged,
             self._update_repeat_identical),
            (self.ui.btn_cancel_operation.clicked, self._cancel_operation),
            # About part combinations
            (self.ui.line_compare_these_parts.editingFinished,
             self._add_parts_combo_by_line_edit),
            (self.ui.btn_add_check_combo.clicked, self._add_parts_combination),
            (self.ui.gui_pieces_list.selection_changed,
             self._update_pieces_selection),
            (self.ui.chk_all_voices.clicked, self._all_voices),
            (self.ui.chk_all_voice_combos.clicked, self._all_voice_pairs),
            # Things causing GUI adjustment
            (self.ui.rdo_consider_interval_ngrams.clicked,
             self._update_experiment_from_object),
            (self.ui.rdo_consider_intervals.clicked,
             self._update_experiment_from_object),
            (self.ui.rdo_consider_score.clicked,
             self._update_experiment_from_object),
            # Clicked an output format radio button (on the GUI: "How to Show Results")
            (self.ui.rdo_table.clicked, self._output_format_changed),
            (self.ui.rdo_chart.clicked, self._output_format_changed),
            (self.ui.rdo_score.clicked, self._output_format_changed),
            # "Shared Settings" from the "Analyzer" panel
            (self.ui.rdo_heedQuality.clicked, self._update_quality),
            (self.ui.rdo_noHeedQuality.clicked, self._update_quality),
            (self.ui.rdo_simple.clicked, self._update_octaves),
            (self.ui.rdo_compound.clicked, self._update_octaves),
        ]
        for signal, slot in mapper:
            signal.connect(slot)
        # Setup the progress bar
        self.ui.progress_bar.setMinimum(0)
        self.ui.progress_bar.setMaximum(100)
        self.ui.progress_bar.setValue(42)
        # set "Ctrl + q" to close the window
        QtGui.QShortcut(QtGui.QKeySequence(Qt.CTRL + Qt.Key_Q),
                        self,
                        self.close,
                        context=Qt.WindowShortcut)
        # visX setup
        self._list_of_ips = None  # holds the IndexedPiece instances
        self._list_of_files = ListOfFiles()
        self.ui.gui_file_list.setModel(self._list_of_files)
        self._work_wrap = WorkflowWrapper()
        self.ui.gui_pieces_list.setModel(self._work_wrap)
        self._prev_setts = None  # hold settings of previous experiment

    def here_is_the_app(self, app):
        "Give the QApplication to the VisQtMainWindow."
        self._app = app

    @QtCore.pyqtSlot()
    def _tool_import(self):
        "Activate the 'import' panel"
        self.ui.main_screen.setCurrentWidget(self.ui.page_choose)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_choose_files.setEnabled(True)
        self.ui.btn_analyze.setEnabled(False)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(True)
        self.ui.btn_step2.setEnabled(False)
        self.ui.btn_cancel_operation.setVisible(True)

    @QtCore.pyqtSlot()
    def _tool_analyze(self):
        "Activate the 'analyze' panel (corresponding to the Analyzer)."
        self.ui.main_screen.setCurrentWidget(self.ui.page_analyze)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setChecked(True)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_analyze.setEnabled(True)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(True)
        self.ui.btn_cancel_operation.setVisible(True)
        self._update_pieces_selection()
        self._update_global_settings()

    @QtCore.pyqtSlot()
    def _tool_working(self):
        "Activate the 'working' panel, for when vis is processing."
        self.ui.main_screen.setCurrentWidget(self.ui.page_working)
        # make sure nothing is enabled
        self.ui.btn_about.setEnabled(False)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setEnabled(False)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(False)
        # make sure nothing is checked
        self.ui.btn_about.setChecked(False)
        self.ui.btn_analyze.setChecked(False)
        self.ui.btn_choose_files.setChecked(False)
        self.ui.btn_experiment.setChecked(False)
        # Disable the details-selection until a particular piece is selected
        self.ui.grp_settings_for_piece.setEnabled(False)
        self.ui.grp_settings_for_piece.setVisible(False)
        # We'll just have a less-detailed progress bar
        self.ui.progress_bar.setMinimum(0)
        self.ui.progress_bar.setMaximum(0)

    @QtCore.pyqtSlot()
    def _tool_about(self):
        "Activate the 'about' panel."
        self.ui.main_screen.setCurrentWidget(self.ui.page_about)
        # leave enabled/disabled as-is, but make sure only "about" is checked
        self.ui.btn_about.setChecked(True)
        self.ui.btn_analyze.setChecked(False)
        self.ui.btn_choose_files.setChecked(False)
        self.ui.btn_experiment.setChecked(False)

    @QtCore.pyqtSlot()
    def _tool_experiment(self):
        "Activate the 'show' panel, which corresponds to the Experimenter controller."
        self.ui.main_screen.setCurrentWidget(self.ui.page_show)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setEnabled(True)
        self.ui.btn_experiment.setEnabled(True)
        self.ui.btn_experiment.setChecked(True)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(False)
        self.ui.btn_cancel_operation.setVisible(False)
        # call the thing to update this panel
        self._update_experiment_from_object()
        # call the thing to to what it says it does
        self._output_format_changed()

    @QtCore.pyqtSlot()
    def _check_for_pieces(self):
        """
        Check there is at least one piece set to be imported. If there is, start importing. If not,
        ask the user to choose some pieces.
        """
        if 0 != self._list_of_files.rowCount():
            self._tool_working()
            # After the WorkflowManager has been created, this method connects relevant signals.
            self._work_wrap.connect_workflow_signals(self._tool_analyze)
            # put everything in the WorkflowWrapper, so we can collect settings and whatever
            self._work_wrap.insertRows(None, self._list_of_files.rowCount())
            for new_row, pathname in enumerate(self._list_of_files):
                self._work_wrap.setData((new_row, WorkflowWrapper.filename),
                                        pathname, QtCore.Qt.EditRole)
        else:
            # then ask the user to stop being a jerk
            QtGui.QMessageBox.information(
                None, u"Please Select Files", u"""The list of files is empty.

You must choose pieces before we can import them.""",
                QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
                QtGui.QMessageBox.Ok)

    @QtCore.pyqtSlot()
    def _add_files(self):
        "Add files to the 'importer' panel."
        files = QtGui.QFileDialog.getOpenFileNames(
            None,  # parent
            u"Choose Files to Analyze",  # title
            u'',  # default directory
        )  # NB: This should be the 'filter' line, which is below, but commented out... it seems
        # that, for some reason, the filter isn't working. For me on Fedora 17, the result is
        # that all file types are "selectable"... but others have reported occasional issues
        # that entirely prevent them from selecting files. So here we are.
        # 'music21 Files (*.nwc *.mid *.midi *.mxl *.krn *.xml *.md)')  # filter
        if files:
            row_count = self._list_of_files.rowCount()
            self._list_of_files.insertRows(row_count, len(files))
            for i, each_file in enumerate(files):
                self._list_of_files.setData(row_count + i, each_file,
                                            QtCore.Qt.EditRole)

    # TODO: re-enable removing files
    @QtCore.pyqtSlot()
    def _remove_files(self):
        """
        Method which finds which files the user has selected for removal and emits a signal
        containing their names.
        """
        # which indices are currently selected?
        #currently_selected = self.ui.gui_file_list.selectedIndexes()
        # take the cheap way out
        QtGui.QMessageBox.information(None,
            "vis",
            "I didn't write that yet.",
            QtGui.QMessageBox.StandardButtons(\
                QtGui.QMessageBox.RestoreDefaults))

    @QtCore.pyqtSlot(str)
    def _update_progress_bar(self, progress):
        """
        Updates the "working" screen in the following ways:
        - If the argument is a two-character string that can be converted into
        an integer, or the string '100', the progress bar is set to that
        percentage completion.
        - If the argument is another string, the text below the progress bar is
        set to that string.
        """
        if not isinstance(progress, (basestring, QtCore.QString)):
            return None
        else:
            if u'100' == progress:
                self.ui.progress_bar.setValue(100)
            elif 3 > len(progress):
                try:
                    new_val = int(progress)
                    self.ui.progress_bar.setValue(new_val)
                except ValueError:
                    self.ui.lbl_currently_processing.setText(progress)
            else:
                self.ui.lbl_currently_processing.setText(progress)

    # TODO: re-enable operation cancellation
    @QtCore.pyqtSlot()
    def _cancel_operation(self):
        "If possible, cancel a running operation (import, analysis, experiment)."
        # confirm with the user that they want to cancel whatever's happening
        QtGui.QMessageBox.question(
            None, u"Confirm",
            u"This doesn't work yet!\n\nAre you sure you want to cancel the running operation?",
            QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No
                                              | QtGui.QMessageBox.Yes))
        return None

    @staticmethod
    def _error_reporter(description):
        "Notify the user that an error has happened. Parameter is a description of the error."
        QtGui.QMessageBox.warning(
            None, u'Error in an Internal Component', description,
            QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
            QtGui.QMessageBox.Ok)

    @QtCore.pyqtSlot()  # self.ui.btn_step1.clicked
    def _start_the_analysis(self):
        """
        Start the analysis, but first... check to make sure a user didn't forget to choose the
        "add voice pair" button!
        """
        # check that all the pieces have at least one part combination selected
        for i in xrange(len(self._work_wrap)):
            combos = unicode(
                self._work_wrap.data((i, WorkflowWrapper.parts_combinations),
                                     Qt.DisplayRole).toPyObject())
            combos = combos.toPyObject() if isinstance(
                combos, QtCore.QVariant) else combos
            combos = unicode(combos)
            if WorkflowWrapper.default_value == combos:
                # we can't analyze, but we *should* tell our user
                QtGui.QMessageBox.information(
                    None, u'vis',
                    u'You forgot to add part combinations for analysis in at least one piece.',
                    QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
                    QtGui.QMessageBox.Ok)
                return None

        # See if any of the part-name checkboxes are checked... if so, we gotta warn our user!
        part_cbs_are_checked = False
        if self.part_checkboxes is not None:
            for each_box in self.part_checkboxes:
                if each_box.isChecked():
                    part_cbs_are_checked = True
                    break
            # if a checkbox was checked, inform the user they may have made a mistake, and *don't*
            # start the analysis yet
            if part_cbs_are_checked:
                response = QtGui.QMessageBox.question(None,
                    u'vis',
                    u'At least one part checkbox is selected, but you did not add the part ' + \
                        u'combination to the list of parts to analyze.\n\nDo you want to go ' + \
                        u'back and add the part combination?',
                    QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes),
                    QtGui.QMessageBox.Yes)
                if response == QtGui.QMessageBox.Yes:
                    return None

        # Since we don't do any computation during this stage, we'll just go straight to the
        # experiment-selection panel.
        self._tool_experiment()

    @QtCore.pyqtSlot()  # self.ui.chk_all_voice_combos.clicked
    def _all_voice_pairs(self):
        "When a user chooses the 'all voice pairs' checkbox"
        part_spec = None
        if self.ui.chk_all_voice_combos.isChecked():
            if self.ui.chk_all_voices.isChecked():
                self.ui.chk_all_voices.setChecked(False)
            part_spec = u'[all pairs]'
        else:
            part_spec = WorkflowWrapper.default_value
        self._update_parts_selection(part_spec)

    @QtCore.pyqtSlot()  # self.ui.line_piece_title.editingFinished
    def _update_piece_title(self):
        "When users change the piece title on the 'assemble' panel."
        # Which piece is/pieces are selected?
        currently_selected = self.ui.gui_pieces_list.selectedIndexes()

        # Find the piece title and update it
        for cell in currently_selected:
            if WorkflowWrapper.title == cell.column():
                new_title = unicode(self.ui.line_piece_title.text())
                self._work_wrap.setData(cell, new_title, Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.chk_repeat_identical.stateChanged
    def _update_repeat_identical(self):
        "When users change 'repeat consecutive identical events' on the 'assemble' panel."
        # what was the QCheckBox changed to?
        changed_to = self.ui.chk_repeat_identical.isChecked()

        # Find the piece and update its settings
        for cell in self.ui.gui_pieces_list.selectedIndexes():
            if WorkflowWrapper.repeat_identical == cell.column():
                self._work_wrap.setData(cell, changed_to, QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.btn_add_check_combo.clicked
    def _add_parts_combination(self):
        """
        When users choose the "Add Combination" button to add the currently selected part
        combination to the list of parts to analyze.
        """
        # If there are no named parts, we can't do this
        if self.part_checkboxes is None:
            return None

        # Hold indices of the selected checkboxes
        selected_checkboxes = [
            i for i, box in enumerate(self.part_checkboxes) if box.isChecked()
        ]

        # Hold the vis-format specification
        vis_format = None

        # How many checkboxes are selected?
        if 1 == len(selected_checkboxes):
            # If we have one checkbox, it means we need to do monophonic analysis
            vis_format = u'[' + unicode(selected_checkboxes[0]) + u',' + \
                unicode(selected_checkboxes[0]) + u']'
        elif 1 < len(selected_checkboxes):
            # 2 or more checkboxes are selected
            vis_format = u'['
            for each_box in selected_checkboxes:
                vis_format += unicode(each_box) + u','
            vis_format = vis_format[:-1] + u']'
        else:
            # No checkboxes. Just give up.
            return

        # Now update the lists
        if vis_format is not None:
            # Hold the new part-combinations specification
            new_spec = ''
            # What's the current specification?
            curr_spec = unicode(self.ui.line_compare_these_parts.text())
            # Is curr_spec the default filler?
            if WorkflowWrapper.default_value == curr_spec:
                # Then just make a new one
                new_spec = u'[' + vis_format + u']'
                # Update the parts selection
                self._update_parts_selection(new_spec)
            # Does curr_spec include "[all]"?
            elif u'[all]' == curr_spec or u'[all pairs]' == curr_spec:
                # we'll just make a new one, and un-check the "all" QCheckBox
                new_spec = u'[' + vis_format + u']'
                self.ui.chk_all_voice_combos.setChecked(False)
                self.ui.chk_all_voices.setChecked(False)
                # NB: this has to be done after the setChecked(), since setChecked() emits a signal
                # that would otherwise obliterate the new_spec
                self._update_parts_selection(new_spec)
            # Does curr_spec contain vis_format?
            elif vis_format in curr_spec:
                pass
            # Else we must add this new thing
            else:
                # Otherwise, we should remove the final ']' in the list, and put
                # our new combo on the end
                new_spec = curr_spec[:-1] + u',' + vis_format + u']'

                # Update the parts selection
                self._update_parts_selection(new_spec)

        # Also clear the part-selection checkboxes
        for box in self.part_checkboxes:
            box.setChecked(False)

    @QtCore.pyqtSlot()  # self.ui.line_compare_these_parts.editingFinished
    def _add_parts_combo_by_line_edit(self):
        """
        Blindly put the contents of the part-specification QLineEdit into the table, trusting that
        the user knows what they're doing.
        """
        self._update_parts_selection(
            unicode(self.ui.line_compare_these_parts.text()))

    # Not a pyqtSlot
    def _update_parts_selection(self, part_spec):
        """
        Updates line_compare_these_parts and the model data for all selected pieces so that the
        "parts to compare" contains part_spec.
        """

        # update the UI
        self.ui.line_compare_these_parts.setText(part_spec)

        # Update the selected pieces
        # get the list of selected cells... for each one that is the "voices"
        # column(), set it to the thing specified
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.parts_combinations == cell.column():
                self._work_wrap.setData(cell, part_spec, QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.line_offset_interval.editingFinished
    def _update_offset_interval(self):
        """
        Take the value of the "offset interval" field, and sets it.

        Assumes, with no good reason, that the value put in the textbox is good.
        """
        new_offset_interval = unicode(self.ui.line_offset_interval.text())

        # Update the selected pieces
        # get the list of selected cells... for each one that is the "n"
        # column(), set it to the thing specified
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.offset_interval == cell.column():
                self._work_wrap.setData(cell, new_offset_interval,
                                        QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.gui_pieces_list.clicked
    def _update_pieces_selection(self):
        "Update detail-selection widgets when the selected pieces are changed."

        # Which piece is/pieces are selected?
        currently_selected = self.ui.gui_pieces_list.selectedIndexes()

        # NB: we get a list of all the cells selected, and this is definitely done
        # in rows, so because each row has 6 things, if we have 6 cells, it means
        # we have only one row... but more than 6 cells means more than one row
        if len(currently_selected) == 0:
            # (1) Disable all the controls
            self.ui.line_offset_interval.setEnabled(False)
            self.ui.btn_choose_note.setEnabled(False)
            self.ui.line_compare_these_parts.setEnabled(False)
            self.ui.chk_all_voices.setEnabled(False)
            self.ui.chk_all_voice_combos.setEnabled(False)
            self.ui.btn_add_check_combo.setEnabled(False)
            self.ui.line_piece_title.setEnabled(False)
            self._piece_settings_visibility(False)
            # (2) Remove the part list
            if self.part_checkboxes is not None:
                self._update_part_checkboxes(u'erase')
        elif len(currently_selected) > 6:
            # Multiple pieces selected... possible customization
            # (1) Enable all the controls
            self.ui.line_offset_interval.setEnabled(True)
            self.ui.btn_choose_note.setEnabled(True)
            self.ui.line_compare_these_parts.setEnabled(True)
            self.ui.chk_all_voices.setEnabled(True)
            self.ui.chk_all_voice_combos.setEnabled(True)
            self.ui.btn_add_check_combo.setEnabled(True)
            self.ui.line_piece_title.setEnabled(False)  # not applicable
            self._piece_settings_visibility(True)
            self.ui.grp_settings_for_piece.setTitle(
                u'Settings for Selected Pieces')
            # (2) if the pieces have the same part names, display them
            # 2.1: get a list of all the lists-of-part-names
            lists_of_part_names = []
            for cell in currently_selected:
                if WorkflowWrapper.parts_list == cell.column():
                    lists_of_part_names.append(\
                        self._work_wrap.data(cell, WorkflowWrapper.ListRole).toPyObject())
            # 2.2: See if each piece has the same number of parts
            number_of_parts = 0
            for parts_list in lists_of_part_names:
                if 0 == number_of_parts:
                    number_of_parts = len(parts_list)
                elif number_of_parts != len(parts_list):
                    number_of_parts = False
                    break
            # 2.3: See if the names in each of the parts is the same
            same_names = True
            for i in xrange(len(lists_of_part_names) - 1):
                if lists_of_part_names[i] != lists_of_part_names[i + 1]:
                    same_names = False
            # 2.4: If all the part names are the same, we'll use them
            if same_names:
                self._update_part_checkboxes(currently_selected)
            # 2.5: If all the pieces have the same number of parts, we can still do it
            elif number_of_parts:
                self._update_part_checkboxes(currently_selected, no_name=True)
            # 2.6: Otherwise, we can't display part checkboxes
            else:
                #self.ui.chk_all_voice_combos.setEnabled(True)
                self._update_part_checkboxes(u'erase')
            # (3) if the pieces have the same offset interval, display it
            first_offset = None
            for cell in currently_selected:
                if WorkflowWrapper.offset_interval == cell.column():
                    if first_offset is None:
                        first_offset = self._work_wrap.\
                        data(cell, QtCore.Qt.DisplayRole).toPyObject()
                    elif first_offset == self._work_wrap.\
                    data(cell, QtCore.Qt.DisplayRole).toPyObject():
                        continue
                    else:
                        first_offset = ''
                        break
            self.ui.line_offset_interval.setText(unicode(first_offset))
            # (4) Update "Compare These Parts"
            first_comp = None
            for cell in currently_selected:
                if WorkflowWrapper.parts_combinations == cell.column():
                    if first_comp is None:
                        first_comp = self._work_wrap.\
                        data(cell, QtCore.Qt.DisplayRole).toPyObject()
                    elif first_comp == self._work_wrap.\
                    data(cell, QtCore.Qt.DisplayRole).toPyObject():
                        continue
                    else:
                        first_comp = u''
                        break
            if '' == first_comp:
                # Multiple parts have different specs
                self.ui.line_compare_these_parts.setText('')
                self.ui.chk_all_voice_combos.setChecked(False)
                self.ui.chk_all_voices.setChecked(False)
            else:
                # Multiple parts have the same spec
                self._update_comparison_parts(currently_selected)
        else:
            # Only one piece... customize for it
            # (1) Enable all the controls
            self.ui.line_offset_interval.setEnabled(True)
            self.ui.btn_choose_note.setEnabled(True)
            self.ui.line_compare_these_parts.setEnabled(True)
            self.ui.chk_all_voices.setEnabled(True)
            self.ui.chk_all_voice_combos.setEnabled(True)
            self.ui.btn_add_check_combo.setEnabled(True)
            self.ui.line_piece_title.setEnabled(True)
            self._piece_settings_visibility(True)
            self.ui.grp_settings_for_piece.setTitle(
                u'Settings for Selected Piece')
            # (2) Populate the part list
            self._update_part_checkboxes(currently_selected)
            # (3) Update "offset interval"
            for cell in currently_selected:
                if WorkflowWrapper.offset_interval == cell.column():
                    self.ui.line_offset_interval.setText(
                        unicode(
                            self._work_wrap.data(
                                cell, QtCore.Qt.DisplayRole).toPyObject()))
                    break
            # (4) Update "Compare These Parts"
            self._update_comparison_parts(currently_selected)
            # (5) Update "Pice Title"
            for cell in currently_selected:
                if WorkflowWrapper.title == cell.column():
                    self.ui.line_piece_title.setText(
                        unicode(
                            self._work_wrap.data(
                                cell, QtCore.Qt.DisplayRole).toPyObject()))
                    break

    # Not a pyqtSlot
    def _update_comparison_parts(self, currently_selected):
        """
        When a different part combination is selected, call this method to update the "All
        Combinations" and "Basso Seguente" checkboxes.

        You should only call this method if all of the selected pieces have the same part names
        (which is true when only one part is selected).

        The argument should be a list of the currently selected cells.
        """

        for cell in currently_selected:
            if WorkflowWrapper.parts_combinations == cell.column():
                comparison_parts = unicode(
                    self._work_wrap.data(cell,
                                         QtCore.Qt.DisplayRole).toPyObject())
                self.ui.line_compare_these_parts.setText(comparison_parts)
                if u'[all]' == comparison_parts:
                    self.ui.chk_all_voice_combos.setChecked(False)
                    self.ui.chk_all_voices.setChecked(True)
                elif u'[all pairs]' == comparison_parts:
                    self.ui.chk_all_voice_combos.setChecked(True)
                    self.ui.chk_all_voices.setChecked(False)
                else:
                    self.ui.chk_all_voice_combos.setChecked(False)
                    self.ui.chk_all_voices.setChecked(False)
                break

    # Not a pyqtSlot
    def _edit_part_name(self, part_index=None):
        """
        Change the name of a part.

        This is called by lambda methods, which are slots that respond to signals emitted by
        buttons on the interface, which when they are displayed correspond each to a part in the
        score.
        """
        # Get the new name
        new_name = unicode(
            QtGui.QInputDialog.getText(
                None, u"Part Name!", u"Choose New Part Name",
                QtGui.QLineEdit.Normal,
                unicode(self.part_checkboxes[part_index].text()))
            [0])  # default filled with new name

        # Find the parts lists and update them
        for cell in self.ui.gui_pieces_list.selectedIndexes():
            if WorkflowWrapper.parts_list == cell.column():
                # We're just going to change the part name in the model, not in the
                # actual Score object itself (which would require re-loading)
                # 1.) Get the parts list
                parts = self._work_wrap.data(
                    cell, WorkflowWrapper.ListRole).toPyObject()
                # 2.) Update the part name as requested
                parts[part_index] = new_name
                # 3.) Convert the part names to str objects (from QString)
                parts = [unicode(name) for name in parts]
                # 4.) Update the data model and QCheckBox objects
                self._work_wrap.setData(cell, parts, QtCore.Qt.EditRole)
                self._update_pieces_selection()

    # Not a pyqtSlot
    def _update_part_checkboxes(self, currently_selected, no_name=False):
        """
        Update the part-selection QCheckBox objects to reflect the currently selected part(s).

        You should only call this method if all of the selected pieces have the same part names
        (which is true when only one part is selected).

        The argument should be a list of the currently selected cells.

        If the argument is == 'erase' then the method removes all current checkboxes and stops.

        The "no_name" keyword argument means we'll use generically numbered parts, rather than
        specific part names. This is useful when, for example, a bunch of pieces are selected, and
        they all have the same number of parts but with different names.
        """

        # (1) Remove previous checkboxes from the layout
        if self.part_layouts is not None:
            for lay in self.part_layouts:
                for part in self.part_checkboxes:
                    lay.removeWidget(part)
                    part.close()
                for button in self.edit_buttons:
                    lay.removeWidget(button)
                    button.close()
                self.ui.verticalLayout_part_boxes.removeItem(lay)
                lay.invalidate()

        self.part_layouts = None
        self.part_checkboxes = None
        self.edit_buttons = None

        # (1a) If currently_selected is "erase" then we should only erase the
        # current checkboxes, and we should stop now.
        if u'erase' == currently_selected:
            return

        # (2) Get the list of parts
        list_of_parts = None
        for cell in currently_selected:
            if WorkflowWrapper.parts_list == cell.column():
                list_of_parts = self._work_wrap.data(
                    cell, WorkflowWrapper.ListRole).toPyObject()
                break
        # deal with a possible "no_name" argument
        if no_name:
            how_many_parts = len(list_of_parts)
            list_of_parts = []
            for i in xrange(how_many_parts):
                list_of_parts.append(u'Part ' + unicode(i + 1))

        # (3) Put up a checkbox for each part
        self.part_checkboxes = []
        self.edit_buttons = []
        self.part_layouts = []
        for i in xrange(len(list_of_parts)):
            part_name = unicode(list_of_parts[i])
            # This is the New CheckBox to select this part
            n_c_b = QtGui.QCheckBox(self.ui.widget_part_boxes)
            n_c_b.setObjectName(u'chk_' + part_name)
            n_c_b.setText(part_name)

            # This is the New BuTtoN to "Edit" this part's name
            n_btn = QtGui.QPushButton(self.ui.widget_part_boxes)
            n_btn.setObjectName(u'btn_' + part_name)
            n_btn.setText(u'Edit Part Name')

            def the_thing(ell):
                "This method runs when the button is clicked"
                return lambda: self._edit_part_name(ell)

            n_btn.clicked.connect(the_thing(i))

            # Add the checkbox and button to the horizontal layout
            lay = QtGui.QHBoxLayout()
            lay.addWidget(n_c_b)
            lay.addWidget(n_btn)

            # Add the layout to the list of part-name checkboxes
            self.edit_buttons.append(n_btn)
            self.part_checkboxes.append(n_c_b)
            self.part_layouts.append(lay)

        # (4) Add all the widgets to the layout
        for part in self.part_layouts:
            self.ui.verticalLayout_part_boxes.addLayout(part)

    @QtCore.pyqtSlot()  # self.ui.btn_choose_note.clicked
    def _launch_offset_selection(self):
        "Launch the dialogue box to help users visually select offset values."
        # Launch the offset-selection QDialog
        selector = VisOffsetSelector()
        chosen_offset = selector.trigger()
        # Update the QLineEdit
        if u'ALL' == chosen_offset:
            chosen_offset = u'(optional)'
        self.ui.line_offset_interval.setText(chosen_offset)
        # Set values in the model
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.offset_interval == cell.column():
                self._work_wrap.setData(cell, chosen_offset,
                                        QtCore.Qt.EditRole)
        # Just to make sure we get rid of this
        del selector

    # Not a pyqtSlot
    def _piece_settings_visibility(self, set_to):
        """
        Given True or False, makes visible and enables (or makes invisible and disables)
        everything in the "Settings for Piece" QGroupBox (and the box itself), and the opposite
        for self.lbl_select_piece
        """
        self.ui.grp_settings_for_piece.setVisible(set_to)
        self.ui.grp_settings_for_piece.setEnabled(set_to)
        self.ui.widget_select_piece.setVisible(not set_to)

    @QtCore.pyqtSlot()  # self.ui.rdo_consider_***.clicked()
    def _update_experiment_from_object(self):
        """
        When one of the self.ui.rdo_consider_*** radio buttons is selected, this method updates the
        rest of the GUI to reflect the options relevant to that output method.
        """

        all_the_widgets = [
            self.ui.rdo_table, self.ui.rdo_chart, self.ui.grp_values_of_n,
            self.ui.rdo_score, self.ui.grp_annotate_these,
            self.ui.grp_ignore_inversion, self.ui.grp_annotate_these
        ]

        def on_offer(enable_these):
            """
            Given a list of the GUI objects in the "experiment" panel that should be "on," this
            method enables them and disables all the others.
            """

            # (1) Disable everything
            for each_widget in all_the_widgets:
                each_widget.setVisible(False)

            # (2) Enable the valid things
            for each_widget in enable_these:
                each_widget.setVisible(True)

        # Determine which widgets to enable
        which_to_enable = []

        if self.ui.rdo_consider_intervals.isChecked():
            which_to_enable = [
                self.ui.rdo_table, self.ui.rdo_chart, self.ui.rdo_score,
                self.ui.grp_ignore_inversion, self.ui.grp_annotate_these
            ]
        elif self.ui.rdo_consider_interval_ngrams.isChecked():
            which_to_enable = [
                self.ui.rdo_table, self.ui.grp_values_of_n, self.ui.rdo_chart,
                self.ui.grp_ignore_inversion
            ]
        elif self.ui.rdo_consider_score.isChecked():
            which_to_enable = [self.ui.rdo_score]

        # Run the on_offer()
        on_offer(which_to_enable)

        # Choose the first enabled output format in "How to Show Results"; update filters
        outs = [self.ui.rdo_table, self.ui.rdo_chart, self.ui.rdo_score]
        for each_box in outs:
            if each_box.isVisible():
                each_box.setChecked(True)
                break
        which_to_enable = [self.ui.rdo_score]
        self._output_format_changed()

    def _output_format_changed(self):
        """
        When a user chooses a different output format (in "How to Show Results" on the "show"
        panel), we may have to enable/disable the Top X and Threshold filter.
        """
        if self.ui.rdo_table.isChecked() or self.ui.rdo_chart.isChecked():
            self.ui.group_top_x.setVisible(True)
            self.ui.group_threshold.setVisible(True)
        else:
            self.ui.group_top_x.setVisible(False)
            self.ui.group_threshold.setVisible(False)

    @QtCore.pyqtSlot()  # self.ui.chk_all_voices.stateChanged
    def _all_voices(self):
        "When a user chooses the 'all voices' checkbox"
        part_spec = None
        if self.ui.chk_all_voices.isChecked():
            if self.ui.chk_all_voice_combos.isChecked():
                self.ui.chk_all_voice_combos.setChecked(False)
            part_spec = u'[all]'
        else:
            part_spec = WorkflowWrapper.default_value
        self._update_parts_selection(part_spec)

    @QtCore.pyqtSlot()  # self.ui.rdo_heedQuality and rdo_noHeedQuality
    def _update_quality(self):
        "When a user changes the 'display quality' radio buttons."
        set_to_what = True if self.ui.rdo_heedQuality.isChecked() else False
        self._work_wrap.setData((0, WorkflowWrapper.quality), set_to_what,
                                Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.rdo_compound and rdo_simple
    def _update_octaves(self):
        "When a user changes the 'simple/compound' radio buttons."
        set_to_what = True if self.ui.rdo_simple.isChecked() else False
        self._work_wrap.setData((0, WorkflowWrapper.simple_ints), set_to_what,
                                Qt.EditRole)

    def _update_global_settings(self):
        """
        Update the radio buttons associated with global settings (at the moment, quality and simple)
        """
        if self._work_wrap.data((0, WorkflowWrapper.quality),
                                Qt.DisplayRole).toPyObject():
            self.ui.rdo_heedQuality.setChecked(True)
        else:
            self.ui.rdo_noHeedQuality.setChecked(True)
        if self._work_wrap.data((0, WorkflowWrapper.simple_ints),
                                Qt.DisplayRole).toPyObject():
            self.ui.rdo_simple.setChecked(True)
        else:
            self.ui.rdo_compound.setChecked(True)

    @QtCore.pyqtSlot()  # self.ui.btn_show_results.clicked
    def _prepare_experiment_submission(self):
        """
        Make sure the Experimenter has a properly-configured Settings instance, then ask it to run
        the experiment.
        """

        # move to the "working" panel and update it
        self._tool_working()
        self._update_progress_bar(u'0')
        self._update_progress_bar(u'Initializing experiment.')

        # hold a list of tuples to be signalled as settings
        list_of_settings = {}

        def do_experiment():
            "Which experiment does the user want to run?"
            # experiment
            if self.ui.rdo_consider_intervals.isChecked():
                list_of_settings['experiment'] = u'intervals'
            elif self.ui.rdo_consider_interval_ngrams.isChecked():
                list_of_settings['experiment'] = u'interval n-grams'
            elif self.ui.rdo_consider_score.isChecked():
                list_of_settings['experiment'] = 'LilyPondExperiment'
                list_of_settings['output format'] = 'LilyPondDisplay'
            # output format
            if self.ui.rdo_table.isChecked():
                list_of_settings['output format'] = 'table'
            elif self.ui.rdo_chart.isChecked():
                list_of_settings['output format'] = 'chart'
            elif self.ui.rdo_score.isChecked():
                list_of_settings['output format'] = 'lilypond'

        def do_threshold():
            "Is there a threshold value?"
            threshold = self.ui.spin_threshold.value()
            if 0 != threshold:
                list_of_settings['threshold'] = threshold

        def do_top_x():
            "Is there a 'top x' value?"
            top_x = self.ui.spin_top_x.value()
            if 0 != top_x:
                list_of_settings['topX'] = top_x

        def do_values_of_n():
            "Are there values of 'n' specified?"
            enn = self.ui.spin_n.value()
            list_of_settings['n'] = enn

        def do_ignore_inversion():
            "Ignore inversion?"
            if self.ui.chk_ignore_inversion.isChecked():
                list_of_settings['ignore direction'] = True
            else:
                list_of_settings['ignore direction'] = False

        # (1) Figure out the settings
        # (1a) Which experiment?
        do_experiment()
        # (1d) Is there a "values_of_n" value?
        if u'interval n-grams' == list_of_settings[u'experiment']:
            do_values_of_n()
        # (1e) Threshold
        do_threshold()
        # (1f) Top X
        do_top_x()
        # (1g) Ignore Voice Crossing
        do_ignore_inversion()

        # (2) Run the experiment
        self._run_the_experiment(list_of_settings)

    def _run_the_experiment(self, settings):
        """
        Run the experiment as instructed by the 'settings' argument.
        """
        # 1.) Do we need to run the experiment at all?
        workm = self._work_wrap.get_workflow_manager()
        run_exp = None
        # 1.a) If the model thinks settings were changed (i.e., user went back to "analyze" page).
        if self._work_wrap.settings_changed():
            run_exp = True
        # 1.b) If there was no previous experiment, then yes.
        elif self._prev_setts is None:
            run_exp = True
        # 1.c) If the previous experiment is different, then yes.
        elif self._prev_setts['experiment'] != settings['experiment']:
            run_exp = True
        # 1.d) If the experiments are the same, but they have 'n' and they're different, then yes.
        elif 'n' in self._prev_setts and self._prev_setts['n'] != settings['n']:
            run_exp = True
        # 1.e) Save the settings for next time.
        self._prev_setts = settings

        # 2.) Run the experiment, if required.
        if run_exp is True:
            if u'n' in settings:
                workm.settings(None, u'n', settings[u'n'])
            workm.run(settings[u'experiment'])

        # 3.) Run the output-getting stuff.
        path = None
        result_type = None
        top_x = settings['topX'] if 'topX' in settings else None
        threshold = settings['threshold'] if 'threshold' in settings else None
        if u'chart' == settings[u'output format']:
            result_type = u'image'
            path = workm.output(u'R histogram', u'outputs/R_chart.png', top_x,
                                threshold)
            # We add "../" because we expect the image to be in the "outputs" directory, but so is
            # the HTML file. We could just set "path" to "R_chart.png" but output() doesn't
            # guarantee that it'll actually output to the pathname requested, so we need it.
            path = u'../' + path
        elif u'table' == settings[u'output format']:
            result_type = u'table'
            path = workm.export(u'HTML', u'outputs/pandas_table.html', top_x,
                                threshold)
        else:
            VisQtMainWindow._error_reporter(u'Unrecognized output format: "' + \
                                            unicode(settings[u'output format']) + u'"')

        if path is not None:
            token_name = u'Interval' if u'intervals' == settings[u'experiment'] else \
                    unicode(settings['n']) + u'-Gram'
            webview = VisWebView()
            trig_ret = webview.trigger(path, result_type, token_name)
            # we may have to save the output!
            if trig_ret is not None:
                for form, pathname in trig_ret:
                    workm.export(form, pathname, top_x, threshold)
            del webview  # make sure we free dat memory!

        self._tool_experiment()
Esempio n. 4
0
class VisQtMainWindow(QtGui.QMainWindow, QtCore.QObject):
    "This class makes the GUI-controlling objects for vis' PyQt4 interface."

    def __init__(self):
        "Parameter is an instance of VisController to use for sending signals."
        super(VisQtMainWindow, self).__init__()  # required for signals
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self._tool_import()
        self.show()
        # Hold a lists of checkboxes, "Edit" buttons, and layouts that represent
        # the parts in a piece for the "assemble" panel
        self.part_checkboxes = None
        self.edit_buttons = None
        self.part_layouts = None
        # Setup GUI-only Signals
        mapper = [
            # Workflow management
            (self.ui.btn_step1.clicked, self._check_for_pieces),
            (self.ui.btn_step2.clicked, self._start_the_analysis),
            # To operate the GUI
            (self.ui.btn_choose_files.clicked, self._tool_import),
            (self.ui.btn_about.clicked, self._tool_about),
            (self.ui.btn_analyze.clicked, self._tool_analyze),
            (self.ui.btn_experiment.clicked, self._tool_experiment),
            (self.ui.btn_file_add.clicked, self._add_files),
            (self.ui.btn_file_remove.clicked, self._remove_files),
            (self.ui.btn_show_results.clicked, self._prepare_experiment_submission),
            (self.ui.line_piece_title.editingFinished, self._update_piece_title),
            (self.ui.line_offset_interval.editingFinished, self._update_offset_interval),
            (self.ui.btn_choose_note.clicked, self._launch_offset_selection),
            (self.ui.chk_repeat_identical.stateChanged, self._update_repeat_identical),
            (self.ui.btn_cancel_operation.clicked, self._cancel_operation),
            # About part combinations
            (self.ui.line_compare_these_parts.editingFinished, self._add_parts_combo_by_line_edit),
            (self.ui.btn_add_check_combo.clicked, self._add_parts_combination),
            (self.ui.gui_pieces_list.selection_changed, self._update_pieces_selection),
            (self.ui.chk_all_voices.clicked, self._all_voices),
            (self.ui.chk_all_voice_combos.clicked, self._all_voice_pairs),
            # Things causing GUI adjustment
            (self.ui.rdo_consider_interval_ngrams.clicked, self._update_experiment_from_object),
            (self.ui.rdo_consider_intervals.clicked, self._update_experiment_from_object),
            (self.ui.rdo_consider_score.clicked, self._update_experiment_from_object),
            # Clicked an output format radio button (on the GUI: "How to Show Results")
            (self.ui.rdo_table.clicked, self._output_format_changed),
            (self.ui.rdo_chart.clicked, self._output_format_changed),
            (self.ui.rdo_score.clicked, self._output_format_changed),
            # "Shared Settings" from the "Analyzer" panel
            (self.ui.rdo_heedQuality.clicked, self._update_quality),
            (self.ui.rdo_noHeedQuality.clicked, self._update_quality),
            (self.ui.rdo_simple.clicked, self._update_octaves),
            (self.ui.rdo_compound.clicked, self._update_octaves),
        ]
        for signal, slot in mapper:
            signal.connect(slot)
        # Setup the progress bar
        self.ui.progress_bar.setMinimum(0)
        self.ui.progress_bar.setMaximum(100)
        self.ui.progress_bar.setValue(42)
        # set "Ctrl + q" to close the window
        QtGui.QShortcut(QtGui.QKeySequence(Qt.CTRL + Qt.Key_Q),
                        self,
                        self.close,
                        context=Qt.WindowShortcut)
        # visX setup
        self._list_of_ips = None  # holds the IndexedPiece instances
        self._list_of_files = ListOfFiles()
        self.ui.gui_file_list.setModel(self._list_of_files)
        self._work_wrap = WorkflowWrapper()
        self.ui.gui_pieces_list.setModel(self._work_wrap)
        self._prev_setts = None  # hold settings of previous experiment

    def here_is_the_app(self, app):
        "Give the QApplication to the VisQtMainWindow."
        self._app = app

    @QtCore.pyqtSlot()
    def _tool_import(self):
        "Activate the 'import' panel"
        self.ui.main_screen.setCurrentWidget(self.ui.page_choose)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_choose_files.setEnabled(True)
        self.ui.btn_analyze.setEnabled(False)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(True)
        self.ui.btn_step2.setEnabled(False)
        self.ui.btn_cancel_operation.setVisible(True)

    @QtCore.pyqtSlot()
    def _tool_analyze(self):
        "Activate the 'analyze' panel (corresponding to the Analyzer)."
        self.ui.main_screen.setCurrentWidget(self.ui.page_analyze)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setChecked(True)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_analyze.setEnabled(True)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(True)
        self.ui.btn_cancel_operation.setVisible(True)
        self._update_pieces_selection()
        self._update_global_settings()

    @QtCore.pyqtSlot()
    def _tool_working(self):
        "Activate the 'working' panel, for when vis is processing."
        self.ui.main_screen.setCurrentWidget(self.ui.page_working)
        # make sure nothing is enabled
        self.ui.btn_about.setEnabled(False)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setEnabled(False)
        self.ui.btn_experiment.setEnabled(False)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(False)
        # make sure nothing is checked
        self.ui.btn_about.setChecked(False)
        self.ui.btn_analyze.setChecked(False)
        self.ui.btn_choose_files.setChecked(False)
        self.ui.btn_experiment.setChecked(False)
        # Disable the details-selection until a particular piece is selected
        self.ui.grp_settings_for_piece.setEnabled(False)
        self.ui.grp_settings_for_piece.setVisible(False)
        # We'll just have a less-detailed progress bar
        self.ui.progress_bar.setMinimum(0)
        self.ui.progress_bar.setMaximum(0)

    @QtCore.pyqtSlot()
    def _tool_about(self):
        "Activate the 'about' panel."
        self.ui.main_screen.setCurrentWidget(self.ui.page_about)
        # leave enabled/disabled as-is, but make sure only "about" is checked
        self.ui.btn_about.setChecked(True)
        self.ui.btn_analyze.setChecked(False)
        self.ui.btn_choose_files.setChecked(False)
        self.ui.btn_experiment.setChecked(False)

    @QtCore.pyqtSlot()
    def _tool_experiment(self):
        "Activate the 'show' panel, which corresponds to the Experimenter controller."
        self.ui.main_screen.setCurrentWidget(self.ui.page_show)
        self.ui.btn_about.setEnabled(True)
        self.ui.btn_choose_files.setEnabled(False)
        self.ui.btn_analyze.setEnabled(True)
        self.ui.btn_experiment.setEnabled(True)
        self.ui.btn_experiment.setChecked(True)
        self.ui.btn_step1.setEnabled(False)
        self.ui.btn_step2.setEnabled(False)
        self.ui.btn_cancel_operation.setVisible(False)
        # call the thing to update this panel
        self._update_experiment_from_object()
        # call the thing to to what it says it does
        self._output_format_changed()

    @QtCore.pyqtSlot()
    def _check_for_pieces(self):
        """
        Check there is at least one piece set to be imported. If there is, start importing. If not,
        ask the user to choose some pieces.
        """
        if 0 != self._list_of_files.rowCount():
            self._tool_working()
            # After the WorkflowManager has been created, this method connects relevant signals.
            self._work_wrap.connect_workflow_signals(self._tool_analyze)
            # put everything in the WorkflowWrapper, so we can collect settings and whatever
            self._work_wrap.insertRows(None, self._list_of_files.rowCount())
            for new_row, pathname in enumerate(self._list_of_files):
                self._work_wrap.setData((new_row, WorkflowWrapper.filename),
                                        pathname,
                                        QtCore.Qt.EditRole)
        else:
            # then ask the user to stop being a jerk
            QtGui.QMessageBox.information(None,
            u"Please Select Files",
            u"""The list of files is empty.

You must choose pieces before we can import them.""",
            QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
            QtGui.QMessageBox.Ok)

    @QtCore.pyqtSlot()
    def _add_files(self):
        "Add files to the 'importer' panel."
        files = QtGui.QFileDialog.getOpenFileNames(
            None,  # parent
            u"Choose Files to Analyze",  # title
            u'',  # default directory
            )  # NB: This should be the 'filter' line, which is below, but commented out... it seems
            # that, for some reason, the filter isn't working. For me on Fedora 17, the result is
            # that all file types are "selectable"... but others have reported occasional issues
            # that entirely prevent them from selecting files. So here we are.
            # 'music21 Files (*.nwc *.mid *.midi *.mxl *.krn *.xml *.md)')  # filter
        if files:
            row_count = self._list_of_files.rowCount()
            self._list_of_files.insertRows(row_count, len(files))
            for i, each_file in enumerate(files):
                self._list_of_files.setData(row_count + i, each_file, QtCore.Qt.EditRole)

    # TODO: re-enable removing files
    @QtCore.pyqtSlot()
    def _remove_files(self):
        """
        Method which finds which files the user has selected for removal and emits a signal
        containing their names.
        """
        # which indices are currently selected?
        #currently_selected = self.ui.gui_file_list.selectedIndexes()
        # take the cheap way out
        QtGui.QMessageBox.information(None,
            "vis",
            "I didn't write that yet.",
            QtGui.QMessageBox.StandardButtons(\
                QtGui.QMessageBox.RestoreDefaults))

    @QtCore.pyqtSlot(str)
    def _update_progress_bar(self, progress):
        """
        Updates the "working" screen in the following ways:
        - If the argument is a two-character string that can be converted into
        an integer, or the string '100', the progress bar is set to that
        percentage completion.
        - If the argument is another string, the text below the progress bar is
        set to that string.
        """
        if not isinstance(progress, (basestring, QtCore.QString)):
            return None
        else:
            if u'100' == progress:
                self.ui.progress_bar.setValue(100)
            elif 3 > len(progress):
                try:
                    new_val = int(progress)
                    self.ui.progress_bar.setValue(new_val)
                except ValueError:
                    self.ui.lbl_currently_processing.setText(progress)
            else:
                self.ui.lbl_currently_processing.setText(progress)

    # TODO: re-enable operation cancellation
    @QtCore.pyqtSlot()
    def _cancel_operation(self):
        "If possible, cancel a running operation (import, analysis, experiment)."
        # confirm with the user that they want to cancel whatever's happening
        QtGui.QMessageBox.question(
            None,
            u"Confirm",
            u"This doesn't work yet!\n\nAre you sure you want to cancel the running operation?",
            QtGui.QMessageBox.StandardButtons(
                QtGui.QMessageBox.No |
                QtGui.QMessageBox.Yes))
        return None

    @staticmethod
    def _error_reporter(description):
        "Notify the user that an error has happened. Parameter is a description of the error."
        QtGui.QMessageBox.warning(None,
                                  u'Error in an Internal Component',
                                  description,
                                  QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
                                  QtGui.QMessageBox.Ok)

    @QtCore.pyqtSlot()  # self.ui.btn_step1.clicked
    def _start_the_analysis(self):
        """
        Start the analysis, but first... check to make sure a user didn't forget to choose the
        "add voice pair" button!
        """
        # check that all the pieces have at least one part combination selected
        for i in xrange(len(self._work_wrap)):
            combos = unicode(self._work_wrap.data((i, WorkflowWrapper.parts_combinations),
                                                  Qt.DisplayRole).toPyObject())
            combos = combos.toPyObject() if isinstance(combos, QtCore.QVariant) else combos
            combos = unicode(combos)
            if WorkflowWrapper.default_value == combos:
                # we can't analyze, but we *should* tell our user
                QtGui.QMessageBox.information(None,
                    u'vis',
                    u'You forgot to add part combinations for analysis in at least one piece.',
                    QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
                    QtGui.QMessageBox.Ok)
                return None

        # See if any of the part-name checkboxes are checked... if so, we gotta warn our user!
        part_cbs_are_checked = False
        if self.part_checkboxes is not None:
            for each_box in self.part_checkboxes:
                if each_box.isChecked():
                    part_cbs_are_checked = True
                    break
            # if a checkbox was checked, inform the user they may have made a mistake, and *don't*
            # start the analysis yet
            if part_cbs_are_checked:
                response = QtGui.QMessageBox.question(None,
                    u'vis',
                    u'At least one part checkbox is selected, but you did not add the part ' + \
                        u'combination to the list of parts to analyze.\n\nDo you want to go ' + \
                        u'back and add the part combination?',
                    QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes),
                    QtGui.QMessageBox.Yes)
                if response == QtGui.QMessageBox.Yes:
                    return None

        # Since we don't do any computation during this stage, we'll just go straight to the
        # experiment-selection panel.
        self._tool_experiment()

    @QtCore.pyqtSlot()  # self.ui.chk_all_voice_combos.clicked
    def _all_voice_pairs(self):
        "When a user chooses the 'all voice pairs' checkbox"
        part_spec = None
        if self.ui.chk_all_voice_combos.isChecked():
            if self.ui.chk_all_voices.isChecked():
                self.ui.chk_all_voices.setChecked(False)
            part_spec = u'[all pairs]'
        else:
            part_spec = WorkflowWrapper.default_value
        self._update_parts_selection(part_spec)

    @QtCore.pyqtSlot()  # self.ui.line_piece_title.editingFinished
    def _update_piece_title(self):
        "When users change the piece title on the 'assemble' panel."
        # Which piece is/pieces are selected?
        currently_selected = self.ui.gui_pieces_list.selectedIndexes()

        # Find the piece title and update it
        for cell in currently_selected:
            if WorkflowWrapper.title == cell.column():
                new_title = unicode(self.ui.line_piece_title.text())
                self._work_wrap.setData(cell, new_title, Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.chk_repeat_identical.stateChanged
    def _update_repeat_identical(self):
        "When users change 'repeat consecutive identical events' on the 'assemble' panel."
        # what was the QCheckBox changed to?
        changed_to = self.ui.chk_repeat_identical.isChecked()

        # Find the piece and update its settings
        for cell in self.ui.gui_pieces_list.selectedIndexes():
            if WorkflowWrapper.repeat_identical == cell.column():
                self._work_wrap.setData(cell, changed_to, QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.btn_add_check_combo.clicked
    def _add_parts_combination(self):
        """
        When users choose the "Add Combination" button to add the currently selected part
        combination to the list of parts to analyze.
        """
        # If there are no named parts, we can't do this
        if self.part_checkboxes is None:
            return None

        # Hold indices of the selected checkboxes
        selected_checkboxes = [i for i, box in enumerate(self.part_checkboxes) if box.isChecked()]

        # Hold the vis-format specification
        vis_format = None

        # How many checkboxes are selected?
        if 1 == len(selected_checkboxes):
            # If we have one checkbox, it means we need to do monophonic analysis
            vis_format = u'[' + unicode(selected_checkboxes[0]) + u',' + \
                unicode(selected_checkboxes[0]) + u']'
        elif 1 < len(selected_checkboxes):
            # 2 or more checkboxes are selected
            vis_format = u'['
            for each_box in selected_checkboxes:
                vis_format += unicode(each_box) + u','
            vis_format = vis_format[:-1] + u']'
        else:
            # No checkboxes. Just give up.
            return

        # Now update the lists
        if vis_format is not None:
            # Hold the new part-combinations specification
            new_spec = ''
            # What's the current specification?
            curr_spec = unicode(self.ui.line_compare_these_parts.text())
            # Is curr_spec the default filler?
            if WorkflowWrapper.default_value == curr_spec:
                # Then just make a new one
                new_spec = u'[' + vis_format + u']'
                # Update the parts selection
                self._update_parts_selection(new_spec)
            # Does curr_spec include "[all]"?
            elif u'[all]' == curr_spec or u'[all pairs]' == curr_spec:
                # we'll just make a new one, and un-check the "all" QCheckBox
                new_spec = u'[' + vis_format + u']'
                self.ui.chk_all_voice_combos.setChecked(False)
                self.ui.chk_all_voices.setChecked(False)
                # NB: this has to be done after the setChecked(), since setChecked() emits a signal
                # that would otherwise obliterate the new_spec
                self._update_parts_selection(new_spec)
            # Does curr_spec contain vis_format?
            elif vis_format in curr_spec:
                pass
            # Else we must add this new thing
            else:
                # Otherwise, we should remove the final ']' in the list, and put
                # our new combo on the end
                new_spec = curr_spec[:-1] + u',' + vis_format + u']'

                # Update the parts selection
                self._update_parts_selection(new_spec)

        # Also clear the part-selection checkboxes
        for box in self.part_checkboxes:
            box.setChecked(False)

    @QtCore.pyqtSlot()  # self.ui.line_compare_these_parts.editingFinished
    def _add_parts_combo_by_line_edit(self):
        """
        Blindly put the contents of the part-specification QLineEdit into the table, trusting that
        the user knows what they're doing.
        """
        self._update_parts_selection(unicode(self.ui.line_compare_these_parts.text()))

    # Not a pyqtSlot
    def _update_parts_selection(self, part_spec):
        """
        Updates line_compare_these_parts and the model data for all selected pieces so that the
        "parts to compare" contains part_spec.
        """

        # update the UI
        self.ui.line_compare_these_parts.setText(part_spec)

        # Update the selected pieces
        # get the list of selected cells... for each one that is the "voices"
        # column(), set it to the thing specified
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.parts_combinations == cell.column():
                self._work_wrap.setData(cell, part_spec, QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.line_offset_interval.editingFinished
    def _update_offset_interval(self):
        """
        Take the value of the "offset interval" field, and sets it.

        Assumes, with no good reason, that the value put in the textbox is good.
        """
        new_offset_interval = unicode(self.ui.line_offset_interval.text())

        # Update the selected pieces
        # get the list of selected cells... for each one that is the "n"
        # column(), set it to the thing specified
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.offset_interval == cell.column():
                self._work_wrap.setData(cell, new_offset_interval, QtCore.Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.gui_pieces_list.clicked
    def _update_pieces_selection(self):
        "Update detail-selection widgets when the selected pieces are changed."

        # Which piece is/pieces are selected?
        currently_selected = self.ui.gui_pieces_list.selectedIndexes()

        # NB: we get a list of all the cells selected, and this is definitely done
        # in rows, so because each row has 6 things, if we have 6 cells, it means
        # we have only one row... but more than 6 cells means more than one row
        if len(currently_selected) == 0:
            # (1) Disable all the controls
            self.ui.line_offset_interval.setEnabled(False)
            self.ui.btn_choose_note.setEnabled(False)
            self.ui.line_compare_these_parts.setEnabled(False)
            self.ui.chk_all_voices.setEnabled(False)
            self.ui.chk_all_voice_combos.setEnabled(False)
            self.ui.btn_add_check_combo.setEnabled(False)
            self.ui.line_piece_title.setEnabled(False)
            self._piece_settings_visibility(False)
            # (2) Remove the part list
            if self.part_checkboxes is not None:
                self._update_part_checkboxes(u'erase')
        elif len(currently_selected) > 6:
            # Multiple pieces selected... possible customization
            # (1) Enable all the controls
            self.ui.line_offset_interval.setEnabled(True)
            self.ui.btn_choose_note.setEnabled(True)
            self.ui.line_compare_these_parts.setEnabled(True)
            self.ui.chk_all_voices.setEnabled(True)
            self.ui.chk_all_voice_combos.setEnabled(True)
            self.ui.btn_add_check_combo.setEnabled(True)
            self.ui.line_piece_title.setEnabled(False)  # not applicable
            self._piece_settings_visibility(True)
            self.ui.grp_settings_for_piece.setTitle(u'Settings for Selected Pieces')
            # (2) if the pieces have the same part names, display them
            # 2.1: get a list of all the lists-of-part-names
            lists_of_part_names = []
            for cell in currently_selected:
                if WorkflowWrapper.parts_list == cell.column():
                    lists_of_part_names.append(\
                        self._work_wrap.data(cell, WorkflowWrapper.ListRole).toPyObject())
            # 2.2: See if each piece has the same number of parts
            number_of_parts = 0
            for parts_list in lists_of_part_names:
                if 0 == number_of_parts:
                    number_of_parts = len(parts_list)
                elif number_of_parts != len(parts_list):
                    number_of_parts = False
                    break
            # 2.3: See if the names in each of the parts is the same
            same_names = True
            for i in xrange(len(lists_of_part_names) - 1):
                if lists_of_part_names[i] != lists_of_part_names[i + 1]:
                    same_names = False
            # 2.4: If all the part names are the same, we'll use them
            if same_names:
                self._update_part_checkboxes(currently_selected)
            # 2.5: If all the pieces have the same number of parts, we can still do it
            elif number_of_parts:
                self._update_part_checkboxes(currently_selected, no_name=True)
            # 2.6: Otherwise, we can't display part checkboxes
            else:
                #self.ui.chk_all_voice_combos.setEnabled(True)
                self._update_part_checkboxes(u'erase')
            # (3) if the pieces have the same offset interval, display it
            first_offset = None
            for cell in currently_selected:
                if WorkflowWrapper.offset_interval == cell.column():
                    if first_offset is None:
                        first_offset = self._work_wrap.\
                        data(cell, QtCore.Qt.DisplayRole).toPyObject()
                    elif first_offset == self._work_wrap.\
                    data(cell, QtCore.Qt.DisplayRole).toPyObject():
                        continue
                    else:
                        first_offset = ''
                        break
            self.ui.line_offset_interval.setText(unicode(first_offset))
            # (4) Update "Compare These Parts"
            first_comp = None
            for cell in currently_selected:
                if WorkflowWrapper.parts_combinations == cell.column():
                    if first_comp is None:
                        first_comp = self._work_wrap.\
                        data(cell, QtCore.Qt.DisplayRole).toPyObject()
                    elif first_comp == self._work_wrap.\
                    data(cell, QtCore.Qt.DisplayRole).toPyObject():
                        continue
                    else:
                        first_comp = u''
                        break
            if '' == first_comp:
                # Multiple parts have different specs
                self.ui.line_compare_these_parts.setText('')
                self.ui.chk_all_voice_combos.setChecked(False)
                self.ui.chk_all_voices.setChecked(False)
            else:
                # Multiple parts have the same spec
                self._update_comparison_parts(currently_selected)
        else:
            # Only one piece... customize for it
            # (1) Enable all the controls
            self.ui.line_offset_interval.setEnabled(True)
            self.ui.btn_choose_note.setEnabled(True)
            self.ui.line_compare_these_parts.setEnabled(True)
            self.ui.chk_all_voices.setEnabled(True)
            self.ui.chk_all_voice_combos.setEnabled(True)
            self.ui.btn_add_check_combo.setEnabled(True)
            self.ui.line_piece_title.setEnabled(True)
            self._piece_settings_visibility(True)
            self.ui.grp_settings_for_piece.setTitle(u'Settings for Selected Piece')
            # (2) Populate the part list
            self._update_part_checkboxes(currently_selected)
            # (3) Update "offset interval"
            for cell in currently_selected:
                if WorkflowWrapper.offset_interval == cell.column():
                    self.ui.line_offset_interval.setText(unicode(
                    self._work_wrap.data(cell, QtCore.Qt.DisplayRole).toPyObject()))
                    break
            # (4) Update "Compare These Parts"
            self._update_comparison_parts(currently_selected)
            # (5) Update "Pice Title"
            for cell in currently_selected:
                if WorkflowWrapper.title == cell.column():
                    self.ui.line_piece_title.setText(
                    unicode(self._work_wrap.data(cell,
                        QtCore.Qt.DisplayRole).toPyObject()))
                    break

    # Not a pyqtSlot
    def _update_comparison_parts(self, currently_selected):
        """
        When a different part combination is selected, call this method to update the "All
        Combinations" and "Basso Seguente" checkboxes.

        You should only call this method if all of the selected pieces have the same part names
        (which is true when only one part is selected).

        The argument should be a list of the currently selected cells.
        """

        for cell in currently_selected:
            if WorkflowWrapper.parts_combinations == cell.column():
                comparison_parts = unicode(self._work_wrap.
                    data(cell, QtCore.Qt.DisplayRole).toPyObject())
                self.ui.line_compare_these_parts.setText(comparison_parts)
                if u'[all]' == comparison_parts:
                    self.ui.chk_all_voice_combos.setChecked(False)
                    self.ui.chk_all_voices.setChecked(True)
                elif u'[all pairs]' == comparison_parts:
                    self.ui.chk_all_voice_combos.setChecked(True)
                    self.ui.chk_all_voices.setChecked(False)
                else:
                    self.ui.chk_all_voice_combos.setChecked(False)
                    self.ui.chk_all_voices.setChecked(False)
                break

    # Not a pyqtSlot
    def _edit_part_name(self, part_index=None):
        """
        Change the name of a part.

        This is called by lambda methods, which are slots that respond to signals emitted by
        buttons on the interface, which when they are displayed correspond each to a part in the
        score.
        """
        # Get the new name
        new_name = unicode(QtGui.QInputDialog.getText(
            None,
            u"Part Name!",
            u"Choose New Part Name",
            QtGui.QLineEdit.Normal,
            unicode(self.part_checkboxes[part_index].text()))[0])  # default filled with new name

        # Find the parts lists and update them
        for cell in self.ui.gui_pieces_list.selectedIndexes():
            if WorkflowWrapper.parts_list == cell.column():
                # We're just going to change the part name in the model, not in the
                # actual Score object itself (which would require re-loading)
                # 1.) Get the parts list
                parts = self._work_wrap.data(cell, WorkflowWrapper.ListRole).toPyObject()
                # 2.) Update the part name as requested
                parts[part_index] = new_name
                # 3.) Convert the part names to str objects (from QString)
                parts = [unicode(name) for name in parts]
                # 4.) Update the data model and QCheckBox objects
                self._work_wrap.setData(cell, parts, QtCore.Qt.EditRole)
                self._update_pieces_selection()

    # Not a pyqtSlot
    def _update_part_checkboxes(self, currently_selected, no_name=False):
        """
        Update the part-selection QCheckBox objects to reflect the currently selected part(s).

        You should only call this method if all of the selected pieces have the same part names
        (which is true when only one part is selected).

        The argument should be a list of the currently selected cells.

        If the argument is == 'erase' then the method removes all current checkboxes and stops.

        The "no_name" keyword argument means we'll use generically numbered parts, rather than
        specific part names. This is useful when, for example, a bunch of pieces are selected, and
        they all have the same number of parts but with different names.
        """

        # (1) Remove previous checkboxes from the layout
        if self.part_layouts is not None:
            for lay in self.part_layouts:
                for part in self.part_checkboxes:
                    lay.removeWidget(part)
                    part.close()
                for button in self.edit_buttons:
                    lay.removeWidget(button)
                    button.close()
                self.ui.verticalLayout_part_boxes.removeItem(lay)
                lay.invalidate()

        self.part_layouts = None
        self.part_checkboxes = None
        self.edit_buttons = None

        # (1a) If currently_selected is "erase" then we should only erase the
        # current checkboxes, and we should stop now.
        if u'erase' == currently_selected:
            return

        # (2) Get the list of parts
        list_of_parts = None
        for cell in currently_selected:
            if WorkflowWrapper.parts_list == cell.column():
                list_of_parts = self._work_wrap.data(cell, WorkflowWrapper.ListRole).toPyObject()
                break
        # deal with a possible "no_name" argument
        if no_name:
            how_many_parts = len(list_of_parts)
            list_of_parts = []
            for i in xrange(how_many_parts):
                list_of_parts.append(u'Part ' + unicode(i + 1))

        # (3) Put up a checkbox for each part
        self.part_checkboxes = []
        self.edit_buttons = []
        self.part_layouts = []
        for i in xrange(len(list_of_parts)):
            part_name = unicode(list_of_parts[i])
            # This is the New CheckBox to select this part
            n_c_b = QtGui.QCheckBox(self.ui.widget_part_boxes)
            n_c_b.setObjectName(u'chk_' + part_name)
            n_c_b.setText(part_name)

            # This is the New BuTtoN to "Edit" this part's name
            n_btn = QtGui.QPushButton(self.ui.widget_part_boxes)
            n_btn.setObjectName(u'btn_' + part_name)
            n_btn.setText(u'Edit Part Name')

            def the_thing(ell):
                "This method runs when the button is clicked"
                return lambda: self._edit_part_name(ell)
            n_btn.clicked.connect(the_thing(i))

            # Add the checkbox and button to the horizontal layout
            lay = QtGui.QHBoxLayout()
            lay.addWidget(n_c_b)
            lay.addWidget(n_btn)

            # Add the layout to the list of part-name checkboxes
            self.edit_buttons.append(n_btn)
            self.part_checkboxes.append(n_c_b)
            self.part_layouts.append(lay)

        # (4) Add all the widgets to the layout
        for part in self.part_layouts:
            self.ui.verticalLayout_part_boxes.addLayout(part)

    @QtCore.pyqtSlot() # self.ui.btn_choose_note.clicked
    def _launch_offset_selection(self):
        "Launch the dialogue box to help users visually select offset values."
        # Launch the offset-selection QDialog
        selector = VisOffsetSelector()
        chosen_offset = selector.trigger()
        # Update the QLineEdit
        if u'ALL' == chosen_offset:
            chosen_offset = u'(optional)'
        self.ui.line_offset_interval.setText(chosen_offset)
        # Set values in the model
        selected_cells = self.ui.gui_pieces_list.selectedIndexes()
        for cell in selected_cells:
            if WorkflowWrapper.offset_interval == cell.column():
                self._work_wrap.setData(cell, chosen_offset, QtCore.Qt.EditRole)
        # Just to make sure we get rid of this
        del selector

    # Not a pyqtSlot
    def _piece_settings_visibility(self, set_to):
        """
        Given True or False, makes visible and enables (or makes invisible and disables)
        everything in the "Settings for Piece" QGroupBox (and the box itself), and the opposite
        for self.lbl_select_piece
        """
        self.ui.grp_settings_for_piece.setVisible(set_to)
        self.ui.grp_settings_for_piece.setEnabled(set_to)
        self.ui.widget_select_piece.setVisible(not set_to)

    @QtCore.pyqtSlot()  # self.ui.rdo_consider_***.clicked()
    def _update_experiment_from_object(self):
        """
        When one of the self.ui.rdo_consider_*** radio buttons is selected, this method updates the
        rest of the GUI to reflect the options relevant to that output method.
        """

        all_the_widgets = [self.ui.rdo_table,
                           self.ui.rdo_chart,
                           self.ui.grp_values_of_n,
                           self.ui.rdo_score,
                           self.ui.grp_annotate_these,
                           self.ui.grp_ignore_inversion,
                           self.ui.grp_annotate_these]

        def on_offer(enable_these):
            """
            Given a list of the GUI objects in the "experiment" panel that should be "on," this
            method enables them and disables all the others.
            """

            # (1) Disable everything
            for each_widget in all_the_widgets:
                each_widget.setVisible(False)

            # (2) Enable the valid things
            for each_widget in enable_these:
                each_widget.setVisible(True)

        # Determine which widgets to enable
        which_to_enable = []

        if self.ui.rdo_consider_intervals.isChecked():
            which_to_enable = [self.ui.rdo_table, self.ui.rdo_chart, self.ui.rdo_score,
                               self.ui.grp_ignore_inversion, self.ui.grp_annotate_these]
        elif self.ui.rdo_consider_interval_ngrams.isChecked():
            which_to_enable = [self.ui.rdo_table, self.ui.grp_values_of_n, self.ui.rdo_chart,
                               self.ui.grp_ignore_inversion]
        elif self.ui.rdo_consider_score.isChecked():
            which_to_enable = [self.ui.rdo_score]

        # Run the on_offer()
        on_offer(which_to_enable)

        # Choose the first enabled output format in "How to Show Results"; update filters
        outs = [self.ui.rdo_table, self.ui.rdo_chart, self.ui.rdo_score]
        for each_box in outs:
            if each_box.isVisible():
                each_box.setChecked(True)
                break
        which_to_enable = [self.ui.rdo_score]
        self._output_format_changed()

    def _output_format_changed(self):
        """
        When a user chooses a different output format (in "How to Show Results" on the "show"
        panel), we may have to enable/disable the Top X and Threshold filter.
        """
        if self.ui.rdo_table.isChecked() or self.ui.rdo_chart.isChecked():
            self.ui.group_top_x.setVisible(True)
            self.ui.group_threshold.setVisible(True)
        else:
            self.ui.group_top_x.setVisible(False)
            self.ui.group_threshold.setVisible(False)

    @QtCore.pyqtSlot()  # self.ui.chk_all_voices.stateChanged
    def _all_voices(self):
        "When a user chooses the 'all voices' checkbox"
        part_spec = None
        if self.ui.chk_all_voices.isChecked():
            if self.ui.chk_all_voice_combos.isChecked():
                self.ui.chk_all_voice_combos.setChecked(False)
            part_spec = u'[all]'
        else:
            part_spec = WorkflowWrapper.default_value
        self._update_parts_selection(part_spec)

    @QtCore.pyqtSlot()  # self.ui.rdo_heedQuality and rdo_noHeedQuality
    def _update_quality(self):
        "When a user changes the 'display quality' radio buttons."
        set_to_what = True if self.ui.rdo_heedQuality.isChecked() else False
        self._work_wrap.setData((0, WorkflowWrapper.quality), set_to_what, Qt.EditRole)

    @QtCore.pyqtSlot()  # self.ui.rdo_compound and rdo_simple
    def _update_octaves(self):
        "When a user changes the 'simple/compound' radio buttons."
        set_to_what = True if self.ui.rdo_simple.isChecked() else False
        self._work_wrap.setData((0, WorkflowWrapper.simple_ints), set_to_what, Qt.EditRole)

    def _update_global_settings(self):
        """
        Update the radio buttons associated with global settings (at the moment, quality and simple)
        """
        if self._work_wrap.data((0, WorkflowWrapper.quality), Qt.DisplayRole).toPyObject():
            self.ui.rdo_heedQuality.setChecked(True)
        else:
            self.ui.rdo_noHeedQuality.setChecked(True)
        if self._work_wrap.data((0, WorkflowWrapper.simple_ints), Qt.DisplayRole).toPyObject():
            self.ui.rdo_simple.setChecked(True)
        else:
            self.ui.rdo_compound.setChecked(True)

    @QtCore.pyqtSlot()  # self.ui.btn_show_results.clicked
    def _prepare_experiment_submission(self):
        """
        Make sure the Experimenter has a properly-configured Settings instance, then ask it to run
        the experiment.
        """

        # move to the "working" panel and update it
        self._tool_working()
        self._update_progress_bar(u'0')
        self._update_progress_bar(u'Initializing experiment.')

        # hold a list of tuples to be signalled as settings
        list_of_settings = {}

        def do_experiment():
            "Which experiment does the user want to run?"
            # experiment
            if self.ui.rdo_consider_intervals.isChecked():
                list_of_settings['experiment'] = u'intervals'
            elif self.ui.rdo_consider_interval_ngrams.isChecked():
                list_of_settings['experiment'] = u'interval n-grams'
            elif self.ui.rdo_consider_score.isChecked():
                list_of_settings['experiment'] = 'LilyPondExperiment'
                list_of_settings['output format'] = 'LilyPondDisplay'
            # output format
            if self.ui.rdo_table.isChecked():
                list_of_settings['output format'] = 'table'
            elif self.ui.rdo_chart.isChecked():
                list_of_settings['output format'] = 'chart'
            elif self.ui.rdo_score.isChecked():
                list_of_settings['output format'] = 'lilypond'

        def do_threshold():
            "Is there a threshold value?"
            threshold = self.ui.spin_threshold.value()
            if 0 != threshold:
                list_of_settings['threshold'] = threshold

        def do_top_x():
            "Is there a 'top x' value?"
            top_x = self.ui.spin_top_x.value()
            if 0 != top_x:
                list_of_settings['topX'] = top_x

        def do_values_of_n():
            "Are there values of 'n' specified?"
            enn = self.ui.spin_n.value()
            list_of_settings['n'] = enn

        def do_ignore_inversion():
            "Ignore inversion?"
            if self.ui.chk_ignore_inversion.isChecked():
                list_of_settings['ignore direction'] = True
            else:
                list_of_settings['ignore direction'] = False

        # (1) Figure out the settings
        # (1a) Which experiment?
        do_experiment()
        # (1d) Is there a "values_of_n" value?
        if u'interval n-grams' == list_of_settings[u'experiment']:
            do_values_of_n()
        # (1e) Threshold
        do_threshold()
        # (1f) Top X
        do_top_x()
        # (1g) Ignore Voice Crossing
        do_ignore_inversion()

        # (2) Run the experiment
        self._run_the_experiment(list_of_settings)

    def _run_the_experiment(self, settings):
        """
        Run the experiment as instructed by the 'settings' argument.
        """
        # 1.) Do we need to run the experiment at all?
        workm = self._work_wrap.get_workflow_manager()
        run_exp = None
        # 1.a) If the model thinks settings were changed (i.e., user went back to "analyze" page).
        if self._work_wrap.settings_changed():
            run_exp = True
        # 1.b) If there was no previous experiment, then yes.
        elif self._prev_setts is None:
            run_exp = True
        # 1.c) If the previous experiment is different, then yes.
        elif self._prev_setts['experiment'] != settings['experiment']:
            run_exp = True
        # 1.d) If the experiments are the same, but they have 'n' and they're different, then yes.
        elif 'n' in self._prev_setts and self._prev_setts['n'] != settings['n']:
            run_exp = True
        # 1.e) Save the settings for next time.
        self._prev_setts = settings

        # 2.) Run the experiment, if required.
        if run_exp is True:
            if u'n' in settings:
                workm.settings(None, u'n', settings[u'n'])
            workm.run(settings[u'experiment'])

        # 3.) Run the output-getting stuff.
        path = None
        result_type = None
        top_x = settings['topX'] if 'topX' in settings else None
        threshold = settings['threshold'] if 'threshold' in settings else None
        if u'chart' == settings[u'output format']:
            result_type = u'image'
            path = workm.output(u'R histogram', u'outputs/R_chart.png', top_x, threshold)
            # We add "../" because we expect the image to be in the "outputs" directory, but so is
            # the HTML file. We could just set "path" to "R_chart.png" but output() doesn't
            # guarantee that it'll actually output to the pathname requested, so we need it.
            path = u'../' + path
        elif u'table' == settings[u'output format']:
            result_type = u'table'
            path = workm.export(u'HTML', u'outputs/pandas_table.html', top_x, threshold)
        else:
            VisQtMainWindow._error_reporter(u'Unrecognized output format: "' + \
                                            unicode(settings[u'output format']) + u'"')

        if path is not None:
            token_name = u'Interval' if u'intervals' == settings[u'experiment'] else \
                    unicode(settings['n']) + u'-Gram'
            webview = VisWebView()
            trig_ret = webview.trigger(path, result_type, token_name)
            # we may have to save the output!
            if trig_ret is not None:
                for form, pathname in trig_ret:
                    workm.export(form, pathname, top_x, threshold)
            del webview  # make sure we free dat memory!

        self._tool_experiment()