[#194 state:fixed] Added the "Replace with symlink" deletion option.

This commit is contained in:
Virgil Dupras 2012-08-01 12:36:23 -04:00
parent 5247ac8abd
commit 5a5a74d0e1
9 changed files with 71 additions and 53 deletions

View File

@ -15,12 +15,14 @@ http://www.hardcoded.net/licenses/bsd_license
PyDeletionOptions *model;
NSTextField *messageTextField;
NSButton *hardlinkButton;
NSButton *linkButton;
NSMatrix *linkTypeRadio;
NSButton *directButton;
}
@property (readwrite, retain) NSTextField *messageTextField;
@property (readwrite, retain) NSButton *hardlinkButton;
@property (readwrite, retain) NSButton *linkButton;
@property (readwrite, retain) NSMatrix *linkTypeRadio;
@property (readwrite, retain) NSButton *directButton;
- (id)initWithPyRef:(PyObject *)aPyRef;

View File

@ -13,7 +13,8 @@ http://www.hardcoded.net/licenses/bsd_license
@implementation DeletionOptions
@synthesize messageTextField;
@synthesize hardlinkButton;
@synthesize linkButton;
@synthesize linkTypeRadio;
@synthesize directButton;
- (id)initWithPyRef:(PyObject *)aPyRef
@ -33,7 +34,8 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)updateOptions
{
[model setHardlink:[hardlinkButton state] == NSOnState];
[model setLinkDeleted:[linkButton state] == NSOnState];
[model setUseHardlinks:[linkTypeRadio selectedColumn] == 1];
[model setDirect:[directButton state] == NSOnState];
}
@ -55,8 +57,9 @@ http://www.hardcoded.net/licenses/bsd_license
- (BOOL)show
{
[hardlinkButton setState:NSOffState];
[linkButton setState:NSOffState];
[directButton setState:NSOffState];
[linkTypeRadio selectCellAtRow:0 column:0];
NSInteger r = [NSApp runModalForWindow:[self window]];
[[self window] close];
return r == NSOKButton;

View File

@ -1,47 +1,49 @@
ownerclass = 'DeletionOptions'
ownerimport = 'DeletionOptions.h'
result = Window(450, 215, "Deletion Options")
result = Window(450, 240, "Deletion Options")
messageLabel = Label(result, "")
hardlinkCheckbox = Checkbox(result, "Hardlink deleted files")
hardlinkLabel = Label(result, "After having deleted a duplicate, place a hardlink targeting the "
linkCheckbox = Checkbox(result, "Link deleted files")
linkLabel = Label(result, "After having deleted a duplicate, place a link targeting the "
"reference file to replace the deleted file.")
linkTypeChoice = RadioButtons(result, ["Symlink", "Hardlink"], columns=2)
directCheckbox = Checkbox(result, "Directly delete files")
directLabel = Label(result, "Instead of sending files to trash, delete them directly. This option "
"is usually used as a workaround when the normal deletion method doesn't work.")
proceedButton = Button(result, "Proceed")
cancelButton = Button(result, "Cancel")
owner.hardlinkButton = hardlinkCheckbox
owner.linkButton = linkCheckbox
owner.linkTypeRadio = linkTypeChoice
owner.directButton = directCheckbox
owner.messageTextField = messageLabel
result.canMinimize = False
result.canResize = False
hardlinkLabel.controlSize = const.NSSmallControlSize
directLabel.controlSize = const.NSSmallControlSize
linkLabel.controlSize = ControlSize.Small
directLabel.controlSize = ControlSize.Small
linkTypeChoice.controlSize = ControlSize.Small
proceedButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e'
hardlinkCheckbox.action = directCheckbox.action = Action(owner, 'updateOptions')
linkCheckbox.action = directCheckbox.action = linkTypeChoice.action = Action(owner, 'updateOptions')
proceedButton.action = Action(owner, 'proceed')
cancelButton.action = Action(owner, 'cancel')
hardlinkLabel.height *= 2 # 2 lines
linkLabel.height *= 2 # 2 lines
directLabel.height *= 3 # 3 lines
proceedButton.width = 92
cancelButton.width = 92
messageLabel.packToCorner(Pack.UpperLeft)
hardlinkCheckbox.packRelativeTo(messageLabel, Pack.Below)
hardlinkLabel.packRelativeTo(hardlinkCheckbox, Pack.Below)
directCheckbox.packRelativeTo(hardlinkLabel, Pack.Below)
directLabel.packRelativeTo(directCheckbox, Pack.Below)
for view in (messageLabel, hardlinkCheckbox, hardlinkLabel, directCheckbox, directLabel):
view.fill(Pack.Right)
proceedButton.packToCorner(Pack.LowerRight)
cancelButton.packRelativeTo(proceedButton, Pack.Left)
mainLayout = VLayout([messageLabel, linkCheckbox, linkLabel, linkTypeChoice, directCheckbox,
directLabel])
mainLayout.packToCorner(Pack.UpperLeft)
mainLayout.fill(Pack.Right)
buttonLayout = HLayout([cancelButton, proceedButton])
buttonLayout.packToCorner(Pack.LowerRight)
# indent the labels under checkboxes a little bit to the right
for label in (hardlinkLabel, directLabel):
label.x += 20
label.width -= 20
for indentedView in (linkLabel, directLabel, linkTypeChoice):
indentedView.x += 20
indentedView.width -= 20
# We actually don't want the link choice radio buttons to take all the width, it looks weird.
linkTypeChoice.width = 170

View File

@ -151,12 +151,12 @@ class DupeGuruME(DupeGuruBase):
self.directories = Directories(fileclasses=self.directories.fileclasses)
self.dead_tracks = []
def _do_delete(self, j, replace_with_hardlinks, direct_deletion):
def _do_delete(self, j, *args):
# XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so
# we'll be able to replace "op" below with DupeGuruBase._do_delete.op.
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion)
return self._do_delete_dupe(dupe, *args)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
@ -169,10 +169,10 @@ class DupeGuruME(DupeGuruBase):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion):
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, ITunesSong):
dupe.remove_from_library()
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion)
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]):

View File

@ -176,10 +176,10 @@ class DupeGuruPE(DupeGuruBase):
DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories()
def _do_delete(self, j, replace_with_hardlinks, direct_deletion):
def _do_delete(self, j, *args):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion)
return self._do_delete_dupe(dupe, *args)
self.deleted_aperture_photos = False
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
@ -202,7 +202,7 @@ class DupeGuruPE(DupeGuruBase):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion):
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, IPhoto):
try:
a = app('iPhoto')
@ -244,7 +244,7 @@ class DupeGuruPE(DupeGuruBase):
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else:
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion)
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):

View File

@ -13,8 +13,11 @@ class DeletionOptionsView(GUIObjectView):
def show(self) -> bool: pass
class PyDeletionOptions(PyGUIObject):
def setHardlink_(self, hardlink: bool):
self.model.hardlink = hardlink
def setLinkDeleted_(self, link_deleted: bool):
self.model.link_deleted = link_deleted
def setUseHardlinks_(self, use_hardlinks: bool):
self.model.use_hardlinks = use_hardlinks
def setDirect_(self, direct: bool):
self.model.direct = direct

View File

@ -159,15 +159,15 @@ class DupeGuru(RegistrableApplication, Broadcaster):
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
return cmp_value(group.ref, key)
def _do_delete(self, j, replace_with_hardlinks, direct_deletion):
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion)
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion):
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
if not io.exists(dupe.path):
return
logging.debug("Sending '%s' to trash", dupe.path)
@ -179,10 +179,11 @@ class DupeGuru(RegistrableApplication, Broadcaster):
os.remove(str_path)
else:
send2trash(str_path) # Raises OSError when there's a problem
if replace_with_hardlinks:
if link_deleted:
group = self.results.get_group_of_duplicate(dupe)
ref = group.ref
os.link(str(ref.path), str_path)
linkfunc = os.link if use_hardlinks else os.symlink
linkfunc(str(ref.path), str_path)
self.clean_empty_dirs(dupe.path[:-1])
def _create_file(self, path):
@ -365,7 +366,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
return
if not self.deletion_options.show(self.results.mark_count):
return
args = [self.deletion_options.hardlink, self.deletion_options.direct]
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
self.deletion_options.direct]
logging.debug("Starting deletion job with args %r", args)
self.view.start_job(JobType.Delete, self._do_delete, args=args)

View File

@ -15,7 +15,8 @@ class DeletionOptions(GUIObject):
#
def show(self, mark_count):
self.hardlink = False
self.link_deleted = False
self.use_hardlinks = False
self.direct = False
msg = tr("You are sending {} file(s) to the Trash.").format(mark_count)
self.view.update_msg(msg)

View File

@ -11,6 +11,7 @@ from PyQt4.QtGui import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBo
from hscommon.plat import ISOSX, ISLINUX
from hscommon.trans import trget
from qtlib.radio_box import RadioBox
tr = trget('ui')
@ -27,20 +28,22 @@ class DeletionOptions(QDialog):
def _setupUi(self):
self.setWindowTitle(tr("Deletion Options"))
self.resize(400, 250)
self.resize(400, 270)
self.verticalLayout = QVBoxLayout(self)
self.msgLabel = QLabel()
self.verticalLayout.addWidget(self.msgLabel)
self.hardlinkCheckbox = QCheckBox(tr("Hardlink deleted files"))
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
if not (ISOSX or ISLINUX):
self.hardlinkCheckbox.setEnabled(False)
self.hardlinkCheckbox.setText(self.hardlinkCheckbox.text() + tr(" (Mac OS X or Linux only)"))
self.verticalLayout.addWidget(self.hardlinkCheckbox)
text = tr("After having deleted a duplicate, place a hardlink targeting the reference file "
self.linkCheckbox.setEnabled(False)
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (Mac OS X or Linux only)"))
self.verticalLayout.addWidget(self.linkCheckbox)
text = tr("After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file.")
self.hardlinkMessageLabel = QLabel(text)
self.hardlinkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.hardlinkMessageLabel)
self.linkMessageLabel = QLabel(text)
self.linkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.linkMessageLabel)
self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False)
self.verticalLayout.addWidget(self.linkTypeRadio)
self.directCheckbox = QCheckBox(tr("Directly delete files"))
self.verticalLayout.addWidget(self.directCheckbox)
text = tr("Instead of sending files to trash, delete them directly. This option is usually "
@ -58,10 +61,12 @@ class DeletionOptions(QDialog):
self.msgLabel.setText(msg)
def show(self):
self.hardlinkCheckbox.setChecked(self.model.hardlink)
self.linkCheckbox.setChecked(self.model.link_deleted)
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
self.directCheckbox.setChecked(self.model.direct)
result = self.exec()
self.model.hardlink = self.hardlinkCheckbox.isChecked()
self.model.link_deleted = self.linkCheckbox.isChecked()
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
self.model.direct = self.directCheckbox.isChecked()
return result == QDialog.Accepted