Initial commit

This commit is contained in:
Virgil Dupras 2017-03-11 20:18:27 -05:00
commit 08eac3844e
68 changed files with 4486 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
.DS_Store
__pycache__
*.so
*.mo
*.waf*
.lock-waf*
/build
/cocoa/build
/env
/cocoa/autogen
/locale
/run.py
/cocoa/*/Info.plist
/cocoa/*/build

9
.gitmodules vendored Normal file
View File

@ -0,0 +1,9 @@
[submodule "dupeguru"]
path = dupeguru
url = https://github.com/hsoft/dupeguru.git
[submodule "hscommon"]
path = hscommon
url = https://github.com/hsoft/hscommon.git
[submodule "cocoalib"]
path = cocoalib
url = https://github.com/hsoft/cocoalib.git

37
Makefile Normal file
View File

@ -0,0 +1,37 @@
PYTHON ?= python3
REQ_MINOR_VERSION = 4
all : | env build
@echo "Build complete! You can run dupeGuru with 'make run'"
# If you're installing into a path that is not going to be the final path prefix (such as a
# sandbox), set DESTDIR to that path.
# Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we
# use one of each file to act as a representative, a target, of these groups.
submodules_target = hscommon/__init__.py
reqs :
@ret=`${PYTHON} -c "import sys; print(int(sys.version_info[:2] >= (3, ${REQ_MINOR_VERSION})))"`; \
if [ $${ret} -ne 1 ]; then \
echo "Python 3.${REQ_MINOR_VERSION}+ required. Aborting."; \
exit 1; \
fi
@${PYTHON} -m venv -h > /dev/null || \
echo "Creation of our virtualenv failed. Something's wrong with your python install."
# Ensure that submodules are initialized
$(submodules_target) :
git submodule init
git submodule update
cd dupeguru; ln -sf ../hscommon .; ln -sf ../cocoalib .
env : | $(submodules_target) reqs
@echo "Creating our virtualenv"
${PYTHON} -m venv env
./env/bin/python -m pip install -r requirements.txt
build:
./env/bin/python build.py
.PHONY : reqs build all

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# dupeguru-cocoa
This is the Cocoa UI for [dupeGuru][dupeguru]. This code was previously directly in the main repo,
but since I'm not planning on supporting MacOS myself any longer, I'm splitting it out.
Also, to make the job easier on a would-be maintainer for the Cocoa UI of dupeGuru, I'm planning
on restoring the XCode/XIB version of the UI from the grave.
### OS X maintainer wanted
My Mac Mini is already a couple of years old and is likely to be my last Apple purchase. When it
dies, I will be unable maintain the OS X version of moneyGuru. I've already stopped paying for the
Mac Developer membership so I can't sign the apps anymore (in the "official way" I mean. The
download is still PGP signed) If you're a Mac developer and are interested in taking this task,
[don't hesitate to let me know][contrib-issue].
## How to build dupeGuru from source
### Prerequisites
* Python 3.4+ compiled in "framework mode".
* MacOS 10.10+ with XCode command line tools.
### make
You can build the app with `make`:
$ make
$ make run
### pyenv
[pyenv][pyenv] is a popular way to manage multiple python versions. However, be aware that dupeGuru
will not compile with a pyenv's python unless it's been built with `--enable-framework`. You can do
this with:
$ env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.4.3
[dupeguru]: https://github.com/hsoft/dupeguru
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
[pyenv]: https://github.com/yyuu/pyenv

319
build.py Normal file
View File

@ -0,0 +1,319 @@
# Copyright 2017 Virgil Dupras
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
import sys
sys.path.append('dupeguru')
import os
import os.path as op
from optparse import OptionParser
import shutil
import compileall
from setuptools import setup, Extension
from hscommon import sphinxgen
from hscommon.build import (
add_to_pythonpath, print_and_do, copy_packages, filereplace,
get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies
)
from hscommon import loc
from hscommon.plat import ISOSX
from hscommon.util import ensure_folder, delete_files_with_pattern
def parse_args():
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option(
'--clean', action='store_true', dest='clean',
help="Clean build folder before building"
)
parser.add_option(
'--doc', action='store_true', dest='doc',
help="Build only the help file"
)
parser.add_option(
'--dev', action='store_true', dest='dev', default=False,
help="If this flag is set, will configure for dev builds."
)
parser.add_option(
'--loc', action='store_true', dest='loc',
help="Build only localization"
)
parser.add_option(
'--cocoa-ext', action='store_true', dest='cocoa_ext',
help="Build only Cocoa extensions"
)
parser.add_option(
'--cocoa-compile', action='store_true', dest='cocoa_compile',
help="Build only Cocoa executable"
)
parser.add_option(
'--xibless', action='store_true', dest='xibless',
help="Build only xibless UIs"
)
parser.add_option(
'--updatepot', action='store_true', dest='updatepot',
help="Generate .pot files from source code."
)
parser.add_option(
'--mergepot', action='store_true', dest='mergepot',
help="Update all .po files based on .pot files."
)
parser.add_option(
'--normpo', action='store_true', dest='normpo',
help="Normalize all PO files (do this before commit)."
)
(options, args) = parser.parse_args()
return options
def cocoa_app():
app_path = 'build/dupeGuru.app'
return OSXAppStructure(app_path)
def build_xibless(dest='cocoa/autogen'):
import xibless
ensure_folder(dest)
FNPAIRS = [
('ignore_list_dialog.py', 'IgnoreListDialog_UI'),
('deletion_options.py', 'DeletionOptions_UI'),
('problem_dialog.py', 'ProblemDialog_UI'),
('directory_panel.py', 'DirectoryPanel_UI'),
('prioritize_dialog.py', 'PrioritizeDialog_UI'),
('result_window.py', 'ResultWindow_UI'),
('main_menu.py', 'MainMenu_UI'),
('details_panel.py', 'DetailsPanel_UI'),
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
]
for srcname, dstname in FNPAIRS:
xibless.generate(
op.join('cocoa', 'ui', srcname), op.join(dest, dstname),
localizationTable='Localizable'
)
for appmode in ('standard', 'music', 'picture'):
xibless.generate(
op.join('cocoa', 'ui', 'preferences_panel.py'),
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()),
localizationTable='Localizable',
args={'appmode': appmode},
)
def build_cocoa(dev):
print("Creating OS X app structure")
app = cocoa_app()
app_version = get_module_version('core')
cocoa_project_path = 'cocoa'
filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version)
app.create(op.join('build', 'Info.plist'))
print("Building localizations")
build_localizations()
print("Building xibless UIs")
build_cocoalib_xibless()
build_xibless()
print("Building Python extensions")
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
print("Building the cocoa layer")
copy_embeddable_python_dylib('build')
pydep_folder = op.join(app.resources, 'py')
if not op.exists(pydep_folder):
os.mkdir(pydep_folder)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
tocopy = [
'dupeguru/core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
]
copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build')
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
# to manually specify it.
extra_deps = ['multiprocessing']
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
del sys.path[0]
# Views are not referenced by python code, so they're not found by the collector.
copy_all('build/inter/*.so', op.join(pydep_folder, 'inter'))
if not dev:
# Important: Don't ever run delete_files_with_pattern('*.py') on dev builds because you'll
# be deleting all py files in symlinked folders.
compileall.compile_dir(pydep_folder, force=True, legacy=True)
delete_files_with_pattern(pydep_folder, '*.py')
delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF")
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru')
build_help()
print("Copying resources and frameworks")
image_path = 'cocoa/dupeguru.icns'
resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python')
print("Creating the run.py file")
tmpl = open('cocoa/run_template.py', 'rt').read()
run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents)
def build_help():
print("Generating Help")
current_path = op.abspath('dupeguru')
help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, '..', 'build', 'help')
changelog_path = op.join(current_path, 'help', 'changelog')
tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
confrepl = {'language': 'en'}
changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl')
conftmpl = op.join(current_path, 'help', 'conf.tmpl')
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
def build_localizations():
if not op.exists('locale'):
os.symlink('dupeguru/locale', 'locale')
loc.compile_all_po('locale')
app = cocoa_app()
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale')
if op.exists(locale_dest):
shutil.rmtree(locale_dest)
shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot'))
def build_updatepot():
print("Updating Cocoa strings file.")
build_cocoalib_xibless('cocoalib/autogen')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
build_xibless()
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj')
def build_mergepot():
print("Updating .po files using .pot files")
loc.merge_pots_into_pos(op.join('cocoalib', 'locale'))
def build_normpo():
loc.normalize_all_pos(op.join('cocoalib', 'locale'))
def build_cocoa_proxy_module():
print("Building Cocoa Proxy")
import objp.p2o
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
build_cocoa_ext(
"CocoaProxy", 'cocoalib/cocoa',
[
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
],
['AppKit', 'CoreServices'],
['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces():
print("Building Cocoa Bridging Interfaces")
import objp.o2p
import objp.p2o
add_to_pythonpath('cocoa')
add_to_pythonpath('cocoalib')
from cocoa.inter import (
PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
PyTextField, ProgressWindowView, PyProgressWindow
)
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
from inter.details_panel import PyDetailsPanel, DetailsPanelView
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
from inter.prioritize_dialog import PyPrioritizeDialog, PrioritizeDialogView
from inter.prioritize_list import PyPrioritizeList, PrioritizeListView
from inter.problem_dialog import PyProblemDialog
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuru, DupeGuruView
allclasses = [
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru,
PyTextField, PyProgressWindow
]
for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
allclasses = [
GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
ProgressWindowView, DupeGuruView
]
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
def build_pe_modules():
print("Building PE Modules")
exts = [
Extension(
"_block",
[op.join('dupeguru', 'core', 'pe', 'modules', 'block.c'), op.join('dupeguru', 'core', 'pe', 'modules', 'common.c')]
),
Extension(
"_cache",
[op.join('dupeguru', 'core', 'pe', 'modules', 'cache.c'), op.join('dupeguru', 'core', 'pe', 'modules', 'common.c')]
),
]
exts.append(Extension(
"_block_osx",
[op.join('dupeguru', 'core', 'pe', 'modules', 'block_osx.m'), op.join('dupeguru', 'core', 'pe', 'modules', 'common.c')],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
"-framework", "ApplicationServices",
]
))
setup(
script_args=['build_ext', '--inplace'],
ext_modules=exts,
)
move_all('_block*', op.join('dupeguru', 'core', 'pe'))
move_all('_cache*', op.join('dupeguru', 'core', 'pe'))
def build_normal(dev):
print("Building dupeGuru with UI cocoa")
add_to_pythonpath('.')
build_pe_modules()
build_cocoa(dev)
def main():
options = parse_args()
if options.dev:
print("Building in Dev mode")
if options.clean:
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
if op.exists(path):
shutil.rmtree(path)
if not op.exists('build'):
os.mkdir('build')
if options.doc:
build_help()
elif options.loc:
build_localizations()
elif options.updatepot:
build_updatepot()
elif options.mergepot:
build_mergepot()
elif options.normpo:
build_normpo()
elif options.cocoa_ext:
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
elif options.cocoa_compile:
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
cocoa_app().copy_executable('cocoa/build/dupeGuru')
elif options.xibless:
build_cocoalib_xibless()
build_xibless()
else:
build_normal(options.dev)
if __name__ == '__main__':
main()

79
cocoa/AppDelegate.h Normal file
View File

@ -0,0 +1,79 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyDupeGuru.h"
#import "ResultWindow.h"
#import "ResultTable.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "IgnoreListDialog.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSAboutBox.h"
#import "HSRecentFiles.h"
#import "HSProgressWindow.h"
@interface AppDelegate : NSObject <NSFileManagerDelegate>
{
NSMenu *recentResultsMenu;
NSMenu *columnsMenu;
PyDupeGuru *model;
ResultWindow *_resultWindow;
DirectoryPanel *_directoryPanel;
DetailsPanel *_detailsPanel;
IgnoreListDialog *_ignoreListDialog;
ProblemDialog *_problemDialog;
DeletionOptions *_deletionOptions;
HSProgressWindow *_progressWindow;
NSWindowController *_preferencesPanel;
HSAboutBox *_aboutBox;
HSRecentFiles *_recentResults;
}
@property (readwrite, retain) NSMenu *recentResultsMenu;
@property (readwrite, retain) NSMenu *columnsMenu;
/* Virtual */
+ (NSDictionary *)defaultPreferences;
- (PyDupeGuru *)model;
- (DetailsPanel *)createDetailsPanel;
- (void)setScanOptions;
/* Public */
- (void)finalizeInit;
- (ResultWindow *)resultWindow;
- (DirectoryPanel *)directoryPanel;
- (DetailsPanel *)detailsPanel;
- (HSRecentFiles *)recentResults;
- (NSInteger)getAppMode;
- (void)setAppMode:(NSInteger)appMode;
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void)applicationWillBecomeActive:(NSNotification *)aNotification;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
- (void)applicationWillTerminate:(NSNotification *)aNotification;
- (void)recentFileClicked:(NSString *)path;
/* Actions */
- (void)clearPictureCache;
- (void)loadResults;
- (void)openWebsite;
- (void)openHelp;
- (void)showAboutBox;
- (void)showDirectoryWindow;
- (void)showPreferencesPanel;
- (void)showResultWindow;
- (void)showIgnoreList;
- (void)startScanning;
/* model --> view */
- (void)showMessage:(NSString *)msg;
@end

394
cocoa/AppDelegate.m Normal file
View File

@ -0,0 +1,394 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "AppDelegate.h"
#import "ProgressController.h"
#import "HSPyUtil.h"
#import "Consts.h"
#import "Dialogs.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "DetailsPanelPicture.h"
#import "PreferencesPanelStandard_UI.h"
#import "PreferencesPanelMusic_UI.h"
#import "PreferencesPanelPicture_UI.h"
@implementation AppDelegate
@synthesize recentResultsMenu;
@synthesize columnsMenu;
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionary];
[d setObject:i2n(1) forKey:@"scanTypeStandard"];
[d setObject:i2n(3) forKey:@"scanTypeMusic"];
[d setObject:i2n(0) forKey:@"scanTypePicture"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
[d setObject:b2n(YES) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
[d setObject:b2n(NO) forKey:@"scanTagYear"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
[d setObject:i2n(1) forKey:@"recreatePathType"];
[d setObject:i2n(11) forKey:TableFontSize];
[d setObject:b2n(YES) forKey:@"mixFileKind"];
[d setObject:b2n(NO) forKey:@"useRegexpFilter"];
[d setObject:b2n(NO) forKey:@"ignoreHardlinkMatches"];
[d setObject:b2n(NO) forKey:@"removeEmptyFolders"];
[d setObject:b2n(NO) forKey:@"DebugMode"];
[d setObject:@"" forKey:@"CustomCommand"];
[d setObject:[NSArray array] forKey:@"recentDirectories"];
[d setObject:[NSArray array] forKey:@"columnsOrder"];
[d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"];
return d;
}
+ (void)initialize
{
HSVTAdd *vt = [[[HSVTAdd alloc] initWithValue:4] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtRowHeightOffset"];
NSDictionary *d = [self defaultPreferences];
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d];
[[NSUserDefaults standardUserDefaults] registerDefaults:d];
}
- (id)init
{
self = [super init];
model = [[PyDupeGuru alloc] init];
[model bindCallback:createCallback(@"DupeGuruView", self)];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
i = [NSMutableIndexSet indexSetWithIndex:4];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
return self;
}
- (void)finalizeInit
{
// We can only finalize initialization once the main menu has been created, which cannot happen
// before AppDelegate is created.
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self];
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
_problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
_deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_directoryPanel window]];
// Lazily loaded
_aboutBox = nil;
_preferencesPanel = nil;
_resultWindow = nil;
_detailsPanel = nil;
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
}
/* Virtual */
- (PyDupeGuru *)model
{
return model;
}
- (DetailsPanel *)createDetailsPanel
{
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
return [[DetailsPanelPicture alloc] initWithApp:model];
}
else {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *scanTypeOptionName;
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
scanTypeOptionName = @"scanTypePicture";
}
else if (appMode == AppModeMusic) {
scanTypeOptionName = @"scanTypeMusic";
}
else {
scanTypeOptionName = @"scanTypeStandard";
}
[model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[model setSizeThreshold:sizeThreshold];
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
}
/* Public */
- (ResultWindow *)resultWindow
{
return _resultWindow;
}
- (DirectoryPanel *)directoryPanel
{
return _directoryPanel;
}
- (DetailsPanel *)detailsPanel
{
return _detailsPanel;
}
- (HSRecentFiles *)recentResults
{
return _recentResults;
}
- (NSInteger)getAppMode
{
return [model getAppMode];
}
- (void)setAppMode:(NSInteger)appMode
{
[model setAppMode:appMode];
if (_preferencesPanel != nil) {
[_preferencesPanel release];
_preferencesPanel = nil;
}
}
/* Actions */
- (void)clearPictureCache
{
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[model clearPictureCache];
}
- (void)loadResults
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:NO];
[op setCanCreateDirectories:NO];
[op setAllowsMultipleSelection:NO];
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
if ([op runModal] == NSOKButton) {
NSString *filename = [[[op URLs] objectAtIndex:0] path];
[model loadResultsFrom:filename];
[[self recentResults] addFile:filename];
}
}
- (void)openWebsite
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]];
}
- (void)openHelp
{
NSBundle *b = [NSBundle mainBundle];
NSString *p = [b pathForResource:@"index" ofType:@"html" inDirectory:@"help"];
NSURL *u = [NSURL fileURLWithPath:p];
[[NSWorkspace sharedWorkspace] openURL:u];
}
- (void)showAboutBox
{
if (_aboutBox == nil) {
_aboutBox = [[HSAboutBox alloc] initWithApp:model];
}
[[_aboutBox window] makeKeyAndOrderFront:nil];
}
- (void)showDirectoryWindow
{
[[[self directoryPanel] window] makeKeyAndOrderFront:nil];
}
- (void)showPreferencesPanel
{
if (_preferencesPanel == nil) {
NSWindow *window;
NSInteger appMode = [model getAppMode];
if (appMode == AppModePicture) {
window = createPreferencesPanelPicture_UI(nil);
}
else if (appMode == AppModeMusic) {
window = createPreferencesPanelMusic_UI(nil);
}
else {
window = createPreferencesPanelStandard_UI(nil);
}
_preferencesPanel = [[NSWindowController alloc] initWithWindow:window];
}
[_preferencesPanel showWindow:nil];
}
- (void)showResultWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showIgnoreList
{
[model showIgnoreList];
}
- (void)startScanning
{
[[self directoryPanel] startDuplicateScan];
}
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[model loadSession];
}
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
{
if (![[[self directoryPanel] window] isVisible]) {
[[self directoryPanel] showWindow:NSApp];
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
if ([model resultsAreModified]) {
NSString *msg = NSLocalizedString(@"You have unsaved results, do you really want to quit?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) { // NO
return NSTerminateCancel;
}
}
return NSTerminateNow;
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSInteger sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"];
if (sc >= 10) {
sc = -1;
[model purgeIgnoreList];
}
sc++;
[model saveSession];
[ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"];
// NSApplication does not release nib instances objects, we must do it manually
// Well, it isn't needed because the memory is freed anyway (we are quitting the application
// But I need to release HSRecentFiles so it saves the user defaults
[_directoryPanel release];
[_recentResults release];
}
- (void)recentFileClicked:(NSString *)path
{
[model loadResultsFrom:path];
}
/* model --> view */
- (void)showMessage:(NSString *)msg
{
[Dialogs showMessage:msg];
}
- (BOOL)askYesNoWithPrompt:(NSString *)prompt
{
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
}
- (void)createResultsWindow
{
if (_resultWindow != nil) {
[_resultWindow release];
}
if (_detailsPanel != nil) {
[_detailsPanel release];
}
// Warning: creation order is important
// If the details panel is not created first and that there are some results in the model
// (happens if we load results), a dupe selection event triggers a details refresh in the
// core before we have the chance to initialize it, and then we crash.
_detailsPanel = [self createDetailsPanel];
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
}
- (void)showResultsWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showProblemDialog
{
[_problemDialog showWindow:self];
}
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:NO];
[op setCanChooseDirectories:YES];
[op setCanCreateDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setTitle:prompt];
if ([op runModal] == NSOKButton) {
return [[[op URLs] objectAtIndex:0] path];
}
else {
return nil;
}
}
- (NSString *)selectDestFileWithPrompt:(NSString *)prompt extension:(NSString *)extension
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
[sp setTitle:prompt];
if ([sp runModal] == NSOKButton) {
return [[sp URL] path];
}
else {
return nil;
}
}
@end

24
cocoa/Consts.h Normal file
View File

@ -0,0 +1,24 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#define JobStarted @"JobStarted"
#define JobInProgress @"JobInProgress"
#define TableFontSize @"TableFontSize"
#define jobLoad @"job_load"
#define jobScan @"job_scan"
#define jobCopy @"job_copy"
#define jobMove @"job_move"
#define jobDelete @"job_delete"
#define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType"
#define ImageLoadedNotification @"ImageLoadedNotification"
#define AppModeStandard 0
#define AppModeMusic 1
#define AppModePicture 2

33
cocoa/DeletionOptions.h Normal file
View File

@ -0,0 +1,33 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyDeletionOptions.h"
@interface DeletionOptions : NSWindowController
{
PyDeletionOptions *model;
NSTextField *messageTextField;
NSButton *linkButton;
NSMatrix *linkTypeRadio;
NSButton *directButton;
}
@property (readwrite, retain) NSTextField *messageTextField;
@property (readwrite, retain) NSButton *linkButton;
@property (readwrite, retain) NSMatrix *linkTypeRadio;
@property (readwrite, retain) NSButton *directButton;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)updateOptions;
- (void)proceed;
- (void)cancel;
@end

72
cocoa/DeletionOptions.m Normal file
View File

@ -0,0 +1,72 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DeletionOptions.h"
#import "DeletionOptions_UI.h"
#import "HSPyUtil.h"
@implementation DeletionOptions
@synthesize messageTextField;
@synthesize linkButton;
@synthesize linkTypeRadio;
@synthesize directButton;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
model = [[PyDeletionOptions alloc] initWithModel:aPyRef];
[self setWindow:createDeletionOptions_UI(self)];
[model bindCallback:createCallback(@"DeletionOptionsView", self)];
return self;
}
- (void)dealloc
{
[model release];
[super dealloc];
}
- (void)updateOptions
{
[model setLinkDeleted:[linkButton state] == NSOnState];
[model setUseHardlinks:[linkTypeRadio selectedColumn] == 1];
[model setDirect:[directButton state] == NSOnState];
}
- (void)proceed
{
[NSApp stopModalWithCode:NSOKButton];
}
- (void)cancel
{
[NSApp stopModalWithCode:NSCancelButton];
}
/* model --> view */
- (void)updateMsg:(NSString *)msg
{
[messageTextField setStringValue:msg];
}
- (BOOL)show
{
[linkButton setState:NSOffState];
[directButton setState:NSOffState];
[linkTypeRadio selectCellAtRow:0 column:0];
NSInteger r = [NSApp runModalForWindow:[self window]];
[[self window] close];
return r == NSOKButton;
}
- (void)setHardlinkOptionEnabled:(BOOL)enabled
{
[linkTypeRadio setEnabled:enabled];
}
@end

31
cocoa/DetailsPanel.h Normal file
View File

@ -0,0 +1,31 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import "PyDetailsPanel.h"
@interface DetailsPanel : NSWindowController <NSTableViewDataSource>
{
NSTableView *detailsTable;
PyDetailsPanel *model;
}
@property (readwrite, retain) NSTableView *detailsTable;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (PyDetailsPanel *)model;
- (NSWindow *)createWindow;
- (BOOL)isVisible;
- (void)toggleVisibility;
/* Python --> Cocoa */
- (void)refresh;
@end

81
cocoa/DetailsPanel.m Normal file
View File

@ -0,0 +1,81 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DetailsPanel.h"
#import "HSPyUtil.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
@synthesize detailsTable;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
[self setWindow:[self createWindow]];
model = [[PyDetailsPanel alloc] initWithModel:aPyRef];
[model bindCallback:createCallback(@"DetailsPanelView", self)];
return self;
}
- (void)dealloc
{
[model release];
[super dealloc];
}
- (PyDetailsPanel *)model
{
return (PyDetailsPanel *)model;
}
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
- (void)refreshDetails
{
[detailsTable reloadData];
}
- (BOOL)isVisible
{
return [[self window] isVisible];
}
- (void)toggleVisibility
{
if ([self isVisible]) {
[[self window] close];
}
else {
[self refreshDetails]; // selection might have changed since last time
[[self window] orderFront:nil];
}
}
/* NSTableView Delegate */
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self model] numberOfRows];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
return [[self model] valueForColumn:[column identifier] row:row];
}
/* Python --> Cocoa */
- (void)refresh
{
if ([[self window] isVisible]) {
[self refreshDetails];
}
}
@end

View File

@ -0,0 +1,32 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "DetailsPanel.h"
#import "PyDupeGuru.h"
@interface DetailsPanelPicture : DetailsPanel
{
NSImageView *dupeImage;
NSProgressIndicator *dupeProgressIndicator;
NSImageView *refImage;
NSProgressIndicator *refProgressIndicator;
PyDupeGuru *pyApp;
BOOL _needsRefresh;
NSString *_dupePath;
NSString *_refPath;
}
@property (readwrite, retain) NSImageView *dupeImage;
@property (readwrite, retain) NSProgressIndicator *dupeProgressIndicator;
@property (readwrite, retain) NSImageView *refImage;
@property (readwrite, retain) NSProgressIndicator *refProgressIndicator;
- (id)initWithApp:(PyDupeGuru *)aApp;
@end

View File

@ -0,0 +1,96 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "Utils.h"
#import "NSNotificationAdditions.h"
#import "NSImageAdditions.h"
#import "PyDupeGuru.h"
#import "DetailsPanelPicture.h"
#import "Consts.h"
#import "DetailsPanelPicture_UI.h"
@implementation DetailsPanelPicture
@synthesize dupeImage;
@synthesize dupeProgressIndicator;
@synthesize refImage;
@synthesize refProgressIndicator;
- (id)initWithApp:(PyDupeGuru *)aApp
{
self = [super initWithPyRef:[aApp detailsPanel]];
pyApp = aApp;
_needsRefresh = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self];
return self;
}
- (NSWindow *)createWindow
{
return createDetailsPanelPicture_UI(self);
}
- (void)loadImageAsync:(NSString *)imagePath
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSImage *image = [[NSImage alloc] initByReferencingFile:imagePath];
NSImage *thumbnail = [image imageByScalingProportionallyToSize:NSMakeSize(512,512)];
[image release];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setValue:imagePath forKey:@"imagePath"];
[params setValue:thumbnail forKey:@"image"];
[[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:ImageLoadedNotification object:self userInfo:params waitUntilDone:YES];
[pool release];
}
- (void)refreshDetails
{
if (!_needsRefresh)
return;
[detailsTable reloadData];
NSString *refPath = [pyApp getSelectedDupeRefPath];
if (_refPath != nil)
[_refPath autorelease];
_refPath = [refPath retain];
[NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:refPath];
NSString *dupePath = [pyApp getSelectedDupePath];
if (_dupePath != nil)
[_dupePath autorelease];
_dupePath = [dupePath retain];
if (![dupePath isEqual: refPath])
[NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:dupePath];
[refProgressIndicator startAnimation:nil];
[dupeProgressIndicator startAnimation:nil];
_needsRefresh = NO;
}
/* Notifications */
- (void)imageLoaded:(NSNotification *)aNotification
{
NSString *imagePath = [[aNotification userInfo] valueForKey:@"imagePath"];
NSImage *image = [[aNotification userInfo] valueForKey:@"image"];
if ([imagePath isEqual: _refPath])
{
[refImage setImage:image];
[refProgressIndicator stopAnimation:nil];
}
if ([imagePath isEqual: _dupePath])
{
[dupeImage setImage:image];
[dupeProgressIndicator stopAnimation:nil];
}
}
/* Python --> Cocoa */
- (void)refresh
{
_needsRefresh = YES;
[super refresh];
}
@end

21
cocoa/DirectoryOutline.h Normal file
View File

@ -0,0 +1,21 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import "HSOutline.h"
#import "PyDirectoryOutline.h"
#define DGAddedFoldersNotification @"DGAddedFoldersNotification"
@interface DirectoryOutline : HSOutline {}
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView;
- (PyDirectoryOutline *)model;
- (void)selectAll;
@end;

87
cocoa/DirectoryOutline.m Normal file
View File

@ -0,0 +1,87 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DirectoryOutline.h"
@implementation DirectoryOutline
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyDirectoryOutline class]
callbackClassName:@"DirectoryOutlineView" view:aOutlineView];
[[self view] registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
return self;
}
- (PyDirectoryOutline *)model
{
return (PyDirectoryOutline *)model;
}
/* Public */
- (void)selectAll
{
[[self model] selectAll];
}
/* Delegate */
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
if (sourceDragMask & NSDragOperationLink)
return NSDragOperationLink;
}
return NSDragOperationNone;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *foldernames = [pboard propertyListForType:NSFilenamesPboardType];
if (!(sourceDragMask & NSDragOperationLink))
return NO;
for (NSString *foldername in foldernames) {
[[self model] addDirectory:foldername];
}
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:foldernames forKey:@"foldernames"];
[[NSNotificationCenter defaultCenter] postNotificationName:DGAddedFoldersNotification
object:self userInfo:userInfo];
}
return YES;
}
- (void)outlineView:(NSOutlineView *)aOutlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
if ([cell isKindOfClass:[NSTextFieldCell class]]) {
NSTextFieldCell *textCell = cell;
NSIndexPath *path = item;
BOOL selected = [path isEqualTo:[[self view] selectedPath]];
if (selected) {
[textCell setTextColor:[NSColor blackColor]];
return;
}
NSInteger state = [self intProperty:@"state" valueAtPath:path];
if (state == 1) {
[textCell setTextColor:[NSColor blueColor]];
}
else if (state == 2) {
[textCell setTextColor:[NSColor redColor]];
}
else {
[textCell setTextColor:[NSColor blackColor]];
}
}
}
@end

57
cocoa/DirectoryPanel.h Normal file
View File

@ -0,0 +1,57 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSOutlineView.h"
#import "HSRecentFiles.h"
#import "DirectoryOutline.h"
#import "PyDupeGuru.h"
@class AppDelegate;
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
{
AppDelegate *_app;
PyDupeGuru *model;
HSRecentFiles *_recentDirectories;
DirectoryOutline *outline;
BOOL _alwaysShowPopUp;
NSSegmentedControl *appModeSelector;
NSPopUpButton *scanTypePopup;
NSPopUpButton *addButtonPopUp;
NSPopUpButton *loadRecentButtonPopUp;
HSOutlineView *outlineView;
NSButton *removeButton;
NSButton *loadResultsButton;
}
@property (readwrite, retain) NSSegmentedControl *appModeSelector;
@property (readwrite, retain) NSPopUpButton *scanTypePopup;
@property (readwrite, retain) NSPopUpButton *addButtonPopUp;
@property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp;
@property (readwrite, retain) HSOutlineView *outlineView;
@property (readwrite, retain) NSButton *removeButton;
@property (readwrite, retain) NSButton *loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp;
- (void)fillPopUpMenu;
- (void)fillScanTypeMenu;
- (void)adjustUIToLocalization;
- (void)askForDirectory;
- (void)popupAddDirectoryMenu:(id)sender;
- (void)popupLoadRecentMenu:(id)sender;
- (void)removeSelectedDirectory;
- (void)startDuplicateScan;
- (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText;
- (void)markAll;
@end

256
cocoa/DirectoryPanel.m Normal file
View File

@ -0,0 +1,256 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DirectoryPanel.h"
#import "DirectoryPanel_UI.h"
#import "Dialogs.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
@implementation DirectoryPanel
@synthesize appModeSelector;
@synthesize scanTypePopup;
@synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp;
@synthesize outlineView;
@synthesize removeButton;
@synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp
{
self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp;
model = [_app model];
[[self window] setTitle:[model appName]];
self.appModeSelector.selectedSegment = 0;
[self fillScanTypeMenu];
_alwaysShowPopUp = NO;
[self fillPopUpMenu];
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
[_recentDirectories setDelegate:self];
outline = [[DirectoryOutline alloc] initWithPyRef:[model directoryTree] outlineView:outlineView];
[self refreshRemoveButtonText];
[self adjustUIToLocalization];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:)
name:NSOutlineViewSelectionDidChangeNotification object:outlineView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(outlineAddedFolders:)
name:DGAddedFoldersNotification object:outline];
return self;
}
- (void)dealloc
{
[outline release];
[_recentDirectories release];
[super dealloc];
}
/* Private */
- (void)fillPopUpMenu
{
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m addItemWithTitle:NSLocalizedString(@"Add New Folder...", @"") action:@selector(askForDirectory) keyEquivalent:@""];
[mi setTarget:self];
[m addItem:[NSMenuItem separatorItem]];
}
- (void)fillScanTypeMenu
{
[[self scanTypePopup] unbind:@"selectedIndex"];
[[self scanTypePopup] removeAllItems];
[[self scanTypePopup] addItemsWithTitles:[[_app model] getScanOptions]];
NSString *keypath;
NSInteger appMode = [_app getAppMode];
if (appMode == AppModePicture) {
keypath = @"values.scanTypePicture";
}
else if (appMode == AppModeMusic) {
keypath = @"values.scanTypeMusic";
}
else {
keypath = @"values.scanTypeStandard";
}
[[self scanTypePopup] bind:@"selectedIndex" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:keypath options:nil];
}
- (void)adjustUIToLocalization
{
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
NSInteger loadResultsWidthDelta = 0;
if ([lang isEqual:@"ru"]) {
loadResultsWidthDelta = 50;
}
else if ([lang isEqual:@"uk"]) {
loadResultsWidthDelta = 70;
}
else if ([lang isEqual:@"hy"]) {
loadResultsWidthDelta = 30;
}
if (loadResultsWidthDelta) {
NSRect r = [loadResultsButton frame];
r.size.width += loadResultsWidthDelta;
r.origin.x -= loadResultsWidthDelta;
[loadResultsButton setFrame:r];
}
}
/* Actions */
- (void)askForDirectory
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:YES];
[op setAllowsMultipleSelection:YES];
[op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")];
[op setDelegate:self];
if ([op runModal] == NSOKButton) {
for (NSURL *directoryURL in [op URLs]) {
[self addDirectory:[directoryURL path]];
}
}
}
- (void)changeAppMode:(id)sender
{
NSInteger appMode;
NSUInteger selectedSegment = self.appModeSelector.selectedSegment;
if (selectedSegment == 2) {
appMode = AppModePicture;
}
else if (selectedSegment == 1) {
appMode = AppModeMusic;
}
else {
appMode = AppModeStandard;
}
[_app setAppMode:appMode];
[self fillScanTypeMenu];
}
- (void)popupAddDirectoryMenu:(id)sender
{
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
[self askForDirectory];
}
else {
[addButtonPopUp selectItem:nil];
[[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
}
- (void)popupLoadRecentMenu:(id)sender
{
if ([[[_app recentResults] filepaths] count] > 0) {
NSMenu *m = [loadRecentButtonPopUp menu];
while ([m numberOfItems] > 0) {
[m removeItemAtIndex:0];
}
NSMenuItem *mi = [m addItemWithTitle:NSLocalizedString(@"Load from file...", @"") action:@selector(loadResults) keyEquivalent:@""];
[mi setTarget:_app];
[m addItem:[NSMenuItem separatorItem]];
[[_app recentResults] fillMenu:m];
[loadRecentButtonPopUp selectItem:nil];
[[loadRecentButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
else {
[_app loadResults];
}
}
- (void)removeSelectedDirectory
{
[[self window] makeKeyAndOrderFront:nil];
[[outline model] removeSelectedDirectory];
[self refreshRemoveButtonText];
}
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[_app setScanOptions];
[model doScan];
}
/* Public */
- (void)addDirectory:(NSString *)directory
{
[model addDirectory:directory];
[_recentDirectories addFile:directory];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)refreshRemoveButtonText
{
if ([outlineView selectedRow] < 0) {
[removeButton setEnabled:NO];
return;
}
[removeButton setEnabled:YES];
NSIndexPath *path = [outline selectedIndexPath];
if (path != nil) {
NSInteger state = [outline intProperty:@"state" valueAtPath:path];
BOOL shouldDisplayArrow = ([path length] > 1) && (state == 2);
NSString *imgName = shouldDisplayArrow ? @"NSGoLeftTemplate" : @"NSRemoveTemplate";
[removeButton setImage:[NSImage imageNamed:imgName]];
}
}
- (void)markAll
{
/* markAll isn't very descriptive of what we do, but since we re-use the Mark All button from
the result window, we don't have much choice.
*/
[outline selectAll];
}
/* Delegate */
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path
{
BOOL isdir;
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isdir];
return isdir;
}
- (void)recentFileClicked:(NSString *)path
{
[self addDirectory:path];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Select All", @"")];
}
return YES;
}
/* Notifications */
- (void)directorySelectionChanged:(NSNotification *)aNotification
{
[self refreshRemoveButtonText];
}
- (void)outlineAddedFolders:(NSNotification *)aNotification
{
NSArray *foldernames = [[aNotification userInfo] objectForKey:@"foldernames"];
for (NSString *foldername in foldernames) {
[_recentDirectories addFile:foldername];
}
}
@end

25
cocoa/IgnoreListDialog.h Normal file
View File

@ -0,0 +1,25 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyIgnoreListDialog.h"
#import "HSTable.h"
@interface IgnoreListDialog : NSWindowController
{
PyIgnoreListDialog *model;
HSTable *ignoreListTable;
NSTableView *ignoreListTableView;
}
@property (readwrite, retain) PyIgnoreListDialog *model;
@property (readwrite, retain) NSTableView *ignoreListTableView;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)initializeColumns;
@end

51
cocoa/IgnoreListDialog.m Normal file
View File

@ -0,0 +1,51 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "IgnoreListDialog.h"
#import "IgnoreListDialog_UI.h"
#import "HSPyUtil.h"
@implementation IgnoreListDialog
@synthesize model;
@synthesize ignoreListTableView;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
self.model = [[[PyIgnoreListDialog alloc] initWithModel:aPyRef] autorelease];
[self.model bindCallback:createCallback(@"IgnoreListDialogView", self)];
[self setWindow:createIgnoreListDialog_UI(self)];
ignoreListTable = [[HSTable alloc] initWithPyRef:[model ignoreListTable] tableView:ignoreListTableView];
[self initializeColumns];
return self;
}
- (void)dealloc
{
[ignoreListTable release];
[super dealloc];
}
- (void)initializeColumns
{
HSColumnDef defs[] = {
{@"path1", 240, 40, 0, NO, nil},
{@"path2", 240, 40, 0, NO, nil},
nil
};
[[ignoreListTable columns] initializeColumns:defs];
[[ignoreListTable columns] setColumnsAsReadOnly];
}
/* model --> view */
- (void)show
{
[self showWindow:self];
}
@end

38
cocoa/InfoTemplate.plist Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>dupeGuru</string>
<key>CFBundleHelpBookFolder</key>
<string>dupeguru_help</string>
<key>CFBundleHelpBookName</key>
<string>dupeGuru Help</string>
<key>CFBundleIconFile</key>
<string>dupeguru</string>
<key>CFBundleIdentifier</key>
<string>com.hardcoded-software.dupeguru</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>dupeGuru</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleShortVersionString</key>
<string>{version}</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2016</string>
<key>SUFeedURL</key>
<string>https://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</dict>
</plist>

37
cocoa/PrioritizeDialog.h Normal file
View File

@ -0,0 +1,37 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyPrioritizeDialog.h"
#import "HSPopUpList.h"
#import "HSSelectableList.h"
#import "PrioritizeList.h"
#import "PyDupeGuru.h"
@interface PrioritizeDialog : NSWindowController
{
NSPopUpButton *categoryPopUpView;
NSTableView *criteriaTableView;
NSTableView *prioritizationTableView;
PyPrioritizeDialog *model;
HSPopUpList *categoryPopUp;
HSSelectableList *criteriaList;
PrioritizeList *prioritizationList;
}
@property (readwrite, retain) NSPopUpButton *categoryPopUpView;
@property (readwrite, retain) NSTableView *criteriaTableView;
@property (readwrite, retain) NSTableView *prioritizationTableView;
- (id)initWithApp:(PyDupeGuru *)aApp;
- (PyPrioritizeDialog *)model;
- (void)ok;
- (void)cancel;
@end;

56
cocoa/PrioritizeDialog.m Normal file
View File

@ -0,0 +1,56 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "PrioritizeDialog.h"
#import "PrioritizeDialog_UI.h"
#import "HSPyUtil.h"
@implementation PrioritizeDialog
@synthesize categoryPopUpView;
@synthesize criteriaTableView;
@synthesize prioritizationTableView;
- (id)initWithApp:(PyDupeGuru *)aApp
{
self = [super initWithWindowNibName:@"PrioritizeDialog"];
model = [[PyPrioritizeDialog alloc] initWithApp:[aApp pyRef]];
[self setWindow:createPrioritizeDialog_UI(self)];
categoryPopUp = [[HSPopUpList alloc] initWithPyRef:[[self model] categoryList] popupView:categoryPopUpView];
criteriaList = [[HSSelectableList alloc] initWithPyRef:[[self model] criteriaList] tableView:criteriaTableView];
prioritizationList = [[PrioritizeList alloc] initWithPyRef:[[self model] prioritizationList] tableView:prioritizationTableView];
[model bindCallback:createCallback(@"PrioritizeDialogView", self)];
return self;
}
- (void)dealloc
{
[categoryPopUp release];
[criteriaList release];
[prioritizationList release];
[model release];
[super dealloc];
}
- (PyPrioritizeDialog *)model
{
return (PyPrioritizeDialog *)model;
}
- (void)ok
{
[NSApp stopModal];
[self close];
}
- (void)cancel
{
[NSApp abortModal];
[self close];
}
@end

16
cocoa/PrioritizeList.h Normal file
View File

@ -0,0 +1,16 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSSelectableList.h"
#import "PyPrioritizeList.h"
@interface PrioritizeList : HSSelectableList {}
- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView;
- (PyPrioritizeList *)model;
@end

58
cocoa/PrioritizeList.m Normal file
View File

@ -0,0 +1,58 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "PrioritizeList.h"
#import "Utils.h"
#import "Consts.h"
@implementation PrioritizeList
- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyPrioritizeList class]
callbackClassName:@"PrioritizeListView" view:aTableView];
return self;
}
- (PyPrioritizeList *)model
{
return (PyPrioritizeList *)model;
}
- (void)setView:(NSTableView *)aTableView
{
[super setView:aTableView];
[[self view] registerForDraggedTypes:[NSArray arrayWithObject:DGPrioritizeIndexPasteboardType]];
}
- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard
{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
[pboard declareTypes:[NSArray arrayWithObject:DGPrioritizeIndexPasteboardType] owner:self];
[pboard setData:data forType:DGPrioritizeIndexPasteboardType];
return YES;
}
- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row
proposedDropOperation:(NSTableViewDropOperation)op
{
if (op == NSTableViewDropAbove) {
return NSDragOperationMove;
}
return NSDragOperationNone;
}
- (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id <NSDraggingInfo>)info
row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation
{
NSPasteboard* pboard = [info draggingPasteboard];
NSData* rowData = [pboard dataForType:DGPrioritizeIndexPasteboardType];
NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
[[self model] moveIndexes:[Utils indexSet2Array:rowIndexes] toIndex:row];
return YES;
}
@end

26
cocoa/ProblemDialog.h Normal file
View File

@ -0,0 +1,26 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyProblemDialog.h"
#import "HSTable.h"
@interface ProblemDialog : NSWindowController
{
PyProblemDialog *model;
HSTable *problemTable;
NSTableView *problemTableView;
}
@property (readwrite, retain) PyProblemDialog *model;
@property (readwrite, retain) NSTableView *problemTableView;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)initializeColumns;
@end

44
cocoa/ProblemDialog.m Normal file
View File

@ -0,0 +1,44 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ProblemDialog.h"
#import "ProblemDialog_UI.h"
#import "Utils.h"
@implementation ProblemDialog
@synthesize model;
@synthesize problemTableView;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
self.model = [[PyProblemDialog alloc] initWithModel:aPyRef];
[self setWindow:createProblemDialog_UI(self)];
problemTable = [[HSTable alloc] initWithPyRef:[self.model problemTable] tableView:problemTableView];
[self initializeColumns];
return self;
}
- (void)dealloc
{
[problemTable release];
[super dealloc];
}
- (void)initializeColumns
{
HSColumnDef defs[] = {
{@"path", 202, 40, 0, NO, nil},
{@"msg", 228, 40, 0, NO, nil},
nil
};
[[problemTable columns] initializeColumns:defs];
[[problemTable columns] setColumnsAsReadOnly];
}
@end

23
cocoa/ResultTable.h Normal file
View File

@ -0,0 +1,23 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#import "HSTable.h"
#import "PyResultTable.h"
@interface ResultTable : HSTable <QLPreviewPanelDataSource, QLPreviewPanelDelegate>
{
}
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView;
- (PyResultTable *)model;
- (BOOL)powerMarkerMode;
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode;
- (BOOL)deltaValuesMode;
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode;
@end;

180
cocoa/ResultTable.m Normal file
View File

@ -0,0 +1,180 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ResultTable.h"
#import "Dialogs.h"
#import "Utils.h"
#import "HSQuicklook.h"
@interface HSTable (private)
- (void)setPySelection;
- (void)setViewSelection;
@end
@implementation ResultTable
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyResultTable class] callbackClassName:@"ResultTableView" view:aTableView];
return self;
}
- (PyResultTable *)model
{
return (PyResultTable *)model;
}
/* Private */
- (void)updateQuicklookIfNeeded
{
if ([[QLPreviewPanel sharedPreviewPanel] dataSource] == self) {
[[QLPreviewPanel sharedPreviewPanel] reloadData];
}
}
- (void)setPySelection
{
[super setPySelection];
[self updateQuicklookIfNeeded];
}
- (void)setViewSelection
{
[super setViewSelection];
[self updateQuicklookIfNeeded];
}
/* Public */
- (BOOL)powerMarkerMode
{
return [[self model] powerMarkerMode];
}
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode
{
[[self model] setPowerMarkerMode:aPowerMarkerMode];
}
- (BOOL)deltaValuesMode
{
return [[self model] deltaValuesMode];
}
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode
{
[[self model] setDeltaValuesMode:aDeltaValuesMode];
}
/* Datasource */
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
return [[self model] valueForColumn:@"marked" row:row];
}
return [[self model] valueForRow:row column:identifier];
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
[[self model] setValue:object forColumn:identifier row:row];
}
else if ([identifier isEqual:@"name"]) {
NSString *oldName = [[self model] valueForRow:row column:identifier];
NSString *newName = object;
if (![newName isEqual:oldName]) {
BOOL renamed = [[self model] renameSelected:newName];
if (!renamed) {
[Dialogs showMessage:[NSString stringWithFormat:NSLocalizedString(@"The name '%@' already exists.", @""), newName]];
}
else {
[[self view] setNeedsDisplay:YES];
}
}
}
}
/* Delegate */
- (void)tableView:(NSTableView *)aTableView didClickTableColumn:(NSTableColumn *)tableColumn
{
if ([[[self view] sortDescriptors] count] < 1)
return;
NSSortDescriptor *sd = [[[self view] sortDescriptors] objectAtIndex:0];
[[self model] sortBy:[sd key] ascending:[sd ascending]];
}
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
BOOL isSelected = [[self view] isRowSelected:row];
BOOL isMarkable = n2b([[self model] valueForColumn:@"markable" row:row]);
if ([[column identifier] isEqual:@"marked"]) {
[cell setEnabled:isMarkable];
// Low-tech solution, for indentation, but it works...
NSCellImagePosition pos = isMarkable ? NSImageRight : NSImageLeft;
[cell setImagePosition:pos];
}
if ([cell isKindOfClass:[NSTextFieldCell class]]) {
NSColor *color = [NSColor textColor];
if (isSelected) {
color = [NSColor selectedTextColor];
}
else if (isMarkable) {
if ([[self model] isDeltaAtRow:row column:[column identifier]]) {
color = [NSColor orangeColor];
}
}
else {
color = [NSColor blueColor];
}
[(NSTextFieldCell *)cell setTextColor:color];
}
}
- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView
{
[[self model] removeSelected];
return YES;
}
- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView
{
[[self model] markSelected];
return YES;
}
/* Quicklook */
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
return [[[self model] selectedRows] count];
}
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index
{
NSArray *selectedRows = [[self model] selectedRows];
NSInteger absIndex = n2i([selectedRows objectAtIndex:index]);
NSString *path = [[self model] pathAtIndex:absIndex];
return [[HSQLPreviewItem alloc] initWithUrl:[NSURL fileURLWithPath:path] title:path];
}
- (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event
{
// redirect all key down events to the table view
if ([event type] == NSKeyDown) {
[[self view] keyDown:event];
return YES;
}
return NO;
}
/* Python --> Cocoa */
- (void)invalidateMarkings
{
[[self view] setNeedsDisplay:YES];
}
@end

76
cocoa/ResultWindow.h Normal file
View File

@ -0,0 +1,76 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#import "StatsLabel.h"
#import "ResultTable.h"
#import "HSTableView.h"
#import "PyDupeGuru.h"
@class AppDelegate;
@interface ResultWindow : NSWindowController
{
@protected
NSSegmentedControl *optionsSwitch;
NSToolbarItem *optionsToolbarItem;
HSTableView *matches;
NSTextField *stats;
NSSearchField *filterField;
AppDelegate *app;
PyDupeGuru *model;
ResultTable *table;
StatsLabel *statsLabel;
QLPreviewPanel* previewPanel;
}
@property (readwrite, retain) NSSegmentedControl *optionsSwitch;
@property (readwrite, retain) NSToolbarItem *optionsToolbarItem;
@property (readwrite, retain) HSTableView *matches;
@property (readwrite, retain) NSTextField *stats;
@property (readwrite, retain) NSSearchField *filterField;
- (id)initWithParentApp:(AppDelegate *)app;
/* Helpers */
- (void)fillColumnsMenu;
- (void)updateOptionSegments;
- (void)adjustUIToLocalization;
- (void)initResultColumns:(ResultTable *)aTable;
/* Actions */
- (void)changeOptions;
- (void)copyMarked;
- (void)trashMarked;
- (void)filter;
- (void)focusOnFilterField;
- (void)ignoreSelected;
- (void)invokeCustomCommand;
- (void)markAll;
- (void)markInvert;
- (void)markNone;
- (void)markSelected;
- (void)moveMarked;
- (void)openClicked;
- (void)openSelected;
- (void)removeMarked;
- (void)removeSelected;
- (void)renameSelected;
- (void)reprioritizeResults;
- (void)resetColumnsToDefault;
- (void)revealSelected;
- (void)saveResults;
- (void)switchSelected;
- (void)toggleColumn:(id)sender;
- (void)toggleDelta;
- (void)toggleDetailsPanel;
- (void)togglePowerMarker;
- (void)toggleQuicklookPanel;
@end

406
cocoa/ResultWindow.m Normal file
View File

@ -0,0 +1,406 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ResultWindow.h"
#import "ResultWindow_UI.h"
#import "Dialogs.h"
#import "ProgressController.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
#import "PrioritizeDialog.h"
@implementation ResultWindow
@synthesize optionsSwitch;
@synthesize optionsToolbarItem;
@synthesize matches;
@synthesize stats;
@synthesize filterField;
- (id)initWithParentApp:(AppDelegate *)aApp;
{
self = [super initWithWindow:nil];
app = aApp;
model = [app model];
[self setWindow:createResultWindow_UI(self)];
[[self window] setTitle:fmt(NSLocalizedString(@"%@ Results", @""), [model appName])];
/* Put a cute iTunes-like bottom bar */
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
[self initResultColumns:table];
[[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu];
[matches setTarget:self];
[matches setDoubleAction:@selector(openClicked)];
[self adjustUIToLocalization];
return self;
}
- (void)dealloc
{
[table release];
[statsLabel release];
[super dealloc];
}
/* Helpers */
- (void)fillColumnsMenu
{
[[app columnsMenu] removeAllItems];
NSArray *menuItems = [[[table columns] model] menuItems];
for (NSInteger i=0; i < [menuItems count]; i++) {
NSArray *pair = [menuItems objectAtIndex:i];
NSString *display = [pair objectAtIndex:0];
BOOL marked = n2b([pair objectAtIndex:1]);
NSMenuItem *mi = [[app columnsMenu] addItemWithTitle:display action:@selector(toggleColumn:) keyEquivalent:@""];
[mi setTarget:self];
[mi setState:marked ? NSOnState : NSOffState];
[mi setTag:i];
}
[[app columnsMenu] addItem:[NSMenuItem separatorItem]];
NSMenuItem *mi = [[app columnsMenu] addItemWithTitle:NSLocalizedString(@"Reset to Default", @"")
action:@selector(resetColumnsToDefault) keyEquivalent:@""];
[mi setTarget:self];
}
- (void)updateOptionSegments
{
[optionsSwitch setSelected:[[app detailsPanel] isVisible] forSegment:0];
[optionsSwitch setSelected:[table powerMarkerMode] forSegment:1];
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
}
- (void)adjustUIToLocalization
{
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
NSInteger seg1delta = 0;
NSInteger seg2delta = 0;
if ([lang isEqual:@"ru"]) {
seg2delta = 20;
}
else if ([lang isEqual:@"uk"]) {
seg2delta = 20;
}
else if ([lang isEqual:@"hy"]) {
seg1delta = 20;
}
if (seg1delta || seg2delta) {
[optionsSwitch setWidth:[optionsSwitch widthForSegment:0]+seg1delta forSegment:0];
[optionsSwitch setWidth:[optionsSwitch widthForSegment:1]+seg2delta forSegment:1];
NSSize s = [optionsToolbarItem maxSize];
s.width += seg1delta + seg2delta;
[optionsToolbarItem setMaxSize:s];
[optionsToolbarItem setMinSize:s];
}
}
- (void)initResultColumns:(ResultTable *)aTable
{
NSInteger appMode = [app getAppMode];
if (appMode == AppModePicture) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 162, 16, 0, YES, nil},
{@"folder_path", 142, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"dimensions", 73, 16, 0, YES, nil},
{@"exif_timestamp", 120, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 58, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else if (appMode == AppModeMusic) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 235, 16, 0, YES, nil},
{@"folder_path", 120, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"duration", 50, 16, 0, YES, nil},
{@"bitrate", 50, 16, 0, YES, nil},
{@"samplerate", 60, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"title", 120, 16, 0, YES, nil},
{@"artist", 120, 16, 0, YES, nil},
{@"album", 120, 16, 0, YES, nil},
{@"genre", 80, 16, 0, YES, nil},
{@"year", 40, 16, 0, YES, nil},
{@"track", 40, 16, 0, YES, nil},
{@"comment", 120, 16, 0, YES, nil},
{@"percentage", 57, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"duration"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"bitrate"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 195, 16, 0, YES, nil},
{@"folder_path", 183, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 60, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
[[aTable columns] restoreColumns];
}
/* Actions */
- (void)changeOptions
{
NSInteger seg = [optionsSwitch selectedSegment];
if (seg == 0) {
[self toggleDetailsPanel];
}
else if (seg == 1) {
[self togglePowerMarker];
}
else if (seg == 2) {
[self toggleDelta];
}
}
- (void)copyMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model setCopyMoveDestType:n2i([ud objectForKey:@"recreatePathType"])];
[model copyMarked];
}
- (void)trashMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model deleteMarked];
}
- (void)filter
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setEscapeFilterRegexp:!n2b([ud objectForKey:@"useRegexpFilter"])];
[model applyFilter:[filterField stringValue]];
}
- (void)focusOnFilterField
{
[[self window] makeFirstResponder:filterField];
}
- (void)ignoreSelected
{
[model addSelectedToIgnoreList];
}
- (void)invokeCustomCommand
{
[model invokeCustomCommand];
}
- (void)markAll
{
[model markAll];
}
- (void)markInvert
{
[model markInvert];
}
- (void)markNone
{
[model markNone];
}
- (void)markSelected
{
[model toggleSelectedMark];
}
- (void)moveMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model setCopyMoveDestType:n2i([ud objectForKey:@"recreatePathType"])];
[model moveMarked];
}
- (void)openClicked
{
if ([matches clickedRow] < 0) {
return;
}
[matches selectRowIndexes:[NSIndexSet indexSetWithIndex:[matches clickedRow]] byExtendingSelection:NO];
[model openSelected];
}
- (void)openSelected
{
[model openSelected];
}
- (void)removeMarked
{
[model removeMarked];
}
- (void)removeSelected
{
[model removeSelected];
}
- (void)renameSelected
{
NSInteger col = [matches columnWithIdentifier:@"name"];
NSInteger row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (void)reprioritizeResults
{
PrioritizeDialog *dlg = [[PrioritizeDialog alloc] initWithApp:model];
NSInteger result = [NSApp runModalForWindow:[dlg window]];
if (result == NSRunStoppedResponse) {
[[dlg model] performReprioritization];
}
[dlg release];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)resetColumnsToDefault
{
[[[table columns] model] resetToDefaults];
[self fillColumnsMenu];
}
- (void)revealSelected
{
[model revealSelected];
}
- (void)saveResults
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")];
if ([sp runModal] == NSOKButton) {
[model saveResultsAs:[[sp URL] path]];
[[app recentResults] addFile:[[sp URL] path]];
}
}
- (void)switchSelected
{
[model makeSelectedReference];
}
- (void)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
BOOL checked = [[[table columns] model] toggleMenuItem:[mi tag]];
[mi setState:checked ? NSOnState : NSOffState];
}
- (void)toggleDetailsPanel
{
[[app detailsPanel] toggleVisibility];
[self updateOptionSegments];
}
- (void)toggleDelta
{
[table setDeltaValuesMode:![table deltaValuesMode]];
[self updateOptionSegments];
}
- (void)togglePowerMarker
{
[table setPowerMarkerMode:![table powerMarkerMode]];
[self updateOptionSegments];
}
- (void)toggleQuicklookPanel
{
if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
}
else {
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
}
}
/* Quicklook */
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
{
return YES;
}
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
previewPanel = [panel retain];
panel.delegate = table;
panel.dataSource = table;
}
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document loses its responsisibility on the preview panel
// Until the next call to -beginPreviewPanelControl: it must not
// change the panel's delegate, data source or refresh it.
[previewPanel release];
previewPanel = nil;
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{
return ![[ProgressController mainProgressController] isShown];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Mark All", @"")];
}
return ![[ProgressController mainProgressController] isShown];
}
@end

17
cocoa/StatsLabel.h Normal file
View File

@ -0,0 +1,17 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSGUIController.h"
#import "PyStatsLabel.h"
@interface StatsLabel : HSGUIController {}
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aLabelView;
- (PyStatsLabel *)model;
- (NSTextField *)labelView;
@end

34
cocoa/StatsLabel.m Normal file
View File

@ -0,0 +1,34 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "StatsLabel.h"
#import "Utils.h"
@implementation StatsLabel
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aLabelView
{
return [super initWithPyRef:aPyRef wrapperClass:[PyStatsLabel class]
callbackClassName:@"StatsLabelView" view:aLabelView];
}
- (PyStatsLabel *)model
{
return (PyStatsLabel *)model;
}
- (NSTextField *)labelView
{
return (NSTextField *)view;
}
/* Python --> Cocoa */
- (void)refresh
{
[[self labelView] setStringValue:[[self model] display]];
}
@end

19
cocoa/dg_cocoa.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright 2017 Virgil Dupras
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
import codecs, io, locale
from hscommon.trans import install_gettext_trans_under_cocoa
install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import *
from inter.app import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module.
import distutils.sysconfig

BIN
cocoa/dupeguru.icns Executable file

Binary file not shown.

View File

@ -0,0 +1,140 @@
"%@ Results" = "%@ Results";
"About dupeGuru" = "About dupeGuru";
"Action" = "Action";
"Actions" = "Actions";
"Add criteria to the right box and click OK to send the dupes that correspond the best to these criteria to their respective group's reference position. Read the help file for more information." = "Add criteria to the right box and click OK to send the dupes that correspond the best to these criteria to their respective group's reference position. Read the help file for more information.";
"Add New Folder..." = "Add New Folder...";
"Add Selected to Ignore List" = "Add Selected to Ignore List";
"Advanced" = "Advanced";
"After having deleted a duplicate, place a link targeting the reference file to replace the deleted file." = "After having deleted a duplicate, place a link targeting the reference file to replace the deleted file.";
"Album" = "Album";
"Application Mode:" = "Application Mode:";
"Artist" = "Artist";
"Attribute" = "Attribute";
"Automatically check for updates" = "Automatically check for updates";
"Basic" = "Basic";
"Bring All to Front" = "Bring All to Front";
"Can mix file kind" = "Can mix file kind";
"Cancel" = "Cancel";
"Check for update..." = "Check for update...";
"Clear" = "Clear";
"Clear Picture Cache" = "Clear Picture Cache";
"Close" = "Close";
"Close Window" = "Close Window";
"Columns" = "Columns";
"Copy" = "Copy";
"Copy and Move:" = "Copy and Move:";
"Copy Marked to..." = "Copy Marked to...";
"Custom command (arguments: %d for dupe, %r for ref):" = "Custom command (arguments: %d for dupe, %r for ref):";
"Cut" = "Cut";
"Debug mode (restart required)" = "Debug mode (restart required)";
"Deletion Options" = "Deletion Options";
"Delta" = "Delta";
"Details" = "Details";
"Details of Selected File" = "Details of Selected File";
"Details Panel" = "Details Panel";
"Directly delete files" = "Directly delete files";
"Directories" = "Directories";
"Do you really want to remove all your cached picture analysis?" = "Do you really want to remove all your cached picture analysis?";
"dupeGuru" = "dupeGuru";
"dupeGuru Help" = "dupeGuru Help";
"dupeGuru Preferences" = "dupeGuru Preferences";
"dupeGuru Results" = "dupeGuru Results";
"dupeGuru Website" = "dupeGuru Website";
"Dupes Only" = "Dupes Only";
"Edit" = "Edit";
"Excluded" = "Excluded";
"Export Results to CSV" = "Export Results to CSV";
"Export Results to XHTML" = "Export Results to XHTML";
"Fewer results" = "Fewer results";
"File" = "File";
"Filter" = "Filter";
"Filter hardness:" = "Filter hardness:";
"Filter Results..." = "Filter Results...";
"Folder Selection Window" = "Folder Selection Window";
"Font Size:" = "Font Size:";
"Genre" = "Genre";
"Help" = "Help";
"Hide dupeGuru" = "Hide dupeGuru";
"Hide Others" = "Hide Others";
"Ignore duplicates hardlinking to the same file" = "Ignore duplicates hardlinking to the same file";
"Ignore files smaller than:" = "Ignore files smaller than:";
"Ignore List" = "Ignore List";
"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." = "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.";
"Invert Marking" = "Invert Marking";
"Invoke Custom Command" = "Invoke Custom Command";
"KB" = "KB";
"Link deleted files" = "Link deleted files";
"Load from file..." = "Load from file...";
"Load Recent Results" = "Load Recent Results";
"Load Results" = "Load Results";
"Load Results..." = "Load Results...";
"Make Selected into Reference" = "Make Selected into Reference";
"Mark All" = "Mark All";
"Mark None" = "Mark None";
"Mark Selected" = "Mark Selected";
"Match pictures of different dimensions" = "Match pictures of different dimensions";
"Match similar words" = "Match similar words";
"Minimize" = "Minimize";
"Mode" = "Mode";
"More results" = "More results";
"Move Marked to..." = "Move Marked to...";
"Music" = "Music";
"Name" = "Name";
"Normal" = "Normal";
"Ok" = "Ok";
"Open Selected with Default Application" = "Open Selected with Default Application";
"Options" = "Options";
"Paste" = "Paste";
"Picture" = "Picture";
"Preferences..." = "Preferences...";
"Problems!" = "Problems!";
"Proceed" = "Proceed";
"Quick Look" = "Quick Look";
"Quit dupeGuru" = "Quit dupeGuru";
"Re-Prioritize duplicates" = "Re-Prioritize duplicates";
"Re-Prioritize Results..." = "Re-Prioritize Results...";
"Recreate absolute path" = "Recreate absolute path";
"Recreate relative path" = "Recreate relative path";
"Reference" = "Reference";
"Remove empty folders on delete or move" = "Remove empty folders on delete or move";
"Remove Marked from Results" = "Remove Marked from Results";
"Remove Selected" = "Remove Selected";
"Remove Selected from Results" = "Remove Selected from Results";
"Rename Selected" = "Rename Selected";
"Reset to Default" = "Reset to Default";
"Reset To Defaults" = "Reset To Defaults";
"Results Window" = "Results Window";
"Reveal" = "Reveal";
"Reveal Selected in Finder" = "Reveal Selected in Finder";
"Right in destination" = "Right in destination";
"Save Results..." = "Save Results...";
"Scan" = "Scan";
"Scan Type:" = "Scan Type:";
"Select a file to save your results to" = "Select a file to save your results to";
"Select a folder to add to the scanning list" = "Select a folder to add to the scanning list";
"Select a results file to load" = "Select a results file to load";
"Select All" = "Select All";
"Select folders to scan and press \"Scan\"." = "Select folders to scan and press \"Scan\".";
"Selected" = "Selected";
"Send Marked to Trash..." = "Send Marked to Trash...";
"Services" = "Services";
"Show All" = "Show All";
"Show Delta Values" = "Show Delta Values";
"Show Dupes Only" = "Show Dupes Only";
"Standard" = "Standard";
"Start Duplicate Scan" = "Start Duplicate Scan";
"State" = "State";
"Tags to scan:" = "Tags to scan:";
"The name '%@' already exists." = "The name '%@' already exists.";
"There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results." = "There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results.";
"Title" = "Title";
"Track" = "Track";
"Use regular expressions when filtering" = "Use regular expressions when filtering";
"Window" = "Window";
"Word weighting" = "Word weighting";
"Year" = "Year";
"You have unsaved results, do you really want to continue?" = "You have unsaved results, do you really want to continue?";
"You have unsaved results, do you really want to quit?" = "You have unsaved results, do you really want to quit?";
"Zoom" = "Zoom";

0
cocoa/inter/__init__.py Normal file
View File

10
cocoa/inter/all.py Normal file
View File

@ -0,0 +1,10 @@
from cocoa.inter import PyTextField, PyProgressWindow
from .deletion_options import PyDeletionOptions
from .details_panel import PyDetailsPanel
from .directory_outline import PyDirectoryOutline
from .prioritize_dialog import PyPrioritizeDialog
from .prioritize_list import PyPrioritizeList
from .problem_dialog import PyProblemDialog
from .ignore_list_dialog import PyIgnoreListDialog
from .result_table import PyResultTable
from .stats_label import PyStatsLabel

252
cocoa/inter/app.py Normal file
View File

@ -0,0 +1,252 @@
import logging
from objp.util import pyref, dontwrap
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
from cocoa.inter import PyBaseApp, BaseAppView
import core.pe.photo
from core.app import DupeGuru as DupeGuruBase, AppMode
from .directories import Directories, Bundle
from .photo import Photo
class DupeGuru(DupeGuruBase):
PICTURE_CACHE_TYPE = 'shelve'
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.directories = Directories()
def selected_dupe_path(self):
if not self.selected_dupes:
return None
return self.selected_dupes[0].path
def selected_dupe_ref_path(self):
if not self.selected_dupes:
return None
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
return None
return ref.path
def _get_fileclasses(self):
result = DupeGuruBase._get_fileclasses(self)
if self.app_mode == AppMode.Standard:
result = [Bundle] + result
return result
class DupeGuruView(BaseAppView):
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
def createResultsWindow(self): pass
def showResultsWindow(self): pass
def showProblemDialog(self): pass
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
class PyDupeGuru(PyBaseApp):
@dontwrap
def __init__(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
install_exception_hook('https://github.com/hsoft/dupeguru/issues')
install_cocoa_logger()
patch_threaded_job_performer()
self.model = DupeGuru(self)
#---Sub-proxies
def detailsPanel(self) -> pyref:
return self.model.details_panel
def directoryTree(self) -> pyref:
return self.model.directory_tree
def problemDialog(self) -> pyref:
return self.model.problem_dialog
def statsLabel(self) -> pyref:
return self.model.stats_label
def resultTable(self) -> pyref:
return self.model.result_table
def ignoreListDialog(self) -> pyref:
return self.model.ignore_list_dialog
def progressWindow(self) -> pyref:
return self.model.progress_window
def deletionOptions(self) -> pyref:
return self.model.deletion_options
#---Directories
def addDirectory_(self, directory: str):
self.model.add_directory(directory)
#---Results
def doScan(self):
self.model.start_scanning()
def exportToXHTML(self):
self.model.export_to_xhtml()
def exportToCSV(self):
self.model.export_to_csv()
def loadSession(self):
self.model.load()
def loadResultsFrom_(self, filename: str):
self.model.load_from(filename)
def markAll(self):
self.model.mark_all()
def markNone(self):
self.model.mark_none()
def markInvert(self):
self.model.mark_invert()
def purgeIgnoreList(self):
self.model.purge_ignore_list()
def toggleSelectedMark(self):
self.model.toggle_selected_mark_state()
def saveSession(self):
self.model.save()
def saveResultsAs_(self, filename: str):
self.model.save_as(filename)
#---Actions
def addSelectedToIgnoreList(self):
self.model.add_selected_to_ignore_list()
def deleteMarked(self):
self.model.delete_marked()
def applyFilter_(self, filter: str):
self.model.apply_filter(filter)
def makeSelectedReference(self):
self.model.make_selected_reference()
def copyMarked(self):
self.model.copy_or_move_marked(copy=True)
def moveMarked(self):
self.model.copy_or_move_marked(copy=False)
def openSelected(self):
self.model.open_selected()
def removeMarked(self):
self.model.remove_marked()
def removeSelected(self):
self.model.remove_selected()
def revealSelected(self):
self.model.reveal_selected()
def invokeCustomCommand(self):
self.model.invoke_custom_command()
def showIgnoreList(self):
self.model.ignore_list_dialog.show()
def clearPictureCache(self):
self.model.clear_picture_cache()
#---Information
def getScanOptions(self) -> list:
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
def resultsAreModified(self) -> bool:
return self.model.results.is_modified
def getSelectedDupePath(self) -> str:
return str(self.model.selected_dupe_path())
def getSelectedDupeRefPath(self) -> str:
return str(self.model.selected_dupe_ref_path())
#---Properties
def getAppMode(self) -> int:
return self.model.app_mode
def setAppMode_(self, app_mode: int):
self.model.app_mode = app_mode
def setScanType_(self, scan_type_index: int):
scan_options = self.model.SCANNER_CLASS.get_scan_options()
try:
so = scan_options[scan_type_index]
self.model.options['scan_type'] = so.scan_type
except IndexError:
pass
def setMinMatchPercentage_(self, percentage: int):
self.model.options['min_match_percentage'] = int(percentage)
def setWordWeighting_(self, words_are_weighted: bool):
self.model.options['word_weighting'] = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.options['match_similar_words'] = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.options['size_threshold'] = size_threshold
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if 'scanned_tags' not in self.model.options:
self.model.options['scanned_tags'] = set()
if enable:
self.model.options['scanned_tags'].add(scan_tag)
else:
self.model.options['scanned_tags'].discard(scan_tag)
def setMatchScaled_(self, match_scaled: bool):
self.model.options['match_scaled'] = match_scaled
def setMixFileKind_(self, mix_file_kind: bool):
self.model.options['mix_file_kind'] = mix_file_kind
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
self.model.options['escape_filter_regexp'] = escape_filter_regexp
def setRemoveEmptyFolders_(self, remove_empty_folders: bool):
self.model.options['clean_empty_dirs'] = remove_empty_folders
def setIgnoreHardlinkMatches_(self, ignore_hardlink_matches: bool):
self.model.options['ignore_hardlink_matches'] = ignore_hardlink_matches
def setCopyMoveDestType_(self, copymove_dest_type: int):
self.model.options['copymove_dest_type'] = copymove_dest_type
#--- model --> view
@dontwrap
def ask_yes_no(self, prompt):
return self.callback.askYesNoWithPrompt_(prompt)
@dontwrap
def create_results_window(self):
self.callback.createResultsWindow()
@dontwrap
def show_results_window(self):
self.callback.showResultsWindow()
@dontwrap
def show_problem_dialog(self):
self.callback.showProblemDialog()
@dontwrap
def select_dest_folder(self, prompt):
return self.callback.selectDestFolderWithPrompt_(prompt)
@dontwrap
def select_dest_file(self, prompt, extension):
return self.callback.selectDestFileWithPrompt_extension_(prompt, extension)

View File

@ -0,0 +1,37 @@
# Created On: 2012-05-30
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from objp.util import dontwrap
from cocoa.inter import PyGUIObject, GUIObjectView
class DeletionOptionsView(GUIObjectView):
def updateMsg_(self, msg: str): pass
def show(self) -> bool: pass
def setHardlinkOptionEnabled_(self, enabled: bool): pass
class PyDeletionOptions(PyGUIObject):
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
#--- model --> view
@dontwrap
def update_msg(self, msg):
self.callback.updateMsg_(msg)
@dontwrap
def show(self):
return self.callback.show()
@dontwrap
def set_hardlink_option_enabled(self, enabled):
self.callback.setHardlinkOptionEnabled_(enabled)

View File

@ -0,0 +1,11 @@
from cocoa.inter import PyGUIObject, GUIObjectView
class DetailsPanelView(GUIObjectView):
pass
class PyDetailsPanel(PyGUIObject):
def numberOfRows(self) -> int:
return self.model.row_count()
def valueForColumn_row_(self, column: str, row: int) -> object:
return self.model.row(row)[int(column)]

View File

@ -0,0 +1,53 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from hscommon.path import Path, pathify
from core.se import fs
from core.directories import Directories as DirectoriesBase, DirectoryState
def is_bundle(str_path):
uti = proxy.getUTI_(str_path)
if uti is None:
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return proxy.type_conformsToType_(uti, 'com.apple.bundle') or proxy.type_conformsToType_(uti, 'com.apple.package')
class Bundle(fs.Folder):
@classmethod
@pathify
def can_handle(cls, path: Path):
return not path.islink() and path.isdir() and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if path in self.ROOT_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
def _get_folders(self, from_folder, j):
# We don't want to scan bundle's subfolder even in Folders mode. Bundle's integrity has to
# stay intact.
if is_bundle(str(from_folder.path)):
# just yield the current folder and bail
state = self.get_state(from_folder.path)
if state != DirectoryState.Excluded:
from_folder.is_ref = state == DirectoryState.Reference
yield from_folder
return
else:
yield from DirectoriesBase._get_folders(self, from_folder, j)
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))]

View File

@ -0,0 +1,21 @@
from objp.util import dontwrap
from cocoa.inter import PyOutline, GUIObjectView
class DirectoryOutlineView(GUIObjectView):
pass
class PyDirectoryOutline(PyOutline):
def addDirectory_(self, path: str):
self.model.add_directory(path)
def removeSelectedDirectory(self):
self.model.remove_selected()
def selectAll(self):
self.model.select_all()
# python --> cocoa
@dontwrap
def refresh_states(self):
# Under cocoa, both refresh() and refresh_states() do the same thing.
self.callback.refresh()

View File

@ -0,0 +1,21 @@
from objp.util import pyref, dontwrap
from cocoa.inter import PyGUIObject, GUIObjectView
class IgnoreListDialogView(GUIObjectView):
def show(self): pass
class PyIgnoreListDialog(PyGUIObject):
def ignoreListTable(self) -> pyref:
return self.model.ignore_list_table
def removeSelected(self):
self.model.remove_selected()
def clear(self):
self.model.clear()
#--- model --> view
@dontwrap
def show(self):
self.callback.show()

35
cocoa/inter/photo.py Normal file
View File

@ -0,0 +1,35 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from core.pe import _block_osx
from core.pe.photo import Photo as PhotoBase
class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
HANDLED_EXTS.update({'psd', 'nef', 'cr2', 'orf'})
def _plat_get_dimensions(self):
return _block_osx.get_image_size(str(self.path))
def _plat_get_blocks(self, block_count_per_side, orientation):
try:
blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks:
raise IOError('The picture %s could not be read' % str(self.path))
return blocks
def _get_exif_timestamp(self):
exifdata = proxy.readExifData_(str(self.path))
if exifdata:
try:
return exifdata['{Exif}']['DateTimeOriginal']
except KeyError:
return ''
else:
return ''

View File

@ -0,0 +1,29 @@
from objp.util import pyref
from cocoa.inter import PyGUIObject, GUIObjectView
from core.gui.prioritize_dialog import PrioritizeDialog
class PrioritizeDialogView(GUIObjectView):
pass
class PyPrioritizeDialog(PyGUIObject):
def __init__(self, app: pyref):
model = PrioritizeDialog(app.model)
PyGUIObject.__init__(self, model)
def categoryList(self) -> pyref:
return self.model.category_list
def criteriaList(self) -> pyref:
return self.model.criteria_list
def prioritizationList(self) -> pyref:
return self.model.prioritization_list
def addSelected(self):
self.model.add_selected()
def removeSelected(self):
self.model.remove_selected()
def performReprioritization(self):
self.model.perform_reprioritization()

View File

@ -0,0 +1,8 @@
from cocoa.inter import PySelectableList, SelectableListView
class PrioritizeListView(SelectableListView):
pass
class PyPrioritizeList(PySelectableList):
def moveIndexes_toIndex_(self, indexes: list, dest_index: int):
self.model.move_indexes(indexes, dest_index)

View File

@ -0,0 +1,9 @@
from objp.util import pyref
from cocoa.inter import PyGUIObject
class PyProblemDialog(PyGUIObject):
def problemTable(self) -> pyref:
return self.model.problem_table
def revealSelected(self):
self.model.reveal_selected_dupe()

View File

@ -0,0 +1,50 @@
from objp.util import dontwrap
from cocoa.inter import PyTable, TableView
class ResultTableView(TableView):
def invalidateMarkings(self): pass
class PyResultTable(PyTable):
def powerMarkerMode(self) -> bool:
return self.model.power_marker
def setPowerMarkerMode_(self, value: bool):
self.model.power_marker = value
def deltaValuesMode(self) -> bool:
return self.model.delta_values
def setDeltaValuesMode_(self, value: bool):
self.model.delta_values = value
def valueForRow_column_(self, row_index: int, column: str) -> object:
return self.model.get_row_value(row_index, column)
def isDeltaAtRow_column_(self, row_index: int, column: str) -> bool:
row = self.model[row_index]
return row.is_cell_delta(column)
def renameSelected_(self, newname: str) -> bool:
return self.model.rename_selected(newname)
def sortBy_ascending_(self, key: str, asc: bool):
self.model.sort(key, asc)
def markSelected(self):
self.model.app.toggle_selected_mark_state()
def removeSelected(self):
self.model.app.remove_selected()
def selectedDupeCount(self) -> int:
return self.model.selected_dupe_count
def pathAtIndex_(self, index: int) -> str:
row = self.model[index]
return str(row._dupe.path)
# python --> cocoa
@dontwrap
def invalidate_markings(self):
self.callback.invalidateMarkings()

View File

@ -0,0 +1,9 @@
from cocoa.inter import PyGUIObject, GUIObjectView
class StatsLabelView(GUIObjectView):
pass
class PyStatsLabel(PyGUIObject):
def display(self) -> str:
return self.model.display

49
cocoa/main.m Normal file
View File

@ -0,0 +1,49 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import <wchar.h>
#import <locale.h>
#import "AppDelegate.h"
#import "MainMenu_UI.h"
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* We have to set the locate to UTF8 for mbstowcs() to correctly convert non-ascii chars in paths */
setlocale(LC_ALL, "en_US.UTF-8");
NSString *respath = [[NSBundle mainBundle] resourcePath];
NSString *mainpy = [respath stringByAppendingPathComponent:@"dg_cocoa.py"];
wchar_t wPythonPath[PATH_MAX+1];
NSString *pypath = [respath stringByAppendingPathComponent:@"py"];
mbstowcs(wPythonPath, [pypath fileSystemRepresentation], PATH_MAX+1);
Py_SetPath(wPythonPath);
Py_SetPythonHome(wPythonPath);
Py_Initialize();
PyEval_InitThreads();
PyGILState_STATE gilState = PyGILState_Ensure();
FILE* fp = fopen([mainpy UTF8String], "r");
PyRun_SimpleFile(fp, [mainpy UTF8String]);
fclose(fp);
PyGILState_Release(gilState);
if (gilState == PyGILState_LOCKED) {
PyThreadState_Swap(NULL);
PyEval_ReleaseLock();
}
[NSApplication sharedApplication];
AppDelegate *appDelegate = [[AppDelegate alloc] init];
[NSApp setDelegate:appDelegate];
[NSApp setMainMenu:createMainMenu_UI(appDelegate)];
[appDelegate finalizeInit];
[pool release];
[NSApp run];
Py_Finalize();
return 0;
}

10
cocoa/run_template.py Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python3
import sys
import os
def main():
return os.system('open "{{app_path}}"')
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,49 @@
ownerclass = 'DeletionOptions'
ownerimport = 'DeletionOptions.h'
result = Window(450, 240, "Deletion Options")
messageLabel = Label(result, "")
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.linkButton = linkCheckbox
owner.linkTypeRadio = linkTypeChoice
owner.directButton = directCheckbox
owner.messageTextField = messageLabel
result.canMinimize = False
result.canResize = False
linkLabel.controlSize = ControlSize.Small
directLabel.controlSize = ControlSize.Small
linkTypeChoice.controlSize = ControlSize.Small
proceedButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e'
linkCheckbox.action = directCheckbox.action = linkTypeChoice.action = Action(owner, 'updateOptions')
proceedButton.action = Action(owner, 'proceed')
cancelButton.action = Action(owner, 'cancel')
linkLabel.height *= 2 # 2 lines
directLabel.height *= 3 # 3 lines
proceedButton.width = 92
cancelButton.width = 92
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 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

32
cocoa/ui/details_panel.py Normal file
View File

@ -0,0 +1,32 @@
ownerclass = 'DetailsPanel'
ownerimport = 'DetailsPanel.h'
result = Panel(451, 146, "Details of Selected File")
table = TableView(result)
owner.detailsTable = table
result.style = PanelStyle.Utility
result.xProportion = 0.2
result.yProportion = 0.4
result.canMinimize = False
result.autosaveName = 'DetailsPanel'
result.minSize = Size(result.width, result.height)
table.dataSource = owner
table.allowsColumnReordering = False
table.allowsColumnSelection = False
table.allowsMultipleSelection = False
table.font = Font(FontFamily.System, FontSize.SmallSystem)
table.rowHeight = 14
table.editable = False
col = table.addColumn('0', "Attribute", 70)
col.autoResizable = True
col = table.addColumn('1', "Selected", 198)
col.autoResizable = True
col = table.addColumn('2', "Reference", 172)
col.autoResizable = True
table.packToCorner(Pack.UpperLeft, margin=0)
table.fill(Pack.LowerRight, margin=0)
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@ -0,0 +1,70 @@
ownerclass = 'DetailsPanelPicture'
ownerimport = 'DetailsPanelPicture.h'
result = Panel(593, 398, "Details of Selected File")
table = TableView(result)
split = SplitView(result, 2, vertical=True)
leftSplit, rightSplit = split.subviews
selectedLabel = Label(leftSplit, "Selected")
selectedImage = ImageView(leftSplit, 'NSApplicationIcon')
leftSpinner = ProgressIndicator(leftSplit)
referenceLabel = Label(rightSplit, "Reference")
referenceImage = ImageView(rightSplit, 'NSApplicationIcon')
rightSpinner = ProgressIndicator(rightSplit)
owner.detailsTable = table
owner.dupeImage = selectedImage
owner.dupeProgressIndicator = leftSpinner
owner.refImage = referenceImage
owner.refProgressIndicator = rightSpinner
table.dataSource = owner
result.style = PanelStyle.Utility
result.xProportion = 0.6
result.yProportion = 0.6
result.canMinimize = False
result.autosaveName = 'DetailsPanel'
result.minSize = Size(451, 240)
table.allowsColumnReordering = False
table.allowsColumnSelection = False
table.allowsMultipleSelection = False
table.font = Font(FontFamily.System, FontSize.SmallSystem)
table.rowHeight = 14
table.editable = False
col = table.addColumn('0', "Attribute", 70)
col.autoResizable = True
col = table.addColumn('1', "Selected", 198)
col.autoResizable = True
col = table.addColumn('2', "Reference", 172)
col.autoResizable = True
table.height = 165
sides = [
(leftSplit, selectedLabel, selectedImage, leftSpinner),
(rightSplit, referenceLabel, referenceImage, rightSpinner),
]
for subSplit, label, image, spinner in sides:
label.alignment = TextAlignment.Center
spinner.style = const.NSProgressIndicatorSpinningStyle
spinner.controlSize = const.NSSmallControlSize
spinner.displayedWhenStopped = False
label.packToCorner(Pack.UpperLeft, margin=0)
label.fill(Pack.Right, margin=0)
label.setAnchor(Pack.UpperLeft, growX=True)
image.packRelativeTo(label, Pack.Below)
image.fill(Pack.LowerRight, margin=0)
image.setAnchor(Pack.UpperLeft, growX=True, growY=True)
spinner.y = label.y
spinner.x = subSplit.width - 30
spinner.setAnchor(Pack.UpperRight)
table.packToCorner(Pack.UpperLeft, margin=0)
table.fill(Pack.Right, margin=0)
table.setAnchor(Pack.UpperLeft, growX=True)
split.packRelativeTo(table, Pack.Below)
split.fill(Pack.LowerRight, margin=0)
split.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@ -0,0 +1,76 @@
ownerclass = 'DirectoryPanel'
ownerimport = 'DirectoryPanel.h'
result = Window(425, 300, "dupeGuru")
promptLabel = Label(result, "Select folders to scan and press \"Scan\".")
directoryOutline = OutlineView(result)
directoryOutline.OBJC_CLASS = 'HSOutlineView'
appModeSelector = SegmentedControl(result)
appModeLabel = Label(result, "Application Mode:")
scanTypePopup = Popup(result)
scanTypeLabel = Label(result, "Scan Type:")
addButton = Button(result, "")
removeButton = Button(result, "")
loadResultsButton = Button(result, "Load Results")
scanButton = Button(result, "Scan")
addPopup = Popup(None)
loadRecentPopup = Popup(None)
owner.outlineView = directoryOutline
owner.appModeSelector = appModeSelector
owner.scanTypePopup = scanTypePopup
owner.removeButton = removeButton
owner.loadResultsButton = loadResultsButton
owner.addButtonPopUp = addPopup
owner.loadRecentButtonPopUp = loadRecentPopup
result.autosaveName = 'DirectoryPanel'
result.canMinimize = False
result.minSize = Size(400, 270)
for label in ["Standard", "Music", "Picture"]:
appModeSelector.addSegment(label, 80)
addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle
addButton.image = 'NSAddTemplate'
removeButton.image = 'NSRemoveTemplate'
for button in (addButton, removeButton):
button.style = const.NSTexturedRoundedBezelStyle
button.imagePosition = const.NSImageOnly
scanButton.keyEquivalent = '\\r'
appModeSelector.action = Action(owner, 'changeAppMode:')
addButton.action = Action(owner, 'popupAddDirectoryMenu:')
removeButton.action = Action(owner, 'removeSelectedDirectory')
loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:')
scanButton.action = Action(None, 'startScanning')
directoryOutline.font = Font(FontFamily.System, FontSize.SmallSystem)
col = directoryOutline.addColumn('name', "Name", 100)
col.editable = False
col.autoResizable = True
col = directoryOutline.addColumn('state', "State", 85)
col.editable = True
col.autoResizable = False
col.dataCell = Popup(None, ["Normal", "Reference", "Excluded"])
col.dataCell.controlSize = const.NSSmallControlSize
directoryOutline.allowsColumnReordering = False
directoryOutline.allowsColumnSelection = False
directoryOutline.allowsMultipleSelection = True
appModeLabel.width = scanTypeLabel.width = 110
scanTypePopup.width = 248
appModeLayout = HLayout([appModeLabel, appModeSelector])
scanTypeLayout = HLayout([scanTypeLabel, scanTypePopup])
for button in (addButton, removeButton):
button.width = 28
for button in (loadResultsButton, scanButton):
button.width = 118
buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton])
mainLayout = VLayout([appModeLayout, scanTypeLayout, promptLabel, directoryOutline, buttonLayout], filler=directoryOutline)
mainLayout.packToCorner(Pack.UpperLeft)
mainLayout.fill(Pack.LowerRight)
directoryOutline.packRelativeTo(promptLabel, Pack.Below)
promptLabel.setAnchor(Pack.UpperLeft, growX=True)
directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True)
buttonLayout.setAnchor(Pack.Below)

View File

@ -0,0 +1,30 @@
ownerclass = 'IgnoreListDialog'
ownerimport = 'IgnoreListDialog.h'
result = Window(550, 350, "Ignore List")
table = TableView(result)
removeSelectedButton = Button(result, "Remove Selected")
clearButton = Button(result, "Clear")
closeButton = Button(result, "Close")
owner.ignoreListTableView = table
result.canMinimize = False
removeSelectedButton.action = Action(owner.model, 'removeSelected')
clearButton.action = Action(owner.model, 'clear')
closeButton.action = Action(result, 'performClose:')
closeButton.keyEquivalent = '\\r'
table.allowsColumnReordering = False
table.allowsColumnSelection = False
table.allowsMultipleSelection = True
removeSelectedButton.width = 142
clearButton.width = 142
closeButton.width = 84
buttonLayout = HLayout([removeSelectedButton, clearButton, None, closeButton])
buttonLayout.packToCorner(Pack.LowerLeft)
buttonLayout.fill(Pack.Right)
buttonLayout.setAnchor(Pack.Below)
table.packRelativeTo(buttonLayout, Pack.Above)
table.fill(Pack.UpperRight)
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)

77
cocoa/ui/main_menu.py Normal file
View File

@ -0,0 +1,77 @@
ownerclass = 'AppDelegate'
ownerimport = 'AppDelegate.h'
result = Menu("")
appMenu = result.addMenu("dupeGuru")
fileMenu = result.addMenu("File")
editMenu = result.addMenu("Edit")
actionMenu = result.addMenu("Actions")
owner.columnsMenu = result.addMenu("Columns")
modeMenu = result.addMenu("Mode")
windowMenu = result.addMenu("Window")
helpMenu = result.addMenu("Help")
appMenu.addItem("About dupeGuru", Action(owner, 'showAboutBox'))
appMenu.addSeparator()
appMenu.addItem("Preferences...", Action(owner, 'showPreferencesPanel'), 'cmd+,')
appMenu.addSeparator()
NSApp.servicesMenu = appMenu.addMenu("Services")
appMenu.addSeparator()
appMenu.addItem("Hide dupeGuru", Action(NSApp, 'hide:'), 'cmd+h')
appMenu.addItem("Hide Others", Action(NSApp, 'hideOtherApplications:'), 'cmd+alt+h')
appMenu.addItem("Show All", Action(NSApp, 'unhideAllApplications:'))
appMenu.addSeparator()
appMenu.addItem("Quit dupeGuru", Action(NSApp, 'terminate:'), 'cmd+q')
fileMenu.addItem("Load Results...", Action(None, 'loadResults'), 'cmd+o')
owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s')
fileMenu.addItem("Export Results to XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e')
fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV'))
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a')
editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a')
editMenu.addItem("Invert Marking", Action(None, 'markInvert'), 'cmd+alt+a')
editMenu.addItem("Mark Selected", Action(None, 'markSelected'), 'ctrl+cmd+a')
editMenu.addSeparator()
editMenu.addItem("Cut", Action(None, 'cut:'), 'cmd+x')
editMenu.addItem("Copy", Action(None, 'copy:'), 'cmd+c')
editMenu.addItem("Paste", Action(None, 'paste:'), 'cmd+v')
editMenu.addSeparator()
editMenu.addItem("Filter Results...", Action(None, 'focusOnFilterField'), 'cmd+alt+f')
actionMenu.addItem("Start Duplicate Scan", Action(owner, 'startScanning'), 'cmd+d')
actionMenu.addSeparator()
actionMenu.addItem("Send Marked to Trash...", Action(None, 'trashMarked'), 'cmd+t')
actionMenu.addItem("Move Marked to...", Action(None, 'moveMarked'), 'cmd+m')
actionMenu.addItem("Copy Marked to...", Action(None, 'copyMarked'), 'cmd+alt+m')
actionMenu.addItem("Remove Marked from Results", Action(None, 'removeMarked'), 'cmd+r')
actionMenu.addItem("Re-Prioritize Results...", Action(None, 'reprioritizeResults'))
actionMenu.addSeparator()
actionMenu.addItem("Remove Selected from Results", Action(None, 'removeSelected'), 'cmd+backspace')
actionMenu.addItem("Add Selected to Ignore List", Action(None, 'ignoreSelected'), 'cmd+g')
actionMenu.addItem("Make Selected into Reference", Action(None, 'switchSelected'), 'cmd+arrowup')
actionMenu.addSeparator()
actionMenu.addItem("Open Selected with Default Application", Action(None, 'openSelected'), 'cmd+return')
actionMenu.addItem("Reveal Selected in Finder", Action(None, 'revealSelected'), 'cmd+alt+return')
actionMenu.addItem("Invoke Custom Command", Action(None, 'invokeCustomCommand'), 'cmd+shift+c')
actionMenu.addItem("Rename Selected", Action(None, 'renameSelected'), 'enter')
modeMenu.addItem("Show Dupes Only", Action(None, 'togglePowerMarker'), 'cmd+1')
modeMenu.addItem("Show Delta Values", Action(None, 'toggleDelta'), 'cmd+2')
windowMenu.addItem("Results Window", Action(owner, 'showResultWindow'))
windowMenu.addItem("Folder Selection Window", Action(owner, 'showDirectoryWindow'))
windowMenu.addItem("Ignore List", Action(owner, 'showIgnoreList'))
windowMenu.addItem("Details Panel", Action(None, 'toggleDetailsPanel'), 'cmd+i')
windowMenu.addItem("Quick Look", Action(None, 'toggleQuicklookPanel'), 'cmd+l')
windowMenu.addSeparator()
windowMenu.addItem("Minimize", Action(None, 'performMinimize:'))
windowMenu.addItem("Zoom", Action(None, 'performZoom:'))
windowMenu.addItem("Close Window", Action(None, 'performClose:'), 'cmd+w')
windowMenu.addSeparator()
windowMenu.addItem("Bring All to Front", Action(None, 'arrangeInFront:'))
helpMenu.addItem("dupeGuru Help", Action(owner, 'openHelp'), 'cmd+?')
helpMenu.addItem("dupeGuru Website", Action(owner, 'openWebsite'))

View File

@ -0,0 +1,173 @@
appmode = args.get('appmode', 'standard')
dialogHeights = {
'standard': 325,
'music': 345,
'picture': 255,
}
result = Window(410, dialogHeights[appmode], "dupeGuru Preferences")
tabView = TabView(result)
basicTab = tabView.addTab("Basic")
advancedTab = tabView.addTab("Advanced")
thresholdSlider = Slider(basicTab.view, 1, 100, 80)
thresholdLabel = Label(basicTab.view, "Filter hardness:")
moreResultsLabel = Label(basicTab.view, "More results")
fewerResultsLabel = Label(basicTab.view, "Fewer results")
thresholdValueLabel = Label(basicTab.view, "")
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
fontSizeLabel = Label(basicTab.view, "Font Size:")
if appmode in ('standard', 'music'):
wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
elif appmode == 'picture':
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
if appmode == 'standard':
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
smallFilesThresholdText = TextField(basicTab.view, "")
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
elif appmode == 'music':
tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
trackBox = Checkbox(basicTab.view, "Track")
artistBox = Checkbox(basicTab.view, "Artist")
albumBox = Checkbox(basicTab.view, "Album")
titleBox = Checkbox(basicTab.view, "Title")
genreBox = Checkbox(basicTab.view, "Genre")
yearBox = Checkbox(basicTab.view, "Year")
tagBoxes = [trackBox, artistBox, albumBox, titleBox, genreBox, yearBox]
regexpCheckbox = Checkbox(advancedTab.view, "Use regular expressions when filtering")
ignoreHardlinksBox = Checkbox(advancedTab.view, "Ignore duplicates hardlinking to the same file")
debugModeCheckbox = Checkbox(advancedTab.view, "Debug mode (restart required)")
customCommandLabel = Label(advancedTab.view, "Custom command (arguments: %d for dupe, %r for ref):")
customCommandText = TextField(advancedTab.view, "")
copyMoveLabel = Label(advancedTab.view, "Copy and Move:")
copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"])
resetToDefaultsButton = Button(result, "Reset To Defaults")
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
mixKindBox.bind('value', defaults, 'values.mixFileKind')
removeEmptyFoldersBox.bind('value', defaults, 'values.removeEmptyFolders')
checkForUpdatesBox.bind('value', defaults, 'values.SUEnableAutomaticChecks')
regexpCheckbox.bind('value', defaults, 'values.useRegexpFilter')
ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
customCommandText.bind('value', defaults, 'values.CustomCommand')
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
if appmode in ('standard', 'music'):
wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
for control in disableWhenContentScan:
vtname = 'vtScanTypeMusicIsNotContent' if appmode == 'music' else 'vtScanTypeIsNotContent'
prefname = 'values.scanTypeMusic' if appmode == 'music' else 'values.scanTypeStandard'
control.bind('enabled', defaults, prefname, valueTransformer=vtname)
if appmode == 'standard':
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
elif appmode == 'music':
for box in tagBoxes:
box.bind('enabled', defaults, 'values.scanTypeMusic', valueTransformer='vtScanTypeIsTag')
trackBox.bind('value', defaults, 'values.scanTagTrack')
artistBox.bind('value', defaults, 'values.scanTagArtist')
albumBox.bind('value', defaults, 'values.scanTagAlbum')
titleBox.bind('value', defaults, 'values.scanTagTitle')
genreBox.bind('value', defaults, 'values.scanTagGenre')
yearBox.bind('value', defaults, 'values.scanTagYear')
elif appmode == 'picture':
matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled')
thresholdSlider.bind('enabled', defaults, 'values.scanTypePicture', valueTransformer='vtScanTypeIsFuzzy')
result.canResize = False
result.canMinimize = False
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
thresholdValueLabel.formatter.maximumFractionDigits = 0
allLabels = [thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
ignoreHardlinksBox, debugModeCheckbox]
if appmode == 'standard':
allLabels += [smallFilesThresholdSuffixLabel]
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'music':
allLabels += [tagsToScanLabel]
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'picture':
allCheckboxes += [matchDifferentDimensionsBox]
for label in allLabels:
label.controlSize = ControlSize.Small
fewerResultsLabel.alignment = TextAlignment.Right
for checkbox in allCheckboxes:
checkbox.font = thresholdValueLabel.font
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
thresholdLabel.width = fontSizeLabel.width = 94
fontSizeCombo.width = 66
thresholdValueLabel.width = 25
resetToDefaultsButton.width = 136
if appmode == 'standard':
smallFilesThresholdText.width = 60
smallFilesThresholdSuffixLabel.width = 40
elif appmode == 'music':
for box in tagBoxes:
box.width = 70
tabView.packToCorner(Pack.UpperLeft)
tabView.fill(Pack.Right)
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
tabView.fill(Pack.Below, margin=14)
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
thresholdLayout = HLayout([thresholdLabel, thresholdSlider, thresholdValueLabel], filler=thresholdSlider)
thresholdLayout.packToCorner(Pack.UpperLeft)
thresholdLayout.fill(Pack.Right)
# We want to give the labels as much space as possible, and we only "know" how much is available
# after the slider's fill operation.
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
if appmode == 'music':
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
tagsToScanLabel.fill(Pack.Left)
tagsToScanLabel.fill(Pack.Right)
trackBox.packRelativeTo(tagsToScanLabel, Pack.Below)
trackBox.x += 10
artistBox.packRelativeTo(trackBox, Pack.Right)
albumBox.packRelativeTo(artistBox, Pack.Right)
titleBox.packRelativeTo(trackBox, Pack.Below)
genreBox.packRelativeTo(titleBox, Pack.Right)
yearBox.packRelativeTo(genreBox, Pack.Right)
viewToPackCheckboxesUnder = titleBox
else:
viewToPackCheckboxesUnder = fontSizeCombo
if appmode == 'standard':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
ignoreSmallFilesBox]
elif appmode == 'music':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
elif appmode == 'picture':
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
checkboxLayout = VLayout(checkboxesToLayout)
checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
checkboxLayout.fill(Pack.Left)
checkboxLayout.fill(Pack.Right)
if appmode == 'standard':
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
checkForUpdatesBox.fill(Pack.Right)
smallFilesThresholdText.x += 20
smallFilesThresholdSuffixLabel.packRelativeTo(smallFilesThresholdText, Pack.Right)
advancedLayout = VLayout(advancedTab.view.subviews[:])
advancedLayout.packToCorner(Pack.UpperLeft)
advancedLayout.fill(Pack.Right)

View File

@ -0,0 +1,65 @@
ownerclass = 'PrioritizeDialog'
ownerimport = 'PrioritizeDialog.h'
result = Window(610, 400, "Re-Prioritize duplicates")
promptLabel = Label(result, "Add criteria to the right box and click OK to send the dupes that "
"correspond the best to these criteria to their respective group's reference position. Read "
"the help file for more information.")
split = SplitView(result, 2, vertical=True)
categoryPopup = Popup(split.subviews[0])
criteriaTable = ListView(split.subviews[0])
prioritizationTable = ListView(split.subviews[1])
addButton = Button(split.subviews[1], NLSTR("-->"))
removeButton = Button(split.subviews[1], NLSTR("<--"))
okButton = Button(result, "Ok")
cancelButton = Button(result, "Cancel")
owner.categoryPopUpView = categoryPopup
owner.criteriaTableView = criteriaTable
owner.prioritizationTableView = prioritizationTable
result.canMinimize = False
result.canClose = False
result.minSize = Size(result.width, result.height)
addButton.action = Action(owner.model, 'addSelected')
removeButton.action = Action(owner.model, 'removeSelected')
okButton.action = Action(owner, 'ok')
cancelButton.action = Action(owner, 'cancel')
okButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e'
# For layouts to correctly work, subviews need to have the dimensions they'll approximately have
# at runtime.
split.subviews[0].width = 260
split.subviews[0].height = 260
split.subviews[1].width = 340
split.subviews[1].height = 260
promptLabel.height *= 3 # 3 lines
leftLayout = VLayout([categoryPopup, criteriaTable], filler=criteriaTable)
middleLayout = VLayout([addButton, removeButton], width=41)
buttonLayout = HLayout([None, cancelButton, okButton])
#pack split subview 0
leftLayout.fillAll()
#pack split subview 1
prioritizationTable.fillAll()
prioritizationTable.width -= 48
prioritizationTable.moveTo(Pack.Right)
middleLayout.moveNextTo(prioritizationTable, Pack.Left, align=Pack.Middle)
# Main layout
promptLabel.packToCorner(Pack.UpperLeft)
promptLabel.fill(Pack.Right)
split.moveNextTo(promptLabel, Pack.Below)
buttonLayout.moveNextTo(split, Pack.Below)
buttonLayout.fill(Pack.Right)
split.fill(Pack.LowerRight)
promptLabel.setAnchor(Pack.UpperLeft, growX=True)
prioritizationTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
categoryPopup.setAnchor(Pack.UpperLeft, growX=True)
criteriaTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
split.setAnchor(Pack.UpperLeft, growX=True, growY=True)
buttonLayout.setAnchor(Pack.Below)

View File

@ -0,0 +1,35 @@
ownerclass = 'ProblemDialog'
ownerimport = 'ProblemDialog.h'
result = Window(480, 310, "Problems!")
messageLabel = Label(result, "There were problems processing some (or all) of the files. The cause "
"of these problems are described in the table below. Those files were not removed from your "
"results.")
problemTable = TableView(result)
revealButton = Button(result, "Reveal")
closeButton = Button(result, "Close")
owner.problemTableView = problemTable
result.canMinimize = False
result.minSize = Size(300, 300)
closeButton.keyEquivalent = '\\r'
revealButton.action = Action(owner.model, 'revealSelected')
closeButton.action = Action(result, 'performClose:')
messageLabel.height *= 3 # 3 lines
revealButton.width = 150
closeButton.width = 98
messageLabel.packToCorner(Pack.UpperLeft)
messageLabel.fill(Pack.Right)
problemTable.packRelativeTo(messageLabel, Pack.Below)
problemTable.fill(Pack.Right)
revealButton.packRelativeTo(problemTable, Pack.Below)
closeButton.packRelativeTo(problemTable, Pack.Below, align=Pack.Right)
problemTable.fill(Pack.Below)
messageLabel.setAnchor(Pack.UpperLeft, growX=True)
problemTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
revealButton.setAnchor(Pack.LowerLeft)
closeButton.setAnchor(Pack.LowerRight)

97
cocoa/ui/result_window.py Normal file
View File

@ -0,0 +1,97 @@
ownerclass = 'ResultWindow'
ownerimport = 'ResultWindow.h'
result = Window(557, 400, "dupeGuru Results")
toolbar = result.createToolbar('ResultsToolbar')
table = TableView(result)
table.OBJC_CLASS = 'HSTableView'
statsLabel = Label(result, "")
contextMenu = Menu("")
#Setup toolbar items
toolbar.displayMode = const.NSToolbarDisplayModeIconOnly
directoriesToolItem = toolbar.addItem('Directories', "Directories", image='folder32')
actionToolItem = toolbar.addItem('Action', "Action")
filterToolItem = toolbar.addItem('Filter', "Filter")
optionsToolItem = toolbar.addItem('Options', "Options")
quicklookToolItem = toolbar.addItem('QuickLook', "Quick Look")
toolbar.defaultItems = [actionToolItem, optionsToolItem, quicklookToolItem, directoriesToolItem,
toolbar.flexibleSpace(), filterToolItem]
actionPopup = Popup(None)
actionPopup.pullsdown = True
actionPopup.bezelStyle = const.NSTexturedRoundedBezelStyle
actionPopup.arrowPosition = const.NSPopUpArrowAtBottom
item = actionPopup.menu.addItem("") # First item is invisible
item.hidden = True
item.image = 'NSActionTemplate'
actionPopup.width = 44
actionToolItem.view = actionPopup
filterField = SearchField(None, "Filter")
filterField.action = Action(owner, 'filter')
filterField.sendsWholeSearchString = True
filterToolItem.view = filterField
filterToolItem.minSize = Size(80, 22)
filterToolItem.maxSize = Size(300, 22)
quickLookButton = Button(None, "")
quickLookButton.bezelStyle = const.NSTexturedRoundedBezelStyle
quickLookButton.image = 'NSQuickLookTemplate'
quickLookButton.width = 44
quickLookButton.action = Action(owner, 'toggleQuicklookPanel')
quicklookToolItem.view = quickLookButton
optionsSegments = SegmentedControl(None)
optionsSegments.segmentStyle = const.NSSegmentStyleCapsule
optionsSegments.trackingMode = const.NSSegmentSwitchTrackingSelectAny
optionsSegments.font = Font(FontFamily.System, 11)
optionsSegments.addSegment("Details", 57)
optionsSegments.addSegment("Dupes Only", 82)
optionsSegments.addSegment("Delta", 48)
optionsSegments.action = Action(owner, 'changeOptions')
optionsToolItem.view = optionsSegments
# Popuplate menus
actionPopup.menu.addItem("Send Marked to Trash...", action=Action(owner, 'trashMarked'))
actionPopup.menu.addItem("Move Marked to...", action=Action(owner, 'moveMarked'))
actionPopup.menu.addItem("Copy Marked to...", action=Action(owner, 'copyMarked'))
actionPopup.menu.addItem("Remove Marked from Results", action=Action(owner, 'removeMarked'))
actionPopup.menu.addSeparator()
for menu in (actionPopup.menu, contextMenu):
menu.addItem("Remove Selected from Results", action=Action(owner, 'removeSelected'))
menu.addItem("Add Selected to Ignore List", action=Action(owner, 'ignoreSelected'))
menu.addItem("Make Selected into Reference", action=Action(owner, 'switchSelected'))
menu.addSeparator()
menu.addItem("Open Selected with Default Application", action=Action(owner, 'openSelected'))
menu.addItem("Reveal Selected in Finder", action=Action(owner, 'revealSelected'))
menu.addItem("Rename Selected", action=Action(owner, 'renameSelected'))
# Doing connections
owner.filterField = filterField
owner.matches = table
owner.optionsSwitch = optionsSegments
owner.optionsToolbarItem = optionsToolItem
owner.stats = statsLabel
table.bind('rowHeight', defaults, 'values.TableFontSize', valueTransformer='vtRowHeightOffset')
# Rest of the setup
result.minSize = Size(340, 340)
result.autosaveName = 'MainWindow'
statsLabel.alignment = TextAlignment.Center
table.alternatingRows = True
table.menu = contextMenu
table.allowsColumnReordering = True
table.allowsColumnResizing = True
table.allowsColumnSelection = False
table.allowsEmptySelection = False
table.allowsMultipleSelection = True
table.allowsTypeSelect = True
table.gridStyleMask = const.NSTableViewSolidHorizontalGridLineMask
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)
statsLabel.setAnchor(Pack.LowerLeft, growX=True)
# Layout
# It's a little weird to pack with a margin of -1, but if I don't do that, I get too thick of a
# border on the upper side of the table.
table.packToCorner(Pack.UpperLeft, margin=-1)
table.fill(Pack.Right, margin=0)
statsLabel.packRelativeTo(table, Pack.Below, margin=6)
statsLabel.fill(Pack.Right, margin=0)
table.fill(Pack.Below, margin=5)

169
cocoa/waf vendored Executable file

File diff suppressed because one or more lines are too long

71
cocoa/wscript Normal file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env python
import sys
import os
import os.path as op
top = '.'
out = 'build'
def options(opt):
opt.load('compiler_c python')
def configure(conf):
# We use clang to compile our app
conf.env.CC = 'clang'
# WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking
# to it. The problem is that because we made a copy of the Python library to mangle with its
# "install name", we don't actually want to link to our installed python, but to our mangled
# Python. The line below tells the "pyembed" WAF feature to look in ../build for Python.
conf.env.LIBPATH_PYEMBED = op.abspath('../build')
# I did a lot of fiddling-around, but I didn't find how to tell WAF the Python library name
# to look for without making the whole compilation process fail, so I just create a symlink
# with the name WAF is looking for.
versioned_dylib_path = '../build/libpython{}m.dylib'.format(sys.version[:3])
if not op.exists(versioned_dylib_path):
os.symlink('../build/Python', versioned_dylib_path)
# The rest is standard WAF code that you can find the the python and macapp demos.
conf.load('compiler_c python')
conf.check_python_version((3,4,0))
conf.check_python_headers()
conf.env.FRAMEWORK_COCOA = 'Cocoa'
conf.env.ARCH_COCOA = ['x86_64']
conf.env.MACOSX_DEPLOYMENT_TARGET = '10.8'
def build(ctx):
# What do we compile?
cocoalib_node = ctx.srcnode.find_dir('..').find_dir('cocoalib')
cocoalib_folders = ['controllers', 'views']
cocoalib_includes = [cocoalib_node] + [cocoalib_node.find_dir(folder) for folder in cocoalib_folders]
cocoalib_uses = ['NSEventAdditions', 'Dialogs', 'HSAboutBox', 'Utils',
'HSPyUtil', 'ProgressController', 'HSRecentFiles', 'HSQuicklook', 'ValueTransformers',
'NSImageAdditions', 'NSNotificationAdditions',
'views/HSTableView', 'views/HSOutlineView', 'views/NSIndexPathAdditions',
'views/NSTableViewAdditions',
'controllers/HSColumns', 'controllers/HSGUIController', 'controllers/HSTable',
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
'controllers/HSTextField', 'controllers/HSProgressWindow']
cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m')
project_folders = [ctx.srcnode, ctx.srcnode.find_dir('autogen')]
project_src = ctx.srcnode.ant_glob('autogen/*.m') + ctx.srcnode.ant_glob('*.m')
# Compile
ctx.program(
# "pyembed" takes care of the include and linking stuff to compile an app that embed Python.
features = 'c cprogram pyembed',
target = ctx.bldnode.make_node("dupeGuru"),
source = cocoalib_src + project_src,
includes = project_folders + cocoalib_includes,
use = 'COCOA',
# Because our python lib's install name is "@rpath/Python", we need to set the executable's
# rpath. Fortunately, WAF supports it and we just need to supply the "rpath" argument.
rpath = '@executable_path/../Frameworks',
framework = ['Quartz'],
)
from waflib import TaskGen
@TaskGen.extension('.m')
def m_hook(self, node):
"""Alias .m files to be compiled the same as .c files, gcc will do the right thing."""
return self.create_compiled_task('c', node)

1
cocoalib Submodule

@ -0,0 +1 @@
Subproject commit d059aa9b7910f76174090ccd449fe6ab92bb43f0

1
dupeguru Submodule

@ -0,0 +1 @@
Subproject commit f51f94e03db3c36468bc40200679f098a0346a62

1
hscommon Submodule

@ -0,0 +1 @@
Subproject commit 5497c6fec44de6767a6488f540526d70218ef0da

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
-r dupeguru/requirements.txt
objp>=1.3.1
xibless>=0.4.1