diff --git a/cocoa/base/DeletionOptions.h b/cocoa/base/DeletionOptions.h index 9b6a9279..8061037c 100644 --- a/cocoa/base/DeletionOptions.h +++ b/cocoa/base/DeletionOptions.h @@ -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; diff --git a/cocoa/base/DeletionOptions.m b/cocoa/base/DeletionOptions.m index 7b746c0c..a307e93e 100644 --- a/cocoa/base/DeletionOptions.m +++ b/cocoa/base/DeletionOptions.m @@ -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; diff --git a/cocoa/base/ui/deletion_options.py b/cocoa/base/ui/deletion_options.py index 2ccbb784..5e6aebda 100644 --- a/cocoa/base/ui/deletion_options.py +++ b/cocoa/base/ui/deletion_options.py @@ -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 diff --git a/cocoa/inter/app_me.py b/cocoa/inter/app_me.py index 98459cdb..fa27306a 100644 --- a/cocoa/inter/app_me.py +++ b/cocoa/inter/app_me.py @@ -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]): diff --git a/cocoa/inter/app_pe.py b/cocoa/inter/app_pe.py index 8b7a11b6..4a37f709 100644 --- a/cocoa/inter/app_pe.py +++ b/cocoa/inter/app_pe.py @@ -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]): diff --git a/cocoa/inter/deletion_options.py b/cocoa/inter/deletion_options.py index 517f7819..0899ba13 100644 --- a/cocoa/inter/deletion_options.py +++ b/cocoa/inter/deletion_options.py @@ -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 diff --git a/core/app.py b/core/app.py index 75625410..5b36b693 100644 --- a/core/app.py +++ b/core/app.py @@ -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) diff --git a/core/gui/deletion_options.py b/core/gui/deletion_options.py index a7009417..e3d1add3 100644 --- a/core/gui/deletion_options.py +++ b/core/gui/deletion_options.py @@ -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) diff --git a/qt/base/deletion_options.py b/qt/base/deletion_options.py index f86e6e64..2a211105 100644 --- a/qt/base/deletion_options.py +++ b/qt/base/deletion_options.py @@ -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