Exemplo n.º 1
0
    def __init__(self, signals, directory: str):
        super().__init__()
        self.setupUi(self)

        # init attributes
        self.signals = signals
        self.directory = directory
        self.db = database.DBHandler()
        self.books = []
        self.selected = None

        self.details_panel = DetailsPanel(self.db, self.signals)
        self.search_panel = SearchPanel(self.db, self.signals)
        self.metadata_panel = MetadataPanel(self.db, self.signals)
        self.setup_panels()

        self.connect_events()
        self.populate_gallery()
Exemplo n.º 2
0
    def __init__(self):
        appdata = str(
            QDesktopServices.storageLocation(QDesktopServices.DataLocation))
        MusicGuruBase.__init__(self, appdata)
        ApplicationBase.__init__(self)
        if not op.exists(appdata):
            os.makedirs(appdata)
        logging.basicConfig(filename=op.join(appdata, 'debug.log'),
                            level=logging.WARNING)
        self.prefs = Preferences()
        self.prefs.load()
        self.selectedBoardItems = []
        self.selectedLocation = None
        self.mainWindow = MainWindow(app=self)
        self.locationsPanel = LocationsPanel(app=self)
        self.detailsPanel = DetailsPanel(app=self)
        self.ignoreBox = IgnoreBox(app=self)
        self.progress = Progress(self.mainWindow)
        self.aboutBox = AboutBox(self.mainWindow, self)

        self.connect(self.progress, SIGNAL('finished(QString)'),
                     self.jobFinished)
        self.connect(self, SIGNAL('applicationFinishedLaunching()'),
                     self.applicationFinishedLaunching)
Exemplo n.º 3
0
 def __init__(self):
     appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
     MusicGuruBase.__init__(self, appdata)
     ApplicationBase.__init__(self)
     if not op.exists(appdata):
         os.makedirs(appdata)
     logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING)
     self.prefs = Preferences()
     self.prefs.load()
     self.selectedBoardItems = []
     self.selectedLocation = None
     self.mainWindow = MainWindow(app=self)
     self.locationsPanel = LocationsPanel(app=self)
     self.detailsPanel = DetailsPanel(app=self)
     self.ignoreBox = IgnoreBox(app=self)
     self.progress = Progress(self.mainWindow)
     self.aboutBox = AboutBox(self.mainWindow, self)
     
     self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished)
     self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
Exemplo n.º 4
0
class Home(QMainWindow, Ui_MainWindow):
    """The main screen

    Args:
        signals (signals.Signals)
        directory (str)

    Attributes:
        search_bar (QLineEdit)
        search_button (QPushButton): same function as pressing entet in search bar
        details_button (QPushButton)
        advanced_search_button (QPushButton): opens a new window with all searchable options
        metadata_button (QPushButton):
        sort_by (QComboBox): contains "Alphabetically", "Rating", and "Randomly"
        random_button (QPushButton): randomly selects from the list

        main_area (QHBoxLayout)
        bookshelf (QGridLayout): the layout where all the books will be displayed
        bookshelf_area (QScrollArea): the whole area where books will be displayed

        books ([QFrame]): holds a list of all book frames
        selected (QFrame): holds the currently selected book
        details_panel (DetailsPanel)
        search_panel (SearchPanel)
        metadata_panel (MetadataPanel)
    """
    def __init__(self, signals, directory: str):
        super().__init__()
        self.setupUi(self)

        # init attributes
        self.signals = signals
        self.directory = directory
        self.db = database.DBHandler()
        self.books = []
        self.selected = None

        self.details_panel = DetailsPanel(self.db, self.signals)
        self.search_panel = SearchPanel(self.db, self.signals)
        self.metadata_panel = MetadataPanel(self.db, self.signals)
        self.setup_panels()

        self.connect_events()
        self.populate_gallery()

    def setup_panels(self):
        """Adds all panels to the main layout and makes only the details panel visible
        """
        self.main_area.addWidget(self.details_panel)
        self.main_area.addWidget(self.search_panel)
        self.main_area.addWidget(self.metadata_panel)
        self.search_panel.setVisible(False)
        self.metadata_panel.setVisible(False)

    def connect_events(self):
        """Connects each signal to their respective functions
        """
        # dropdowns
        self.sort_by.textActivated.connect(self.sort_gallery)

        # buttons
        self.details_button.clicked.connect(lambda: [
            self.details_panel.setVisible(True),
            self.search_panel.setVisible(False),
            self.metadata_panel.setVisible(False)
        ])
        self.advanced_search_button.clicked.connect(lambda: [
            self.details_panel.setVisible(False),
            self.search_panel.setVisible(True),
            self.metadata_panel.setVisible(False)
        ])
        self.metadata_button.clicked.connect(lambda: [
            self.details_panel.setVisible(False),
            self.search_panel.setVisible(False),
            self.metadata_panel.setVisible(True)
        ])
        self.random_button.clicked.connect(self.random_select)

        # bookshelf
        self.bookshelf_area.contextMenuEvent = self.context_menu
        self.bookshelf_area.mousePressEvent = self.reset_selected

        # signals
        self.signals.update_spines.connect(self.update_gallery)
        self.signals.search_advanced.connect(self.populate_gallery)

    def random_select(self):
        """Randomly selects a book from the list
        """
        self.select(random.choice(self.books))

    def generate_books(self, filters, sort):
        """Creates objects for the books based on certain search parameters

        Args:
            filters (dict, list, optional): filters to filter by (either a dict from search panel or a list from sort_gallery)
            sort (int): obtained from self.sort_by.currentIndex()
        """
        self.books = []
        for book in self.db.get_books(filters, sort):
            # set up frame
            spine = spines.BookSpine(book['id'], book['name'],
                                     book['directory'])

            # add the new frames to the list
            self.books.append(spine)

            # connect events
            spine.mousePressEvent = partial(self.select, self.books[-1])
            spine.mouseDoubleClickEvent = partial(self.open_book,
                                                  self.books[-1])
            spine.enterEvent = partial(self.highlight, self.books[-1])
            spine.leaveEvent = partial(self.unhighlight, self.books[-1])

    def populate_gallery(self, filters=None):
        """Populates the gallery with books that match a search filter

        Books should be generated by self.generate_books()

        Args:
            filters (dict, list, optional): filters to filter by (either a dict from search panel or a list from sort_gallery)
        """
        self.clear_gallery()
        self.generate_books(filters, self.sort_by.currentIndex())

        row_pos = 0
        col_pos = 0
        for book in self.books:
            self.bookshelf.addWidget(book, col_pos, row_pos)

            # calculate next position
            row_pos += 1
            if row_pos > 4:  # max 5 columns. aka 5 slots per row
                row_pos = 0
                col_pos += 1

        while col_pos <= 1:
            # if there's only a few books, place invisible frames to shove things into the top left corner
            # this avoids books showing up in the middle and messing up the look of the gallery
            blank = spines.BlankSpine()
            self.bookshelf.addWidget(blank, col_pos, row_pos)

            # calculate next position
            row_pos += 1
            if row_pos > 4:  # max 5 columns. aka 5 slots per row
                row_pos = 0
                col_pos += 1

        if (new_select := next((x for x in self.books if x == self.selected),
                               None)):
            self.select(new_select)
        else:
Exemplo n.º 5
0
class MusicGuru(MusicGuruBase, ApplicationBase):
    LOGO_NAME = 'mg_logo'

    def __init__(self):
        appdata = str(
            QDesktopServices.storageLocation(QDesktopServices.DataLocation))
        MusicGuruBase.__init__(self, appdata)
        ApplicationBase.__init__(self)
        if not op.exists(appdata):
            os.makedirs(appdata)
        logging.basicConfig(filename=op.join(appdata, 'debug.log'),
                            level=logging.WARNING)
        self.prefs = Preferences()
        self.prefs.load()
        self.selectedBoardItems = []
        self.selectedLocation = None
        self.mainWindow = MainWindow(app=self)
        self.locationsPanel = LocationsPanel(app=self)
        self.detailsPanel = DetailsPanel(app=self)
        self.ignoreBox = IgnoreBox(app=self)
        self.progress = Progress(self.mainWindow)
        self.aboutBox = AboutBox(self.mainWindow, self)

        self.connect(self.progress, SIGNAL('finished(QString)'),
                     self.jobFinished)
        self.connect(self, SIGNAL('applicationFinishedLaunching()'),
                     self.applicationFinishedLaunching)

    #--- Private
    def _placeDetailsPanel(self):
        # locations panel must be placed first
        if self.detailsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        h = self.detailsPanel.height()
        x = self.locationsPanel.x()
        windowBottom = self.locationsPanel.frameGeometry().y(
        ) + self.locationsPanel.frameGeometry().height()
        y = windowBottom
        self.detailsPanel.move(x, y)
        self.detailsPanel.resize(w, h)

    def _placeIgnoreBox(self):
        if self.ignoreBox.isVisible():
            return
        desktop = QApplication.desktop()
        windowWidth = self.mainWindow.frameGeometry().width()
        frameWidth = self.ignoreBox.frameGeometry().width(
        ) - self.ignoreBox.width()
        w = windowWidth - frameWidth
        h = self.ignoreBox.height()
        x = self.mainWindow.x()
        windowBottom = self.mainWindow.frameGeometry().y(
        ) + self.mainWindow.frameGeometry().height()
        y = min(windowBottom, desktop.height() - h)
        self.ignoreBox.move(x, y)
        self.ignoreBox.resize(w, h)

    def _placeLocationsPanel(self):
        if self.locationsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        windowHeight = self.mainWindow.frameGeometry().height()
        frameHeight = self.locationsPanel.frameGeometry().height(
        ) - self.locationsPanel.height()
        h = windowHeight - frameHeight - self.detailsPanel.frameGeometry(
        ).height()
        windowRight = self.mainWindow.frameGeometry().x(
        ) + self.mainWindow.frameGeometry().width()
        x = min(windowRight, desktop.width() - w)
        y = self.mainWindow.y()
        self.locationsPanel.move(x, y)
        self.locationsPanel.resize(w, h)

    def _setup_as_registered(self):
        self.prefs.registration_code = self.registration_code
        self.prefs.registration_email = self.registration_email
        self.prefs.save()
        self.mainWindow.actionRegister.setVisible(False)
        self.aboutBox.registerButton.hide()
        self.aboutBox.registeredEmailLabel.setText(
            self.prefs.registration_email)

    def _startJob(self, jobid, func):
        title = JOBID2TITLE[jobid]
        try:
            j = self.progress.create_job()
            self.progress.run(jobid, title, func, args=(j, ))
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)

    #--- Public
    def addLocation(self, path, name, removeable):
        def do(j):
            MusicGuruBase.AddLocation(self, path, name, removeable, j)

        error_msg = self.CanAddLocation(path, name)
        if error_msg:
            QMessageBox.warning(self.mainWindow, "Add Location", error_msg)
            return
        self._startJob(JOB_ADD, do)

    def addLocationPrompt(self):
        dialog = AddLocationDialog(self)
        result = dialog.exec_()
        if result == QDialog.Accepted:
            self.addLocation(dialog.locationPath, dialog.locationName,
                             dialog.isLocationRemovable)

    def askForRegCode(self):
        if self.reg.ask_for_code():
            self._setup_as_registered()

    def copyOrMove(self, copy):
        def onNeedCd(location):
            # We can't do anything GUI related in a separate thread with Qt. Since copy/move
            # operations are performed asynchronously, the calls made to needCdDialog (created in
            # the main thread) must also be made asynchronously.
            return needCdDialog.askForDiskAsync(location.name)

        def do(j):
            MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd)

        needCdDialog = DiskNeededDialog()
        title = "Choose a destination"
        flags = QFileDialog.ShowDirsOnly
        dirpath = str(
            QFileDialog.getExistingDirectory(self.mainWindow, title, '',
                                             flags))
        if dirpath:
            jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE
            self._startJob(jobid, do)

    def massRename(self, model, whitespace):
        def do(j):
            self.board.MassRename(model, whitespace, j)

        self._startJob(JOB_MASS_RENAME, do)

    def moveConflicts(self, with_original=False):
        if self.board.MoveConflicts(with_original=with_original) > 0:
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))

    def moveSelectedToIgnoreBox(self):
        smart_move(self.selectedBoardItems,
                   self.board.ignore_box,
                   allow_merge=True)
        self.emit(SIGNAL('boardChanged()'))
        self.emit(SIGNAL('ignoreBoxChanged()'))

    def removeEmptyFolders(self):
        MusicGuruBase.RemoveEmptyDirs(self)
        self.emit(SIGNAL('boardChanged()'))

    def removeLocation(self, location):
        self.board.RemoveLocation(location)
        location.delete()
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))

    def removeLocationPrompt(self):
        location = self.selectedLocation
        if location is None:
            return
        title = "Remove location"
        msg = "Do you really want to remove location {0}?".format(
            location.name)
        buttons = QMessageBox.Yes | QMessageBox.No
        answer = QMessageBox.question(self.mainWindow, title, msg, buttons,
                                      QMessageBox.Yes)
        if answer != QMessageBox.Yes:
            return
        self.removeLocation(location)

    def renameInRespectiveLocations(self):
        def do(j):
            MusicGuruBase.RenameInRespectiveLocations(self, j)

        self._startJob(JOB_MATERIALIZE_RENAME, do)

    def selectBoardItems(self, items):
        self.selectedBoardItems = items
        self.emit(SIGNAL('boardSelectionChanged()'))

    def selectLocation(self, location):
        self.selectedLocation = location

    def showAboutBox(self):
        self.aboutBox.show()

    def showDetailsPanel(self):
        self._placeLocationsPanel()
        self._placeDetailsPanel()
        self.detailsPanel.show()
        self.detailsPanel.activateWindow()

    def showHelp(self):
        url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm'))
        QDesktopServices.openUrl(url)

    def showIgnoreBox(self):
        self._placeIgnoreBox()
        self.ignoreBox.show()
        self.ignoreBox.activateWindow()

    def showLocationPanel(self):
        self._placeLocationsPanel()
        self.locationsPanel.show()
        self.locationsPanel.activateWindow()

    def split(self, model, capacity, grouping_level):
        def do(j):
            self.board.Split(model, capacity, grouping_level, j)

        self._startJob(JOB_SPLIT, do)

    def toggleLocation(self, location):
        self.board.ToggleLocation(location)
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))

    def undoSplit(self):
        self.board.Unsplit()
        self.emit(SIGNAL('boardChanged()'))

    def updateCollection(self):
        def do(j):
            self.collection.update_volumes(j)

        self._startJob(JOB_UPDATE, do)

    def updateLocation(self, location):
        def do(j):
            location.update(None, j)

        self._startJob(JOB_UPDATE, do)

    #--- Events
    def applicationFinishedLaunching(self):
        self.reg = Registration(self)
        self.set_registration(self.prefs.registration_code,
                              self.prefs.registration_email)
        if not self.registered and self.unpaid_hours >= 1:
            self.reg.show_nag()
        self.mainWindow.show()
        self.showLocationPanel()
        self.showDetailsPanel()
        self.updateCollection()

    def jobFinished(self, jobid):
        if jobid in (JOB_UPDATE, JOB_ADD):
            self.emit(SIGNAL('locationsChanged()'))
        if jobid in (JOB_MASS_RENAME, JOB_SPLIT):
            self.emit(SIGNAL('boardChanged()'))
        if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE):
            self.board.Empty()
            self.emit(SIGNAL('locationsChanged()'))
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))
Exemplo n.º 6
0
class MusicGuru(MusicGuruBase, ApplicationBase):
    LOGO_NAME = 'mg_logo'
    
    def __init__(self):
        appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
        MusicGuruBase.__init__(self, appdata)
        ApplicationBase.__init__(self)
        if not op.exists(appdata):
            os.makedirs(appdata)
        logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING)
        self.prefs = Preferences()
        self.prefs.load()
        self.selectedBoardItems = []
        self.selectedLocation = None
        self.mainWindow = MainWindow(app=self)
        self.locationsPanel = LocationsPanel(app=self)
        self.detailsPanel = DetailsPanel(app=self)
        self.ignoreBox = IgnoreBox(app=self)
        self.progress = Progress(self.mainWindow)
        self.aboutBox = AboutBox(self.mainWindow, self)
        
        self.connect(self.progress, SIGNAL('finished(QString)'), self.jobFinished)
        self.connect(self, SIGNAL('applicationFinishedLaunching()'), self.applicationFinishedLaunching)
    
    #--- Private
    def _placeDetailsPanel(self):
        # locations panel must be placed first
        if self.detailsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        h = self.detailsPanel.height()
        x = self.locationsPanel.x()
        windowBottom = self.locationsPanel.frameGeometry().y() + self.locationsPanel.frameGeometry().height()
        y = windowBottom
        self.detailsPanel.move(x, y)
        self.detailsPanel.resize(w, h)
    
    def _placeIgnoreBox(self):
        if self.ignoreBox.isVisible():
            return
        desktop = QApplication.desktop()
        windowWidth = self.mainWindow.frameGeometry().width()
        frameWidth = self.ignoreBox.frameGeometry().width() - self.ignoreBox.width()
        w = windowWidth - frameWidth
        h = self.ignoreBox.height()
        x = self.mainWindow.x()
        windowBottom = self.mainWindow.frameGeometry().y() + self.mainWindow.frameGeometry().height()
        y = min(windowBottom, desktop.height() - h)
        self.ignoreBox.move(x, y)
        self.ignoreBox.resize(w, h)
    
    def _placeLocationsPanel(self):
        if self.locationsPanel.isVisible():
            return
        desktop = QApplication.desktop()
        w = self.locationsPanel.width()
        windowHeight = self.mainWindow.frameGeometry().height()
        frameHeight = self.locationsPanel.frameGeometry().height() - self.locationsPanel.height()
        h = windowHeight - frameHeight - self.detailsPanel.frameGeometry().height()
        windowRight = self.mainWindow.frameGeometry().x() + self.mainWindow.frameGeometry().width()
        x = min(windowRight, desktop.width() - w)
        y = self.mainWindow.y()
        self.locationsPanel.move(x, y)
        self.locationsPanel.resize(w, h)
    
    def _setup_as_registered(self):
        self.prefs.registration_code = self.registration_code
        self.prefs.registration_email = self.registration_email
        self.prefs.save()
        self.mainWindow.actionRegister.setVisible(False)
        self.aboutBox.registerButton.hide()
        self.aboutBox.registeredEmailLabel.setText(self.prefs.registration_email)
    
    def _startJob(self, jobid, func):
        title = JOBID2TITLE[jobid]
        try:
            j = self.progress.create_job()
            self.progress.run(jobid, title, func, args=(j, ))
        except job.JobInProgressError:
            msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
            QMessageBox.information(self.mainWindow, "Action in progress", msg)
    
    #--- Public
    def addLocation(self, path, name, removeable):
        def do(j):
            MusicGuruBase.AddLocation(self, path, name, removeable, j)
        
        error_msg = self.CanAddLocation(path, name)
        if error_msg:
            QMessageBox.warning(self.mainWindow, "Add Location", error_msg)
            return
        self._startJob(JOB_ADD, do)
    
    def addLocationPrompt(self):
        dialog = AddLocationDialog(self)
        result = dialog.exec_()
        if result == QDialog.Accepted:
            self.addLocation(dialog.locationPath, dialog.locationName, dialog.isLocationRemovable)
    
    def askForRegCode(self):
        if self.reg.ask_for_code():
            self._setup_as_registered()
    
    def copyOrMove(self, copy):
        def onNeedCd(location):
            # We can't do anything GUI related in a separate thread with Qt. Since copy/move
            # operations are performed asynchronously, the calls made to needCdDialog (created in
            # the main thread) must also be made asynchronously.
            return needCdDialog.askForDiskAsync(location.name)
        
        def do(j):
            MusicGuruBase.CopyOrMove(self, copy, dirpath, j, onNeedCd)
        
        needCdDialog = DiskNeededDialog()
        title = "Choose a destination"
        flags = QFileDialog.ShowDirsOnly
        dirpath = str(QFileDialog.getExistingDirectory(self.mainWindow, title, '', flags))
        if dirpath:
            jobid = JOB_MATERIALIZE_COPY if copy else JOB_MATERIALIZE_MOVE
            self._startJob(jobid, do)
    
    def massRename(self, model, whitespace):
        def do(j):
            self.board.MassRename(model, whitespace, j)
        
        self._startJob(JOB_MASS_RENAME, do)
    
    def moveConflicts(self, with_original=False):
        if self.board.MoveConflicts(with_original=with_original) > 0:
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))
    
    def moveSelectedToIgnoreBox(self):
        smart_move(self.selectedBoardItems, self.board.ignore_box, allow_merge=True)
        self.emit(SIGNAL('boardChanged()'))
        self.emit(SIGNAL('ignoreBoxChanged()'))
    
    def removeEmptyFolders(self):
        MusicGuruBase.RemoveEmptyDirs(self)
        self.emit(SIGNAL('boardChanged()'))
    
    def removeLocation(self, location):
        self.board.RemoveLocation(location)
        location.delete()
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))
    
    def removeLocationPrompt(self):
        location = self.selectedLocation
        if location is None:
            return
        title = "Remove location"
        msg = "Do you really want to remove location {0}?".format(location.name)
        buttons = QMessageBox.Yes | QMessageBox.No
        answer = QMessageBox.question(self.mainWindow, title, msg, buttons, QMessageBox.Yes)
        if answer != QMessageBox.Yes:
            return
        self.removeLocation(location)
    
    def renameInRespectiveLocations(self):
        def do(j):
            MusicGuruBase.RenameInRespectiveLocations(self, j)
        
        self._startJob(JOB_MATERIALIZE_RENAME, do)
    
    def selectBoardItems(self, items):
        self.selectedBoardItems = items
        self.emit(SIGNAL('boardSelectionChanged()'))
    
    def selectLocation(self, location):
        self.selectedLocation = location
    
    def showAboutBox(self):
        self.aboutBox.show()
    
    def showDetailsPanel(self):
        self._placeLocationsPanel()
        self._placeDetailsPanel()
        self.detailsPanel.show()
        self.detailsPanel.activateWindow()
    
    def showHelp(self):
        url = QUrl.fromLocalFile(op.join(op.abspath(HELP_PATH), 'intro.htm'))
        QDesktopServices.openUrl(url)
    
    def showIgnoreBox(self):
        self._placeIgnoreBox()
        self.ignoreBox.show()
        self.ignoreBox.activateWindow()
    
    def showLocationPanel(self):
        self._placeLocationsPanel()
        self.locationsPanel.show()
        self.locationsPanel.activateWindow()
    
    def split(self, model, capacity, grouping_level):
        def do(j):
            self.board.Split(model, capacity, grouping_level, j)
        
        self._startJob(JOB_SPLIT, do)
    
    def toggleLocation(self, location):
        self.board.ToggleLocation(location)
        self.emit(SIGNAL('locationsChanged()'))
        self.emit(SIGNAL('boardChanged()'))
    
    def undoSplit(self):
        self.board.Unsplit()
        self.emit(SIGNAL('boardChanged()'))
    
    def updateCollection(self):
        def do(j):
            self.collection.update_volumes(j)
        
        self._startJob(JOB_UPDATE, do)
    
    def updateLocation(self, location):
        def do(j):
            location.update(None, j)
        
        self._startJob(JOB_UPDATE, do)
    
    #--- Events
    def applicationFinishedLaunching(self):
        self.reg = Registration(self)
        self.set_registration(self.prefs.registration_code, self.prefs.registration_email)
        if not self.registered and self.unpaid_hours >= 1:
            self.reg.show_nag()
        self.mainWindow.show()
        self.showLocationPanel()
        self.showDetailsPanel()
        self.updateCollection()
    
    def jobFinished(self, jobid):
        if jobid in (JOB_UPDATE, JOB_ADD):
            self.emit(SIGNAL('locationsChanged()'))
        if jobid in (JOB_MASS_RENAME, JOB_SPLIT):
            self.emit(SIGNAL('boardChanged()'))
        if jobid in (JOB_MATERIALIZE_RENAME, JOB_MATERIALIZE_MOVE):
            self.board.Empty()
            self.emit(SIGNAL('locationsChanged()'))
            self.emit(SIGNAL('boardChanged()'))
            self.emit(SIGNAL('ignoreBoxChanged()'))