Esempio n. 1
0
 def testSaveLoad(self):
     prefs = Preferences('prefstest.conf')
     
     self.assertEqual(None, prefs.btDevice)
     self.assertEqual('bluetooth', prefs.connectionMethod)
     self.assertEqual('', prefs.customDevice)
     self.assertEqual(0, prefs.gammuIndex)
     
     prefs.btDevice = BluetoothDevice('00:00:00:00', 42, 'deviceName', 'serviceName')
     prefs.connectionMethod = 'connection'
     prefs.customDevice = '/dev/rfcomm0'
     prefs.gammuIndex = 2
             
     prefs.save()
     
     prefsLoaded = Preferences('prefstest.conf')
     prefsLoaded.load()
     
     self.assertNotEqual(None, prefsLoaded.btDevice, "Device has not been loaded")
     self.assertEqual('00:00:00:00', prefsLoaded.btDevice.address)
     self.assertEqual(42, prefsLoaded.btDevice.port)
     self.assertEqual('deviceName', prefsLoaded.btDevice.deviceName)
     self.assertEqual('serviceName', prefsLoaded.btDevice.serviceName)
     self.assertEqual('connection', prefsLoaded.connectionMethod)
     self.assertEqual('/dev/rfcomm0', prefsLoaded.customDevice)
     self.assertEqual(2, prefsLoaded.gammuIndex)
Esempio n. 2
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()'))
Esempio n. 3
0
class MainFrame(wx.Frame):
	def __init__(self, parent):
		wx.Frame.__init__(self, parent, size=(800,600))

		# main menue definition
		fileMenu = wx.Menu()
		menuNew = fileMenu.Append(wx.ID_NEW, '&Import', 'Put directory under checksum control')
		self.Bind(wx.EVT_MENU, self.OnNew, menuNew)
		menuOpen = fileMenu.Append(wx.ID_OPEN, '&Open', 'Open directory under checksum control')
		self.Bind(wx.EVT_MENU, self.OnOpen, menuOpen)
		fileMenu.AppendSeparator()
		menuPreferences = fileMenu.Append(wx.ID_PREFERENCES, '&Preferences\tCtrl+P', 'Show program\'s preferences')
		self.Bind(wx.EVT_MENU, self.OnPreferences, menuPreferences)
		fileMenu.AppendSeparator()
		menuExit = fileMenu.Append(wx.ID_EXIT, 'E&xit', 'Terminate Program')
		self.Bind(wx.EVT_MENU, self.OnExit, menuExit)
		actionMenu = wx.Menu()
		menuCheck = actionMenu.Append(wx.ID_FILE, '&Check\tCtrl+K', 'Check')
		self.Bind(wx.EVT_MENU, self.OnCheck, menuCheck)
		helpMenu = wx.Menu()
		menuAbout = helpMenu.Append(wx.ID_ABOUT, '&About', 'Information about this program')
		self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout)
		# assemble menu
		menuBar = wx.MenuBar()
		menuBar.Append(fileMenu, '&File')
		menuBar.Append(actionMenu, '&Action')
		menuBar.Append(helpMenu, '&Help')
		self.SetMenuBar(menuBar)

		# current directories and files
		self.UpdateRootDir(None)
		self.preferences = None

		# main window consists of address line and directory listing
		self.address = wx.TextCtrl(self, -1, style=wx.TE_READONLY)
		self.list = ListControlPanel(self)

		sizer = wx.BoxSizer(wx.VERTICAL)
		sizer.Add(self.address, 0, wx.ALL | wx.EXPAND, 5)
		sizer.Add(self.list, 1, wx.ALL | wx.EXPAND, 5)
		self.SetSizer(sizer)

		self.statusbar = self.CreateStatusBar()
		self.statusbar.SetFieldsCount(1)

		self.Show(True)

	def SetAddressLine(self, path=''):
		self.address.SetValue(path)

	def SetStatusBarText(self, text=''):
		self.statusbar.SetStatusText(text)

	def UpdateRootDir(self, rootDir):
		if rootDir is None:
			self.rootDir = None
			self.metaDir = None
			self.dbFile = None
			self.sigFile = None
			self.preferencesFile = None
			self.Title = ProgramName + ' ' + ProgramVersion
		else:
			self.rootDir = rootDir
			self.metaName = '.' + ProgramName
			self.metaDir = os.path.join(self.rootDir, self.metaName)
			self.dbFile = os.path.join(self.metaDir, u'base.sqlite3')
			self.sigFile = os.path.join(self.metaDir, u'base.signature')
			self.preferencesFile = os.path.join(self.metaDir, u'preferences.json')
			self.Title = ProgramName + ' ' + ProgramVersion + \
				' - ' + self.rootDir

	def OnNew(self, event):
		# get a valid path from user
		dirDialog = wx.DirDialog(self, "Choose a directory for import:", \
			style=wx.DD_DEFAULT_STYLE)
		if platform.system() == 'Windows':
			dirDialog.SetPath('D:\\Projects\\treeseal-example') # TESTING
		else:
			dirDialog.SetPath('/home/phil/Projects/treeseal-example') # TESTING

		if dirDialog.ShowModal() == wx.ID_OK:
			self.UpdateRootDir(dirDialog.GetPath())
		else:
			self.UpdateRootDir(None)
			return
		# check pre-conditions
		if os.path.exists(self.metaDir):
			dial = wx.MessageBox('Path "' + self.rootDir + '" already seems to be under checksum control.\n\nDo you still want to continue?', \
				'Warning', wx.YES_NO | wx.ICON_WARNING | wx.NO_DEFAULT)
			if not dial == wx.YES:
				self.UpdateRootDir(None)
				return

		# close eventually existing previous instance
		self.list.ClearInstance()
		self.SetStatusBarText()

		self.Import()

	def OnOpen(self, event):
		# get a valid path from user
		dirDialog = wx.DirDialog(self, "Choose a directory for open:", \
			style=wx.DD_DEFAULT_STYLE)
		if platform.system() == 'Windows':
			dirDialog.SetPath('D:\\Projects\\treeseal-example') # TESTING
		else:
			dirDialog.SetPath('/home/phil/Projects/treeseal-example') # TESTING

		if dirDialog.ShowModal() == wx.ID_OK:
			self.UpdateRootDir(dirDialog.GetPath())
		else:
			self.UpdateRootDir(None)
			return

		# check pre-conditions
		if not os.path.exists(self.metaDir):
			wx.MessageBox('Path "' + self.rootDir + '" is no valid root dir.', \
				'Error', wx.OK | wx.ICON_ERROR)
			self.UpdateRootDir(None)
			return
		if not os.path.exists(self.dbFile):
			wx.MessageBox('Cannot find database file "' + self.dbFile + '".', \
				'Error', wx.OK | wx.ICON_ERROR)
			self.UpdateRootDir(None)
			return
		if not os.path.exists(self.sigFile):
			dial = wx.MessageBox('Cannot find database signature file "' + self.sigFile + '".\n\nUse database without verification?', \
				'Warning', wx.YES_NO | wx.ICON_WARNING | wx.NO_DEFAULT)
			if not dial == wx.YES:
				self.UpdateRootDir(None)
				return
		else:
			cs = Checksum()
			cs.calculateForFile(self.dbFile)
			if not cs.isValidUsingSavedFile(self.sigFile):
				dial = wx.MessageBox('Database or database signature file have been corrupted.\n\nUse database without verification?', \
					'Warning', wx.YES_NO | wx.ICON_WARNING | wx.NO_DEFAULT)
				if not dial == wx.YES:
					self.UpdateRootDir(None)
					return

		# close eventually existing previous instance
		self.list.ClearInstance()
		self.SetStatusBarText()

		# load preferences or create default ones
		self.preferences = Preferences()
		if os.path.exists(self.preferencesFile):
			self.preferences.load(self.preferencesFile)
		else:
			self.preferences.save(self.preferencesFile)


	def OnPreferences(self, event):
		if self.preferences is None:
			wx.MessageBox('Import or Open directory before you can access its preferences.', \
				'Error', wx.OK | wx.ICON_ERROR)
			return
		preferencesDialog = PreferencesDialog(self, self.preferences)
		preferencesDialog.ShowModal()
		self.preferences.save(self.preferencesFile)

	def OnExit(self, event):
		self.Close(True)

	def Import(self):
		# do not care about previous content: reset meta directory and database files
		if os.path.exists(self.metaDir):
			if os.path.exists(self.dbFile):
				os.remove(self.dbFile)
			if os.path.exists(self.sigFile):
				os.remove(self.sigFile)
			if os.path.exists(self.preferencesFile):
				os.remove(self.preferencesFile)
		else:
			os.mkdir(self.metaDir)
		# if on windows platform, hide directory
		if platform.system() == 'Windows':
			os.system('attrib +h "' + self.metaDir + '"')
		self.preferences = Preferences()
		preferencesDialog = PreferencesDialog(self, self.preferences)
		preferencesDialog.ShowModal()
		self.preferences.save(self.preferencesFile)

		try:
			# create trees
			fstree = FilesystemTree(self.rootDir, self.preferences.includes, \
				[ os.path.sep + self.metaName ] + self.preferences.excludes)
			fstree.open()
			dbtree = DatabaseTree(self.dbFile, self.sigFile)
			dbtree.open()
		except MyException as e:
			e.showDialog('Importing ' + self.rootDir)
			return

		try:
			# create progress dialog
			progressDialog = FileProcessingProgressDialog(self, 'Importing ' + self.rootDir)
			progressDialog.Show()
			stats = fstree.getNodeStatistics()
			progressDialog.Init(stats.getNodeCount(), stats.getNodeSize())

			# execute task
			fstree.registerHandlers(progressDialog.SignalNewFile, \
				progressDialog.SignalBytesDone)
			fstree.copyTo(dbtree)
			dbtree.commit()
			fstree.unRegisterHandlers()
		except UserCancelledException:
			progressDialog.SignalFinished()
			return
		except MyException as e:
			progressDialog.Destroy()
			e.showDialog('Importing ' + self.rootDir)
			return

		# signal that we have returned from calculation, either
		# after it is done or after progressDialog signalled that the
		# user stopped the calculation using the cancel button
		progressDialog.SignalFinished()

		self.list.SetInstance(Instance(self.preferences, dbtree, None, None))
		fstree.close()
		self.list.readonly = True

		self.SetStatusBarText('Imported ' + str(stats))

	def OnCheck(self, event):
		# close eventually existing previous instance
		self.list.ClearInstance()
		self.SetStatusBarText()

		try:
			# create trees
			fstree = FilesystemTree(self.rootDir, self.preferences.includes, \
				[ os.path.sep + self.metaName ] + self.preferences.excludes)
			fstree.open()
			dbtree = DatabaseTree(self.dbFile, self.sigFile)
			dbtree.open()
			memtree = MemoryTree()
			memtree.open()
		except MyException as e:
			e.showDialog('Checking ' + self.rootDir)
			return

		try:
			# create progress dialog
			progressDialog = FileProcessingProgressDialog(self, 'Checking ' + self.rootDir)
			progressDialog.Show()
			stats = fstree.getNodeStatistics()
			progressDialog.Init(stats.getNodeCount(), stats.getNodeSize())

			# execute task
			fstree.registerHandlers(progressDialog.SignalNewFile, \
				progressDialog.SignalBytesDone)
			fstree.diff(dbtree, memtree)
			memtree.commit()
			fstree.unRegisterHandlers()
		except UserCancelledException:
			progressDialog.SignalFinished()
			return
		except MyException as e:
			progressDialog.Destroy()
			e.showDialog('Checking ' + self.rootDir)
			return

		# signal that we have returned from calculation, either
		# after it is done or after progressDialog signalled that the
		# user stopped the calcuation using the cancel button
		progressDialog.SignalFinished()

		self.list.SetInstance(Instance(self.preferences, memtree, dbtree, fstree))
		self.list.readonly = False

		self.SetStatusBarText('Checked ' + str(stats))

	def OnAbout(self, event):
		info = wx.AboutDialogInfo()
		#info.SetIcon(wx.Icon('hunter.png', wx.BITMAP_TYPE_PNG))
		info.SetName('treeseal')
		info.SetVersion('3.0')
		info.SetDescription('TreeSeal is a tool for checking the integrity ' + \
			'of a directory structure by keeping checksums and meta ' + \
			'information for each file in a separate database.')
		info.SetCopyright('(C) 2010 - 2013 Philipp Roebrock')
		info.SetWebSite('https://github.com/proebrock/treeseal')
		#info.SetLicence(licence)
		info.AddDeveloper('Philipp Roebrock')
		info.AddDocWriter('Philipp Roebrock')
		wx.AboutBox(info)
Esempio n. 4
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()'))