def __init__( self, engine_manager, package, version ):
        super( QMainWindow, self ).__init__()
        self._engine_manager = engine_manager
        self.setupUi( self )
        self.setWindowTitle( "{0} v{1}".format( package, version ) )

        self.loopListViewModel = LoopListModel()
        self.loopListView.setModel( self.loopListViewModel )

        self.mappingTableViewModel = MappingTableModel()
        self.mappingTableView.setModel( self.mappingTableViewModel )
        # PyUIC doesn't generate code for word wrap :(
        self.mappingTableView.setWordWrap( True )
        self.mappingTableView.resizeRowsToContents()
        tableHeader = self.mappingTableView.horizontalHeader()
        tableHeader.setSectionResizeMode( QtWidgets.QHeaderView.Stretch )

        # Only update the model on the GUI thread - thread-safe.
        self.loopUpdated.connect(
            self._loop_update_handler, type=QtCore.Qt.QueuedConnection )
        self.mappingUpdated.connect(
            self._mapping_update_handler, type=QtCore.Qt.QueuedConnection )
        self._engine_manager.subscribe( "loops", self.signal_loop_update )
        self._engine_manager.subscribe( "mappings", self.signal_mapping_update )

        QtCore.QTimer.singleShot( 0, self.mappingTableView.resizeRowsToContents )
class _LooperWindow( QMainWindow, Ui_MainWindow ):

    loopUpdated = QtCore.pyqtSignal( str, str )
    mappingUpdated = QtCore.pyqtSignal( str, MIDIMappingInfo )

    def __init__( self, engine_manager, package, version ):
        super( QMainWindow, self ).__init__()
        self._engine_manager = engine_manager
        self.setupUi( self )
        self.setWindowTitle( "{0} v{1}".format( package, version ) )

        self.loopListViewModel = LoopListModel()
        self.loopListView.setModel( self.loopListViewModel )

        self.mappingTableViewModel = MappingTableModel()
        self.mappingTableView.setModel( self.mappingTableViewModel )
        # PyUIC doesn't generate code for word wrap :(
        self.mappingTableView.setWordWrap( True )
        self.mappingTableView.resizeRowsToContents()
        tableHeader = self.mappingTableView.horizontalHeader()
        tableHeader.setSectionResizeMode( QtWidgets.QHeaderView.Stretch )

        # Only update the model on the GUI thread - thread-safe.
        self.loopUpdated.connect(
            self._loop_update_handler, type=QtCore.Qt.QueuedConnection )
        self.mappingUpdated.connect(
            self._mapping_update_handler, type=QtCore.Qt.QueuedConnection )
        self._engine_manager.subscribe( "loops", self.signal_loop_update )
        self._engine_manager.subscribe( "mappings", self.signal_mapping_update )

        QtCore.QTimer.singleShot( 0, self.mappingTableView.resizeRowsToContents )

    def signal_loop_update( self, change, data ):
        self.loopUpdated.emit( change, data )

    def signal_mapping_update( self, change, data ):
        self.mappingUpdated.emit( change, data )

    def _loop_update_handler( self, change, data ):
        if change == "add":
            self.loopListViewModel.insertLoopRow(
                self.loopListViewModel.rowCount(), data )
        elif change == "remove":
            self.loopListViewModel.removeLoop( data )
        else:
            raise ValueError( "Invalid loop update!" )

    def _mapping_update_handler( self, change, data ):
        if change == "add":
            self.mappingTableViewModel.insertMappingRow(
                self.mappingTableViewModel.rowCount(), data )
            self.mappingTableView.resizeRowsToContents() # HACK
        elif change == "remove":
            self.mappingTableViewModel.removeMapping( data )
        else:
            raise ValueError( "Invalid mapping update!" )

    # Slots are named in camelcase for consistency with the rest of Qt.
    def newLoop( self ):
        new_loop_name, ok = QtWidgets.QInputDialog.getText( self, "New Loop",
            "Provide the name for the new loop." )
        if ok:
            self._engine_manager.new_loop( new_loop_name )

    def removeLoops( self ):
        # Gets the names because a normal iterative removal would invalidate the
        # index list at each step.  This is not particularly efficient.
        selected = [self.loopListViewModel.data( x ) for x in
            self.loopListView.selectedIndexes()]
        self._engine_manager.remove_loops( selected )

    def newMapping( self ):
        loops = [
            self.loopListViewModel.data( self.loopListViewModel.createIndex( x, 0 ) )
                for x in range( 0, self.loopListViewModel.rowCount() ) ]

        if not loops:
            QtWidgets.QMessageBox.warning( self, "No loops",
                "You must add a loop before defining mappings." )
            return

        mapping_dialog = _MappingDialog( loops, self )
        if mapping_dialog.exec_():
            channel, midi_type, value, name, action = mapping_dialog.getMappingData()
            self._engine_manager.new_mapping(
                MIDIMappingInfo( channel, midi_type, value, name, action ) )

    def removeMappings( self ):
        mappings = [self.mappingTableViewModel.dataRow( x.row() )
            for x in self.mappingTableView.selectionModel().selectedRows()]
        self._engine_manager.remove_mappings( mappings )