2009-06-01 09:55:11 +00:00
|
|
|
# Created By: Virgil Dupras
|
|
|
|
# Created On: 2009-04-25
|
2011-04-12 08:04:01 +00:00
|
|
|
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
|
2009-08-05 08:59:46 +00:00
|
|
|
#
|
2010-09-30 10:17:41 +00:00
|
|
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
2009-08-05 08:59:46 +00:00
|
|
|
# which should be included with this package. The terms are also available at
|
2010-09-30 10:17:41 +00:00
|
|
|
# http://www.hardcoded.net/licenses/bsd_license
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-01-23 15:09:47 +00:00
|
|
|
import sys
|
2009-06-01 09:55:11 +00:00
|
|
|
import logging
|
2009-10-13 14:59:19 +00:00
|
|
|
import os
|
2009-06-01 09:55:11 +00:00
|
|
|
import os.path as op
|
2011-02-18 10:10:11 +00:00
|
|
|
import io
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, QProcess, SIGNAL, pyqtSignal
|
|
|
|
from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox, QApplication
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2010-11-20 11:42:15 +00:00
|
|
|
from jobprogress import job
|
|
|
|
from jobprogress.qt import Progress
|
2011-11-01 19:44:18 +00:00
|
|
|
from hscommon.trans import trget
|
2011-09-22 13:32:09 +00:00
|
|
|
from hscommon.plat import ISLINUX
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-09-21 17:55:26 +00:00
|
|
|
from core.app import JobType
|
2011-01-21 12:57:54 +00:00
|
|
|
|
2009-10-30 11:41:14 +00:00
|
|
|
from qtlib.about_box import AboutBox
|
2011-01-15 15:29:35 +00:00
|
|
|
from qtlib.recent import Recent
|
2009-10-30 11:41:14 +00:00
|
|
|
from qtlib.reg import Registration
|
2012-03-04 16:14:59 +00:00
|
|
|
from qtlib.util import createActions
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2009-09-27 08:44:06 +00:00
|
|
|
from . import platform
|
2011-01-15 15:29:35 +00:00
|
|
|
from .result_window import ResultWindow
|
2009-09-27 08:44:06 +00:00
|
|
|
from .directories_dialog import DirectoriesDialog
|
2010-04-12 13:29:56 +00:00
|
|
|
from .problem_dialog import ProblemDialog
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-11-01 19:44:18 +00:00
|
|
|
tr = trget('ui')
|
|
|
|
|
2009-06-01 09:55:11 +00:00
|
|
|
JOBID2TITLE = {
|
2011-09-21 17:55:26 +00:00
|
|
|
JobType.Scan: tr("Scanning for duplicates"),
|
|
|
|
JobType.Load: tr("Loading"),
|
|
|
|
JobType.Move: tr("Moving"),
|
|
|
|
JobType.Copy: tr("Copying"),
|
|
|
|
JobType.Delete: tr("Sending files to the recycle bin"),
|
2009-06-01 09:55:11 +00:00
|
|
|
}
|
|
|
|
|
2011-02-18 10:10:11 +00:00
|
|
|
class SysWrapper(io.IOBase):
|
|
|
|
def write(self, s):
|
|
|
|
if s.strip(): # don't log empty stuff
|
|
|
|
logging.warning(s)
|
|
|
|
|
2011-09-20 19:06:29 +00:00
|
|
|
class DupeGuru(QObject):
|
2011-09-20 22:40:27 +00:00
|
|
|
MODELCLASS = None
|
2009-06-01 09:55:11 +00:00
|
|
|
LOGO_NAME = '<replace this>'
|
|
|
|
NAME = '<replace this>'
|
|
|
|
|
2011-11-28 15:27:17 +00:00
|
|
|
DETAILS_DIALOG_CLASS = None
|
|
|
|
RESULT_WINDOW_CLASS = ResultWindow
|
|
|
|
RESULT_MODEL_CLASS = None
|
|
|
|
PREFERENCES_CLASS = None
|
|
|
|
PREFERENCES_DIALOG_CLASS = None
|
|
|
|
|
2011-09-20 22:40:27 +00:00
|
|
|
def __init__(self):
|
2011-09-20 19:06:29 +00:00
|
|
|
QObject.__init__(self)
|
2010-08-11 14:39:06 +00:00
|
|
|
appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
|
2009-10-13 14:59:19 +00:00
|
|
|
if not op.exists(appdata):
|
|
|
|
os.makedirs(appdata)
|
2009-10-02 11:12:25 +00:00
|
|
|
# For basicConfig() to work, we have to be sure that no logging has taken place before this call.
|
2011-01-26 11:50:44 +00:00
|
|
|
logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING,
|
|
|
|
format='%(asctime)s - %(levelname)s - %(message)s')
|
2011-02-18 10:10:11 +00:00
|
|
|
if sys.stderr is None: # happens under a cx_freeze environment
|
|
|
|
sys.stderr = SysWrapper()
|
|
|
|
if sys.stdout is None:
|
|
|
|
sys.stdout = SysWrapper()
|
2011-11-28 15:27:17 +00:00
|
|
|
self.prefs = self.PREFERENCES_CLASS()
|
2011-01-24 10:30:45 +00:00
|
|
|
self.prefs.load()
|
2011-09-20 22:40:27 +00:00
|
|
|
self.model = self.MODELCLASS(view=self, appdata=appdata)
|
2009-06-01 09:55:11 +00:00
|
|
|
self._setup()
|
2011-09-23 14:29:25 +00:00
|
|
|
self.prefsChanged.emit(self.prefs)
|
2009-06-01 09:55:11 +00:00
|
|
|
|
|
|
|
#--- Private
|
|
|
|
def _setup(self):
|
2011-01-15 15:29:35 +00:00
|
|
|
self._setupActions()
|
2009-06-01 09:55:11 +00:00
|
|
|
self._update_options()
|
2011-01-17 16:15:16 +00:00
|
|
|
self.recentResults = Recent(self, 'recentResults')
|
2011-09-20 19:06:29 +00:00
|
|
|
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
2011-11-28 15:27:17 +00:00
|
|
|
self.resultWindow = self.RESULT_WINDOW_CLASS(self)
|
2011-01-15 15:29:35 +00:00
|
|
|
self._progress = Progress(self.resultWindow)
|
|
|
|
self.directories_dialog = DirectoriesDialog(self.resultWindow, self)
|
2011-11-28 15:27:17 +00:00
|
|
|
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
|
2012-01-16 14:29:57 +00:00
|
|
|
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
|
2011-11-28 15:27:17 +00:00
|
|
|
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
|
2011-01-15 15:29:35 +00:00
|
|
|
self.about_box = AboutBox(self.resultWindow, self)
|
2011-09-24 20:21:20 +00:00
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
self.directories_dialog.show()
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.load()
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-09-21 17:42:54 +00:00
|
|
|
# The timer scheme is because if the nag is not shown before the application is
|
|
|
|
# completely initialized, the nag will be shown before the app shows up in the task bar
|
|
|
|
# In some circumstances, the nag is hidden by other window, which may make the user think
|
|
|
|
# that the application haven't launched.
|
|
|
|
QTimer.singleShot(0, self.finishedLaunching)
|
2009-06-01 09:55:11 +00:00
|
|
|
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
|
|
|
|
self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished)
|
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
def _setupActions(self):
|
|
|
|
# Setup actions that are common to both the directory dialog and the results window.
|
|
|
|
# (name, shortcut, icon, desc, func)
|
|
|
|
ACTIONS = [
|
2011-01-21 12:57:54 +00:00
|
|
|
('actionQuit', 'Ctrl+Q', '', tr("Quit"), self.quitTriggered),
|
|
|
|
('actionPreferences', 'Ctrl+P', '', tr("Preferences"), self.preferencesTriggered),
|
|
|
|
('actionShowHelp', 'F1', '', tr("dupeGuru Help"), self.showHelpTriggered),
|
|
|
|
('actionAbout', '', '', tr("About dupeGuru"), self.showAboutBoxTriggered),
|
|
|
|
('actionRegister', '', '', tr("Register dupeGuru"), self.registerTriggered),
|
|
|
|
('actionCheckForUpdate', '', '', tr("Check for Update"), self.checkForUpdateTriggered),
|
|
|
|
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
|
2011-01-15 15:29:35 +00:00
|
|
|
]
|
|
|
|
createActions(ACTIONS, self)
|
2011-01-23 15:09:47 +00:00
|
|
|
|
2011-09-22 13:32:09 +00:00
|
|
|
if ISLINUX:
|
2011-01-23 15:09:47 +00:00
|
|
|
self.actionCheckForUpdate.setVisible(False) # This only works on Windows
|
2011-01-15 15:29:35 +00:00
|
|
|
|
2009-06-01 09:55:11 +00:00
|
|
|
def _update_options(self):
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
|
|
|
|
self.model.options['escape_filter_regexp'] = self.prefs.use_regexp
|
|
|
|
self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
|
|
|
|
self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-09-20 19:06:29 +00:00
|
|
|
#--- Public
|
2010-02-06 11:12:20 +00:00
|
|
|
def add_selected_to_ignore_list(self):
|
2011-09-20 19:06:29 +00:00
|
|
|
dupes = self.model.without_ref(self.model.selected_dupes)
|
2010-02-06 11:12:20 +00:00
|
|
|
if not dupes:
|
|
|
|
return
|
2011-01-21 12:57:54 +00:00
|
|
|
title = tr("Add to Ignore List")
|
2011-11-04 17:10:11 +00:00
|
|
|
msg = tr("All selected %d matches are going to be ignored in all subsequent scans. Continue?") % len(dupes)
|
2011-01-15 15:29:35 +00:00
|
|
|
if self.confirm(title, msg):
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.add_selected_to_ignore_list(self)
|
2009-06-01 09:55:11 +00:00
|
|
|
|
|
|
|
def copy_or_move_marked(self, copy):
|
2011-01-21 12:57:54 +00:00
|
|
|
opname = tr("copy") if copy else tr("move")
|
2011-11-04 18:37:07 +00:00
|
|
|
title = tr("Select a directory to {} marked files to").format(opname)
|
2009-06-01 09:55:11 +00:00
|
|
|
flags = QFileDialog.ShowDirsOnly
|
2011-01-15 15:29:35 +00:00
|
|
|
destination = str(QFileDialog.getExistingDirectory(self.resultWindow, title, '', flags))
|
2009-06-01 09:55:11 +00:00
|
|
|
if not destination:
|
|
|
|
return
|
|
|
|
recreate_path = self.prefs.destination_type
|
2011-10-02 14:27:40 +00:00
|
|
|
self.model.copy_or_move_marked(copy, destination, recreate_path)
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2010-02-06 11:44:21 +00:00
|
|
|
def remove_selected(self):
|
2011-09-20 19:06:29 +00:00
|
|
|
dupes = self.model.without_ref(self.model.selected_dupes)
|
2010-02-06 11:44:21 +00:00
|
|
|
if not dupes:
|
|
|
|
return
|
2011-01-21 12:57:54 +00:00
|
|
|
title = tr("Remove duplicates")
|
2011-11-04 17:10:11 +00:00
|
|
|
msg = tr("You are about to remove %d files from results. Continue?") % len(dupes)
|
2011-01-15 15:29:35 +00:00
|
|
|
if self.confirm(title, msg):
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.remove_selected(self)
|
2010-02-06 11:44:21 +00:00
|
|
|
|
2010-02-06 11:12:20 +00:00
|
|
|
def askForRegCode(self):
|
2011-09-21 17:42:54 +00:00
|
|
|
reg = Registration(self.model)
|
|
|
|
reg.ask_for_code()
|
2010-02-06 11:12:20 +00:00
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
|
|
|
active = QApplication.activeWindow()
|
|
|
|
buttons = QMessageBox.Yes | QMessageBox.No
|
|
|
|
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
|
|
|
return answer == QMessageBox.Yes
|
|
|
|
|
2010-04-13 08:02:09 +00:00
|
|
|
def invokeCustomCommand(self):
|
|
|
|
cmd = self.prefs.custom_command
|
|
|
|
if cmd:
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.invoke_command(cmd)
|
2010-04-13 08:02:09 +00:00
|
|
|
else:
|
2011-11-04 17:10:11 +00:00
|
|
|
msg = tr("You have no custom command set up. Set it up in your preferences.")
|
2011-01-21 12:57:54 +00:00
|
|
|
QMessageBox.warning(self.resultWindow, tr("Custom Command"), msg)
|
2009-06-01 09:55:11 +00:00
|
|
|
|
|
|
|
def show_details(self):
|
|
|
|
self.details_dialog.show()
|
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
def showResultsWindow(self):
|
|
|
|
self.resultWindow.show()
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2010-08-15 10:27:15 +00:00
|
|
|
#--- Signals
|
|
|
|
willSavePrefs = pyqtSignal()
|
2011-09-23 14:29:25 +00:00
|
|
|
prefsChanged = pyqtSignal(object)
|
2010-08-15 10:27:15 +00:00
|
|
|
|
2009-06-01 09:55:11 +00:00
|
|
|
#--- Events
|
2011-09-21 17:42:54 +00:00
|
|
|
def finishedLaunching(self):
|
2011-09-24 20:21:20 +00:00
|
|
|
self.model.initial_registration_setup()
|
2011-12-07 17:04:02 +00:00
|
|
|
if sys.getfilesystemencoding() == 'ascii':
|
|
|
|
# No need to localize this, it's a debugging message.
|
|
|
|
msg = "Something is wrong with the way your system locale is set. If the files you're "\
|
|
|
|
"scanning have accented letters, you'll probably get a crash. It is advised that "\
|
|
|
|
"you set your system locale properly."
|
|
|
|
QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg)
|
2011-09-21 17:42:54 +00:00
|
|
|
|
2009-06-01 09:55:11 +00:00
|
|
|
def application_will_terminate(self):
|
2010-08-15 10:27:15 +00:00
|
|
|
self.willSavePrefs.emit()
|
|
|
|
self.prefs.save()
|
2011-09-20 19:06:29 +00:00
|
|
|
self.model.save()
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-01-15 15:29:35 +00:00
|
|
|
def checkForUpdateTriggered(self):
|
|
|
|
QProcess.execute('updater.exe', ['/checknow'])
|
|
|
|
|
2009-06-01 09:55:11 +00:00
|
|
|
def job_finished(self, jobid):
|
2012-03-05 19:09:42 +00:00
|
|
|
result = self.model._job_completed(jobid, self._progress.last_error)
|
|
|
|
if not result:
|
|
|
|
self._progress.reraise_if_error()
|
2011-09-21 17:55:26 +00:00
|
|
|
if jobid in {JobType.Move, JobType.Copy, JobType.Delete}:
|
2011-09-20 19:06:29 +00:00
|
|
|
if self.model.results.problems:
|
2010-04-12 13:29:56 +00:00
|
|
|
self.problemDialog.show()
|
|
|
|
else:
|
2011-11-04 18:37:07 +00:00
|
|
|
msg = tr("All files were processed successfully.")
|
2011-01-21 12:57:54 +00:00
|
|
|
QMessageBox.information(self.resultWindow, tr("Operation Complete"), msg)
|
2011-09-21 17:55:26 +00:00
|
|
|
elif jobid == JobType.Scan:
|
2011-09-20 19:06:29 +00:00
|
|
|
if not self.model.results.groups:
|
2011-01-21 12:57:54 +00:00
|
|
|
title = tr("Scan complete")
|
2011-11-04 17:10:11 +00:00
|
|
|
msg = tr("No duplicates found.")
|
2011-01-15 15:29:35 +00:00
|
|
|
QMessageBox.information(self.resultWindow, title, msg)
|
|
|
|
else:
|
|
|
|
self.showResultsWindow()
|
2011-09-21 17:55:26 +00:00
|
|
|
elif jobid == JobType.Load:
|
2011-01-15 15:29:35 +00:00
|
|
|
self.showResultsWindow()
|
|
|
|
|
|
|
|
def openDebugLogTriggered(self):
|
2011-09-20 19:06:29 +00:00
|
|
|
debugLogPath = op.join(self.model.appdata, 'debug.log')
|
|
|
|
self.open_path(debugLogPath)
|
2011-01-15 15:29:35 +00:00
|
|
|
|
|
|
|
def preferencesTriggered(self):
|
|
|
|
self.preferences_dialog.load()
|
2011-09-20 19:06:29 +00:00
|
|
|
result = self.preferences_dialog.exec()
|
2011-01-15 15:29:35 +00:00
|
|
|
if result == QDialog.Accepted:
|
|
|
|
self.preferences_dialog.save()
|
|
|
|
self.prefs.save()
|
|
|
|
self._update_options()
|
2011-09-23 14:29:25 +00:00
|
|
|
self.prefsChanged.emit(self.prefs)
|
2011-01-15 15:29:35 +00:00
|
|
|
|
|
|
|
def quitTriggered(self):
|
|
|
|
self.directories_dialog.close()
|
|
|
|
|
|
|
|
def registerTriggered(self):
|
2011-09-21 17:42:54 +00:00
|
|
|
reg = Registration(self.model)
|
|
|
|
reg.ask_for_code()
|
2011-01-15 15:29:35 +00:00
|
|
|
|
|
|
|
def showAboutBoxTriggered(self):
|
|
|
|
self.about_box.show()
|
|
|
|
|
|
|
|
def showHelpTriggered(self):
|
2011-11-30 16:06:08 +00:00
|
|
|
base_path = platform.HELP_PATH
|
2011-01-15 15:29:35 +00:00
|
|
|
url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
|
|
|
|
QDesktopServices.openUrl(url)
|
2009-06-01 09:55:11 +00:00
|
|
|
|
2011-09-20 19:06:29 +00:00
|
|
|
#--- model --> view
|
|
|
|
@staticmethod
|
|
|
|
def open_path(path):
|
|
|
|
url = QUrl.fromLocalFile(str(path))
|
|
|
|
QDesktopServices.openUrl(url)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def reveal_path(path):
|
|
|
|
DupeGuru.open_path(path[:-1])
|
|
|
|
|
2011-09-20 22:40:27 +00:00
|
|
|
def start_job(self, jobid, func, args=()):
|
2011-09-20 19:06:29 +00:00
|
|
|
title = JOBID2TITLE[jobid]
|
|
|
|
try:
|
|
|
|
j = self._progress.create_job()
|
2011-09-20 22:40:27 +00:00
|
|
|
args = (j, ) + tuple(args)
|
2011-09-20 19:06:29 +00:00
|
|
|
self._progress.run(jobid, title, func, args=args)
|
|
|
|
except job.JobInProgressError:
|
2011-11-04 17:10:11 +00:00
|
|
|
msg = tr("A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.")
|
2011-09-20 19:06:29 +00:00
|
|
|
QMessageBox.information(self.resultWindow, 'Action in progress', msg)
|
|
|
|
|
|
|
|
def get_default(self, key):
|
|
|
|
return self.prefs.get_value(key)
|
|
|
|
|
|
|
|
def set_default(self, key, value):
|
|
|
|
self.prefs.set_value(key, value)
|
|
|
|
|
2011-09-24 20:21:20 +00:00
|
|
|
def setup_as_registered(self):
|
|
|
|
self.actionRegister.setVisible(False)
|
|
|
|
self.about_box.registerButton.hide()
|
|
|
|
self.about_box.registeredEmailLabel.setText(self.model.registration_email)
|
|
|
|
|
2011-09-26 15:54:17 +00:00
|
|
|
def show_fairware_nag(self, prompt):
|
2011-09-24 20:21:20 +00:00
|
|
|
reg = Registration(self.model)
|
2011-09-26 15:54:17 +00:00
|
|
|
reg.show_fairware_nag(prompt)
|
|
|
|
|
|
|
|
def show_demo_nag(self, prompt):
|
|
|
|
reg = Registration(self.model)
|
|
|
|
reg.show_demo_nag(prompt)
|
2011-09-24 20:21:20 +00:00
|
|
|
|
2011-09-22 14:35:17 +00:00
|
|
|
def show_message(self, msg):
|
|
|
|
window = QApplication.activeWindow()
|
|
|
|
QMessageBox.information(window, '', msg)
|
|
|
|
|
2011-09-26 15:54:17 +00:00
|
|
|
def open_url(self, url):
|
2011-09-29 19:01:37 +00:00
|
|
|
url = QUrl(url)
|
|
|
|
QDesktopServices.openUrl(url)
|
2011-09-26 15:54:17 +00:00
|
|
|
|