1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-25 16:11:39 +00:00

Compare commits

..

3 Commits
4.0.2 ... rust

Author SHA1 Message Date
Virgil Dupras
f29fe4c756 Playing with Rust and Python 2015-04-20 22:32:22 -04:00
Virgil Dupras
942e5a3c31 Merge branch 'master' into rust 2015-04-20 18:58:01 -04:00
Virgil Dupras
6be927ed8b New rust branch 2014-10-31 17:36:16 -04:00
486 changed files with 48460 additions and 11930 deletions

5
.ctags
View File

@@ -1,5 +0,0 @@
-R
--exclude=build
--exclude=env
--exclude=.tox
--python-kinds=-i

10
.gitignore vendored
View File

@@ -1,21 +1,25 @@
.DS_Store .DS_Store
__pycache__ *.pyc
*.so *.so
*.mo *.mo
*.pyd
*.waf* *.waf*
.lock-waf* .lock-waf*
.idea
.tox .tox
/tags
build build
dist dist
install
installer_tmp-cache
env env
/deps /deps
cocoa/autogen cocoa/autogen
/run.py /run.py
/conf.json
/cocoa/*/Info.plist /cocoa/*/Info.plist
/cocoa/*/build /cocoa/*/build
/qt/*_rc.py /qt/base/*_rc.py
/help/*/conf.py /help/*/conf.py
/help/*/changelog.rst /help/*/changelog.rst

12
.gitmodules vendored
View File

@@ -1,12 +0,0 @@
[submodule "qtlib"]
path = qtlib
url = https://github.com/hsoft/qtlib.git
[submodule "hscommon"]
path = hscommon
url = https://github.com/hsoft/hscommon.git
[submodule "cocoalib"]
path = cocoalib
url = https://github.com/hsoft/cocoalib.git
[submodule "cocoa/Sparkle"]
path = cocoa/Sparkle
url = https://github.com/sparkle-project/Sparkle.git

19
CREDITS
View File

@@ -1,19 +0,0 @@
To know who contributed to dupeGuru, you can look at the commit log, but not all contributions
result in a commit. This file lists contributors who don't necessarily appear in the commit log.
* Jérôme Cantin, Main icon
* Gregor Tätzner, German localization
* Frank Weber, German localization
* Eric Dee, Chinese localization
* Aleš Nehyba, Czech localization
* Paolo Rossi, Italian localization
* Hrant Ohanyan, Armenian localization
* Igor Pavlov, Russian localization
* Kyrill Detinov, Russian localization
* Yuri Petrashko, Ukrainian localization
* Nickolas Pohilets, Ukrainian localization
* Victor Figueiredo, Brazilian localization
* Phan Anh, Vietnamese localization
* Gabriel Koutilellis, Greek localization
Thanks!

111
Makefile
View File

@@ -1,111 +0,0 @@
PYTHON ?= python3
REQ_MINOR_VERSION = 4
PREFIX ?= /usr/local
# 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
packages = hscommon qtlib core qt
localedirs = $(wildcard locale/*/LC_MESSAGES)
pofiles = $(wildcard locale/*/LC_MESSAGES/*.po)
mofiles = $(patsubst %.po,%.mo,$(pofiles))
vpath %.po $(localedirs)
vpath %.mo $(localedirs)
all : | run.py
@echo "Build complete! You can run dupeGuru with 'make run'"
run.py : | env i18n modules qt/dg_rc.py
cp qt/run_template.py run.py
run: | run.py
./env/bin/python run.py
pyc:
${PYTHON} -m compileall ${packages}
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. If you're on Ubuntu, you probably need python3-venv."
@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \
{ echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
# Ensure that submodules are initialized
$(submodules_target) :
git submodule init
git submodule update
env : | $(submodules_target) reqs
@echo "Creating our virtualenv"
${PYTHON} -m venv env --system-site-packages
./env/bin/python -m pip install -r requirements.txt
build/help : | env
./env/bin/python build.py --doc
qt/dg_rc.py : qt/dg.qrc
pyrcc5 qt/dg.qrc > qt/dg_rc.py
i18n: $(mofiles)
%.mo : %.po
msgfmt -o $@ $<
core/pe/_block.*.so : core/pe/modules/block.c core/pe/modules/common.c | env
./env/bin/python hscommon/build_ext.py $^ _block
mv _block.*.so core/pe
core/pe/_cache.*.so : core/pe/modules/cache.c core/pe/modules/common.c | env
./env/bin/python hscommon/build_ext.py $^ _cache
mv _cache.*.so core/pe
qt/pe/_block_qt.*.so : qt/pe/modules/block.c | env
./env/bin/python hscommon/build_ext.py $^ _block_qt
mv _block_qt.*.so qt/pe
modules : core/pe/_block.*.so core/pe/_cache.*.so qt/pe/_block_qt.*.so
mergepot :
./env/bin/python build.py --mergepot
normpo :
./env/bin/python build.py --normpo
srcpkg :
./scripts/srcpkg.sh
install: build/help | all pyc
mkdir -p ${DESTDIR}${PREFIX}/share/dupeguru
cp -rf ${packages} locale build/help ${DESTDIR}${PREFIX}/share/dupeguru
cp -f run.py ${DESTDIR}${PREFIX}/share/dupeguru/run.py
chmod 755 ${DESTDIR}${PREFIX}/share/dupeguru/run.py
mkdir -p ${DESTDIR}${PREFIX}/bin
ln -sf ${PREFIX}/share/dupeguru/run.py ${DESTDIR}${PREFIX}/bin/dupeguru
mkdir -p ${DESTDIR}${PREFIX}/share/applications
cp -f pkg/dupeguru.desktop ${DESTDIR}${PREFIX}/share/applications
mkdir -p ${DESTDIR}${PREFIX}/share/pixmaps
cp -f images/dgse_logo_128.png ${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png
uninstall :
rm -rf "${DESTDIR}${PREFIX}/share/dupeguru"
rm -f "${DESTDIR}${PREFIX}/bin/dupeguru"
rm -f "${DESTDIR}${PREFIX}/share/applications/dupeguru.desktop"
rm -f "${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png"
clean:
-rm run.py
-rm -rf build
-rm locale/*/LC_MESSAGES/*.mo
-rm core/pe/*.so qt/pe/*.so
.PHONY : clean srcpkg normpo mergepot modules i18n reqs run pyc install uninstall all

144
README.md
View File

@@ -1,11 +1,21 @@
# About the "rust" branch
I'm really excited about Rust and I'm trying out a Rust implementation of dupeGuru's bottlenecks
(PE's image comparison).
For fun.
# dupeGuru # dupeGuru
[dupeGuru][dupeguru] is a cross-platform (Linux and OS X) GUI tool to find duplicate files in [dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in
a system. It's written mostly in Python 3 and has the peculiarity of using a system. It's written mostly in Python 3 and has the peculiarity of using
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer [multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
is written in Objective-C and uses Cocoa. On Linux, it's written in Python and uses Qt5. is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt5.
## Current status: People wanted dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same
source tree. You choose the edition you want to build in a `configure.py` flag.
# Current status: People wanted
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
corrected. corrected.
@@ -17,39 +27,13 @@ Whatever your skills, if you are remotely interestested in being a contributor,
mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue] mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue]
and let's get started. and let's get started.
### Slowed development # Contents of this folder
Until I manage to find contributors, I'm slowing the development pace of dupeGuru. I'm not much
interested in maintaining it alone, I personally have no use for this app (it's been a *loooong*,
time since I had dupe problems :) )
I don't want to let it die, however, so I will still do normal maintainership, that is, issue
triaging, code review, critical bugfixes, releases management.
But anything non-critical, I'm not going to implement it myself because I see every issue as a
contribution opportunity.
### Windows maintainer wanted
As [described on my website][nowindows], dupeGuru v4.0 dropped Windows support
because there isn't anyone to bear the burden of Windows maintenance. If
you're a Windows developer and are interested in taking this task, [don't
hesitate to let me know][contrib-issue].
### 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].
## Contents of this folder
This folder contains the source for dupeGuru. Its documentation is in `help`, but is also This folder contains the source for dupeGuru. Its documentation is in `help`, but is also
[available online][documentation] in its built form. Here's how this source tree is organised: [available online][documentation] in its built form. Here's how this source tree is organised:
* core: Contains the core logic code for dupeGuru. It's Python code. * core: Contains the core logic code for dupeGuru. It's Python code.
* core_*: Edition-specific-cross-toolkit code written in Python.
* cocoa: UI code for the Cocoa toolkit. It's Objective-C code. * cocoa: UI code for the Cocoa toolkit. It's Objective-C code.
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt. * qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
* images: Images used by the different UI codebases. * images: Images used by the different UI codebases.
@@ -58,114 +42,78 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu
* locale: .po files for localisation. * locale: .po files for localisation.
There are also other sub-folder that comes from external repositories and are part of this repo as There are also other sub-folder that comes from external repositories and are part of this repo as
git submodules: git subtrees:
* Sparkle: An auto-update library for the OS X version.
* hscommon: A collection of helpers used across HS applications. * hscommon: A collection of helpers used across HS applications.
* cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications. * cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications.
* qtlib: A collection of helpers used across Qt UI codebases of HS applications. * qtlib: A collection of helpers used across Qt UI codebases of HS applications.
## How to build dupeGuru from source # How to build dupeGuru from source
### make ## The very, very, very easy way
If you're on linux, you can build the ap for local development with `make`: If you're on Linux or Mac, there's a bootstrap script that will make building very, very easy. There
might be some things that you need to install manually on your system, but the bootstrap script will
tell you when what you need to install. You can run the bootstrap with:
$ make ./bootstrap.sh
$ make run
The `Makefile` is a recent addition, however. You might have to fallback to the legacy build and follow instructions from the script. You can then ignore the rest of the build documentation.
scripts.
### Legacy build ## Prerequisites installation
If you're on OS X or that if the `make` method didn't work, you can build dupeGuru with the
legacy scripts.
There's a bootstrap script that will make building very easy. There might be some things that you
need to install manually on your system, but the bootstrap script will tell you when what you need
to install. You can run the bootstrap with:
$ ./bootstrap.sh
and follow instructions from the script.
### Prerequisites installation
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
to be installed manually. to be installed manually.
* All systems: [Python 3.4+][python] * All systems: [Python 3.3+][python] and [setuptools][setuptools]
* Mac OS X: OS X 10.10+ with XCode command line tools. * Mac OS X: The last XCode to have the 10.7 SDK included. Python 3.4+.
* Linux: PyQt5 * Windows: Visual Studio 2010, [PyQt 5.0+][pyqt], [cx_Freeze][cxfreeze] and
[Advanced Installer][advinst] (you only need the last two if you want to create an installer)
On Ubuntu (14.04+), the apt-get command to install all pre-requisites is: On Ubuntu (14.04+), the apt-get command to install all pre-requisites is:
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools python3-venv $ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools
### OS X and pyenv On Arch, it's:
[pyenv][pyenv] is a popular way to manage multiple python versions. However, be aware that dupeGuru $ pacman -S python-pyqt5
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 ## Setting up the virtual environment
### Setting up the virtual environment
*This is done automatically by the bootstrap script. This is a reference in case you need to do it
manually.*
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our. Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
Python-related dependencies. In that environment, we then install our requirements with pip. Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
it doesn't install setuptools and pip (unless you use Python 3.4+), so it has to be installed
For Linux (`--system-site-packages` is to be able to import PyQt): manually:
$ pyvenv --system-site-packages env $ pyvenv --system-site-packages env
$ source env/bin/activate $ source env/bin/activate
$ pip install -r requirements.txt $ python get-pip.py
For OS X: Then, you can install pip requirements in your virtualenv:
$ pyvenv env $ pip install -r requirements-[osx|win].txt
$ source env/bin/activate
$ pip install -r requirements-osx.txt ([osx|win] depends, of course, on your platform. On other platforms, just use requirements.txt).
### Actual building and running ## Actual building and running
With your virtualenv activated, you can build and run dupeGuru with these commands: With your virtualenv activated, you can build and run dupeGuru with these commands:
$ python configure.py
$ python build.py $ python build.py
$ python run.py $ python run.py
You can also package dupeGuru into an installable package with: You can also package dupeGuru into an installable package with:
$ python package.py $ python package.py
### Generate Ubuntu packages
$ bash -c "pyvenv --system-site-packages env && source env/bin/activate && pip install -r requirements.txt && python3 build.py --clean && python3 package.py"
### Running tests
The complete test suite is ran with [Tox 1.7+][tox]. If you have it installed system-wide, you
don't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`.
If you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then
run `tox`.
You can also run automated tests without Tox. Extra requirements for running tests are in
`requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your
virtualenv and then `py.test core hscommon`
[dupeguru]: http://www.hardcoded.net/dupeguru/ [dupeguru]: http://www.hardcoded.net/dupeguru/
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software [cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300 [contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
[nowindows]: https://www.hardcoded.net/archive2015#2015-11-01
[documentation]: http://www.hardcoded.net/dupeguru/help/en/ [documentation]: http://www.hardcoded.net/dupeguru/help/en/
[python]: http://www.python.org/ [python]: http://www.python.org/
[setuptools]: https://pypi.python.org/pypi/setuptools
[pyqt]: http://www.riverbankcomputing.com [pyqt]: http://www.riverbankcomputing.com
[pyenv]: https://github.com/yyuu/pyenv [cxfreeze]: http://cx-freeze.sourceforge.net/
[tox]: https://tox.readthedocs.org/en/latest/ [advinst]: http://www.advancedinstaller.com

View File

@@ -1,16 +1,12 @@
#!/bin/bash #!/bin/bash
PYTHON=python3 PYTHON=python3
ret=`$PYTHON -c "import sys; print(int(sys.version_info[:2] >= (3, 4)))"` command -v $PYTHON -m venv >/dev/null 2>&1 || { echo >&2 "Python 3.3 required. Install it and try again. Aborting"; exit 1; }
if [ $ret -ne 1 ]; then
echo "Python 3.4+ required. Aborting."
exit 1
fi
if [ -d "deps" ]; then
if [ -d ".git" ]; then # We have a collection of dependencies in our source package. We might as well use it instead
git submodule init # of downloading it from PyPI.
git submodule update PIPARGS="--no-index --find-links=deps"
fi fi
if [ ! -d "env" ]; then if [ ! -d "env" ]; then
@@ -19,11 +15,27 @@ if [ ! -d "env" ]; then
# install. To achieve our latter goal, we start with a normal venv, which we later upgrade to # install. To achieve our latter goal, we start with a normal venv, which we later upgrade to
# a system-site-packages once pip is installed. # a system-site-packages once pip is installed.
if ! $PYTHON -m venv env ; then if ! $PYTHON -m venv env ; then
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv." # We're probably under braindead Ubuntu 14.04 which completely messed up ensurepip.
exit 1 # Work around it :(
echo "Ubuntu 14.04's version of Python 3.4 is braindead stupid, but we work around it anyway..."
$PYTHON -m venv --without-pip env
fi fi
source env/bin/activate
if python -m ensurepip; then
echo "We're under Python 3.4+, no need to try to install pip!"
else
python get-pip.py $PIPARGS --force-reinstall
fi
deactivate
if [ "$(uname)" != "Darwin" ]; then if [ "$(uname)" != "Darwin" ]; then
$PYTHON -m venv env --upgrade --system-site-packages # We only need system site packages for PyQt, so under OS X, we don't enable it
if ! $PYTHON -m venv env --upgrade --system-site-packages ; then
# We're probably under v3.4.1 and experiencing http://bugs.python.org/issue21643
# Work around it.
echo "Oops, can't upgrade our venv. Trying to work around it."
rm env/lib64
$PYTHON -m venv env --upgrade --system-site-packages
fi
fi fi
fi fi
@@ -31,11 +43,11 @@ source env/bin/activate
echo "Installing pip requirements" echo "Installing pip requirements"
if [ "$(uname)" == "Darwin" ]; then if [ "$(uname)" == "Darwin" ]; then
./env/bin/pip install -r requirements-osx.txt pip install $PIPARGS -r requirements-osx.txt
else else
./env/bin/python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; } python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.1+ required. Install it and try again. Aborting"; exit 1; }
./env/bin/pip install -r requirements.txt pip install $PIPARGS -r requirements.txt
fi fi
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:" echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
echo ". env/bin/activate && python build.py && python run.py" echo ". env/bin/activate && python configure.py && python build.py && python run.py"

209
build.py
View File

@@ -1,4 +1,6 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # Created By: Virgil Dupras
# Created On: 2009-12-30
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# #
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
@@ -9,6 +11,8 @@ import os
import os.path as op import os.path as op
from optparse import OptionParser from optparse import OptionParser
import shutil import shutil
import json
import importlib
import compileall import compileall
from setuptools import setup, Extension from setuptools import setup, Extension
@@ -18,10 +22,10 @@ from hscommon.build import (
add_to_pythonpath, print_and_do, copy_packages, filereplace, add_to_pythonpath, print_and_do, copy_packages, filereplace,
get_module_version, move_all, copy_all, OSXAppStructure, get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib, build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies collect_stdlib_dependencies, copy
) )
from hscommon import loc from hscommon import loc
from hscommon.plat import ISOSX from hscommon.plat import ISOSX, ISLINUX
from hscommon.util import ensure_folder, delete_files_with_pattern from hscommon.util import ensure_folder, delete_files_with_pattern
def parse_args(): def parse_args():
@@ -35,14 +39,6 @@ def parse_args():
'--doc', action='store_true', dest='doc', '--doc', action='store_true', dest='doc',
help="Build only the help file" help="Build only the help file"
) )
parser.add_option(
'--ui', dest='ui',
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system."
)
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( parser.add_option(
'--loc', action='store_true', dest='loc', '--loc', action='store_true', dest='loc',
help="Build only localization" help="Build only localization"
@@ -74,11 +70,18 @@ def parse_args():
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
return options return options
def cocoa_app(): def cocoa_compile_command(edition):
app_path = 'build/dupeGuru.app' return '{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition)
def cocoa_app(edition):
app_path = {
'se': 'build/dupeGuru.app',
'me': 'build/dupeGuru ME.app',
'pe': 'build/dupeGuru PE.app',
}[edition]
return OSXAppStructure(app_path) return OSXAppStructure(app_path)
def build_xibless(dest='cocoa/autogen'): def build_xibless(edition, dest='cocoa/autogen'):
import xibless import xibless
ensure_folder(dest) ensure_folder(dest)
FNPAIRS = [ FNPAIRS = [
@@ -89,57 +92,62 @@ def build_xibless(dest='cocoa/autogen'):
('prioritize_dialog.py', 'PrioritizeDialog_UI'), ('prioritize_dialog.py', 'PrioritizeDialog_UI'),
('result_window.py', 'ResultWindow_UI'), ('result_window.py', 'ResultWindow_UI'),
('main_menu.py', 'MainMenu_UI'), ('main_menu.py', 'MainMenu_UI'),
('details_panel.py', 'DetailsPanel_UI'), ('preferences_panel.py', 'PreferencesPanel_UI'),
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
] ]
for srcname, dstname in FNPAIRS: for srcname, dstname in FNPAIRS:
xibless.generate( xibless.generate(
op.join('cocoa', 'ui', srcname), op.join(dest, dstname), op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
localizationTable='Localizable', args={'edition': edition}
)
if edition == 'pe':
xibless.generate(
'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
localizationTable='Localizable' localizationTable='Localizable'
) )
for appmode in ('standard', 'music', 'picture'): else:
xibless.generate( xibless.generate(
op.join('cocoa', 'ui', 'preferences_panel.py'), 'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()), localizationTable='Localizable'
localizationTable='Localizable',
args={'appmode': appmode},
) )
def build_cocoa(dev): def build_cocoa(edition, dev):
sparkle_framework_path = op.join('cocoa', 'Sparkle', 'build', 'Release', 'Sparkle.framework')
if not op.exists(sparkle_framework_path):
print("Building Sparkle")
os.chdir(op.join('cocoa', 'Sparkle'))
print_and_do('make build')
os.chdir(op.join('..', '..'))
print("Creating OS X app structure") print("Creating OS X app structure")
app = cocoa_app() ed = lambda s: s.format(edition)
app_version = get_module_version('core') app = cocoa_app(edition)
cocoa_project_path = 'cocoa' app_version = get_module_version(ed('core_{}'))
cocoa_project_path = ed('cocoa/{}')
filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version) filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version)
app.create(op.join('build', 'Info.plist')) app.create(op.join('build', 'Info.plist'))
print("Building localizations") print("Building localizations")
build_localizations('cocoa') build_localizations('cocoa', edition)
print("Building xibless UIs") print("Building xibless UIs")
build_cocoalib_xibless() build_cocoalib_xibless()
build_xibless() build_xibless(edition)
print("Building Python extensions") print("Building Python extensions")
build_cocoa_proxy_module() build_cocoa_proxy_module()
build_cocoa_bridging_interfaces() build_cocoa_bridging_interfaces(edition)
print("Building the cocoa layer") print("Building the cocoa layer")
copy_embeddable_python_dylib('build') copy_embeddable_python_dylib('build')
pydep_folder = op.join(app.resources, 'py') pydep_folder = op.join(app.resources, 'py')
if not op.exists(pydep_folder): if not op.exists(pydep_folder):
os.mkdir(pydep_folder) os.mkdir(pydep_folder)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build') shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
appscript_pkgs = ['appscript', 'aem', 'mactypes', 'osax']
specific_packages = {
'se': ['core_se'],
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
'pe': ['core_pe'] + appscript_pkgs,
}[edition]
tocopy = [ tocopy = [
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag', 'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
] ] + specific_packages
copy_packages(tocopy, pydep_folder, create_links=dev) copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build') sys.path.insert(0, 'build')
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have extra_deps = None
# to manually specify it. if edition == 'pe':
extra_deps = ['multiprocessing'] # 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) collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
del sys.path[0] del sys.path[0]
# Views are not referenced by python code, so they're not found by the collector. # Views are not referenced by python code, so they're not found by the collector.
@@ -152,38 +160,40 @@ def build_cocoa(dev):
delete_files_with_pattern(pydep_folder, '__pycache__') delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF") print("Compiling with WAF")
os.chdir('cocoa') os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable)) print_and_do(cocoa_compile_command(edition))
os.chdir('..') os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru') app.copy_executable('cocoa/build/dupeGuru')
build_help() build_help(edition)
print("Copying resources and frameworks") print("Copying resources and frameworks")
image_path = 'cocoa/dupeguru.icns' image_path = ed('cocoa/{}/dupeguru.icns')
resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help'] resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev) app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python', sparkle_framework_path) app.copy_frameworks('build/Python', 'cocoalib/Sparkle.framework')
print("Creating the run.py file") print("Creating the run.py file")
tmpl = open('cocoa/run_template.py', 'rt').read() tmpl = open('cocoa/run_template.py', 'rt').read()
run_contents = tmpl.replace('{{app_path}}', app.dest) run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents) open('run.py', 'wt').write(run_contents)
def build_qt(dev): def build_qt(edition, dev, conf):
print("Building localizations") print("Building localizations")
build_localizations('qt') build_localizations('qt', edition)
print("Building Qt stuff") print("Building Qt stuff")
print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'dg.qrc'), op.join('qt', 'dg_rc.py'))) print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
fix_qt_resource_file(op.join('qt', 'dg_rc.py')) fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py'))
build_help() build_help(edition)
print("Creating the run.py file") print("Creating the run.py file")
shutil.copy(op.join('qt', 'run_template.py'), 'run.py') filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition)
def build_help(): def build_help(edition):
print("Generating Help") print("Generating Help")
current_path = op.abspath('.') current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en') help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help') help_destpath = op.join(current_path, 'build', 'help'.format(edition))
changelog_path = op.join(current_path, 'help', 'changelog') changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition))
tixurl = "https://github.com/hsoft/dupeguru/issues/{}" tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
confrepl = {'language': 'en'} appname = {'se': 'dupeGuru', 'me': 'dupeGuru Music Edition', 'pe': 'dupeGuru Picture Edition'}[edition]
homepage = 'http://www.hardcoded.net/dupeguru{}/'.format('_' + edition if edition != 'se' else '')
confrepl = {'edition': edition, 'appname': appname, 'homepage': homepage, 'language': 'en'}
changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl') changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl')
conftmpl = op.join(current_path, 'help', 'conf.tmpl') conftmpl = op.join(current_path, 'help', 'conf.tmpl')
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl) sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
@@ -192,11 +202,11 @@ def build_qt_localizations():
loc.compile_all_po(op.join('qtlib', 'locale')) loc.compile_all_po(op.join('qtlib', 'locale'))
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale') loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
def build_localizations(ui): def build_localizations(ui, edition):
loc.compile_all_po('locale') loc.compile_all_po('locale')
if ui == 'cocoa': if ui == 'cocoa':
app = cocoa_app() app = cocoa_app(edition)
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings')) loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale') locale_dest = op.join(app.resources, 'locale')
elif ui == 'qt': elif ui == 'qt':
build_qt_localizations() build_qt_localizations()
@@ -204,19 +214,32 @@ def build_localizations(ui):
if op.exists(locale_dest): if op.exists(locale_dest):
shutil.rmtree(locale_dest) shutil.rmtree(locale_dest)
shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot')) shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot'))
if ui == 'qt' and not ISLINUX:
print("Copying qt_*.qm files into the 'locale' folder")
from PyQt5.QtCore import QLibraryInfo
trfolder = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
for lang in loc.get_langs('locale'):
qmname = 'qt_%s.qm' % lang
src = op.join(trfolder, qmname)
if op.exists(src):
copy(src, op.join('build', 'locale', qmname))
def build_updatepot(): def build_updatepot():
if ISOSX: if ISOSX:
print("Updating Cocoa strings file.") print("Updating Cocoa strings file.")
# We need to have strings from *all* editions in here, so we'll call xibless for all editions
# in dummy subfolders.
build_cocoalib_xibless('cocoalib/autogen') build_cocoalib_xibless('cocoalib/autogen')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj') loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
build_xibless() for edition in ('se', 'me', 'pe'):
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj') build_xibless(edition, op.join('cocoa', 'autogen', edition))
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/base/en.lproj')
print("Building .pot files from source files") print("Building .pot files from source files")
print("Building core.pot") print("Building core.pot")
loc.generate_pot(['core'], op.join('locale', 'core.pot'), ['tr']) all_cores = ['core', 'core_se', 'core_me', 'core_pe']
loc.generate_pot(all_cores, op.join('locale', 'core.pot'), ['tr'])
print("Building columns.pot") print("Building columns.pot")
loc.generate_pot(['core'], op.join('locale', 'columns.pot'), ['coltr']) loc.generate_pot(all_cores, op.join('locale', 'columns.pot'), ['coltr'])
print("Building ui.pot") print("Building ui.pot")
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs # When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
# We want to merge the generated pot with the old pot in the most preserving way possible. # We want to merge the generated pot with the old pot in the most preserving way possible.
@@ -231,7 +254,7 @@ def build_updatepot():
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot) loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
print("Enhancing ui.pot with Cocoa's strings files") print("Enhancing ui.pot with Cocoa's strings files")
loc.strings2pot( loc.strings2pot(
op.join('cocoa', 'en.lproj', 'Localizable.strings'), op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
op.join('locale', 'ui.pot') op.join('locale', 'ui.pot')
) )
@@ -260,7 +283,7 @@ def build_cocoa_proxy_module():
['cocoalib', 'cocoa/autogen'] ['cocoalib', 'cocoa/autogen']
) )
def build_cocoa_bridging_interfaces(): def build_cocoa_bridging_interfaces(edition):
print("Building Cocoa Bridging Interfaces") print("Building Cocoa Bridging Interfaces")
import objp.o2p import objp.o2p
import objp.p2o import objp.p2o
@@ -280,12 +303,13 @@ def build_cocoa_bridging_interfaces():
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
from inter.result_table import PyResultTable, ResultTableView from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuru, DupeGuruView from inter.app import PyDupeGuruBase, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition))
allclasses = [ allclasses = [
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog, PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru, PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
PyTextField, PyProgressWindow PyTextField, PyProgressWindow, appmod.PyDupeGuru
] ]
for class_ in allclasses: for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True) objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
@@ -302,21 +326,14 @@ def build_cocoa_bridging_interfaces():
def build_pe_modules(ui): def build_pe_modules(ui):
print("Building PE Modules") print("Building PE Modules")
exts = [ exts = [
Extension( Extension("_block", [op.join('core_pe', 'modules', 'block.c'), op.join('core_pe', 'modules', 'common.c')]),
"_block", Extension("_cache", [op.join('core_pe', 'modules', 'cache.c'), op.join('core_pe', 'modules', 'common.c')]),
[op.join('core', 'pe', 'modules', 'block.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
Extension(
"_cache",
[op.join('core', 'pe', 'modules', 'cache.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
] ]
if ui == 'qt': if ui == 'qt':
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')])) exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
elif ui == 'cocoa': elif ui == 'cocoa':
exts.append(Extension( exts.append(Extension(
"_block_osx", "_block_osx", [op.join('core_pe', 'modules', 'block_osx.m'), op.join('core_pe', 'modules', 'common.c')],
[op.join('core', 'pe', 'modules', 'block_osx.m'), op.join('core', 'pe', 'modules', 'common.c')],
extra_link_args=[ extra_link_args=[
"-framework", "CoreFoundation", "-framework", "CoreFoundation",
"-framework", "Foundation", "-framework", "Foundation",
@@ -328,25 +345,27 @@ def build_pe_modules(ui):
ext_modules=exts, ext_modules=exts,
) )
move_all('_block_qt*', op.join('qt', 'pe')) move_all('_block_qt*', op.join('qt', 'pe'))
move_all('_block*', op.join('core', 'pe')) move_all('_block*', 'core_pe')
move_all('_cache*', op.join('core', 'pe')) move_all('_cache*', 'core_pe')
def build_normal(ui, dev): def build_normal(edition, ui, dev, conf):
print("Building dupeGuru with UI {}".format(ui)) print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
add_to_pythonpath('.') add_to_pythonpath('.')
print("Building dupeGuru") print("Building dupeGuru")
build_pe_modules(ui) if edition == 'pe':
build_pe_modules(ui)
if ui == 'cocoa': if ui == 'cocoa':
build_cocoa(dev) build_cocoa(edition, dev)
elif ui == 'qt': elif ui == 'qt':
build_qt(dev) build_qt(edition, dev, conf)
def main(): def main():
options = parse_args() options = parse_args()
ui = options.ui conf = json.load(open('conf.json'))
if ui not in ('cocoa', 'qt'): edition = conf['edition']
ui = 'cocoa' if ISOSX else 'qt' ui = conf['ui']
if options.dev: dev = conf['dev']
if dev:
print("Building in Dev mode") print("Building in Dev mode")
if options.clean: if options.clean:
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]: for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
@@ -355,9 +374,9 @@ def main():
if not op.exists('build'): if not op.exists('build'):
os.mkdir('build') os.mkdir('build')
if options.doc: if options.doc:
build_help() build_help(edition)
elif options.loc: elif options.loc:
build_localizations(ui) build_localizations(ui, edition)
elif options.updatepot: elif options.updatepot:
build_updatepot() build_updatepot()
elif options.mergepot: elif options.mergepot:
@@ -366,17 +385,17 @@ def main():
build_normpo() build_normpo()
elif options.cocoa_ext: elif options.cocoa_ext:
build_cocoa_proxy_module() build_cocoa_proxy_module()
build_cocoa_bridging_interfaces() build_cocoa_bridging_interfaces(edition)
elif options.cocoa_compile: elif options.cocoa_compile:
os.chdir('cocoa') os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable)) print_and_do(cocoa_compile_command(edition))
os.chdir('..') os.chdir('..')
cocoa_app().copy_executable('cocoa/build/dupeGuru') cocoa_app(edition).copy_executable('cocoa/build/dupeGuru')
elif options.xibless: elif options.xibless:
build_cocoalib_xibless() build_cocoalib_xibless()
build_xibless() build_xibless(edition)
else: else:
build_normal(ui, options.dev) build_normal(edition, ui, dev, conf)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

Submodule cocoa/Sparkle deleted from 1c8d54166b

View File

@@ -10,29 +10,24 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import <Sparkle/SUUpdater.h> #import <Sparkle/SUUpdater.h>
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
#import "ResultWindow.h" #import "ResultWindow.h"
#import "ResultTable.h"
#import "DetailsPanel.h" #import "DetailsPanel.h"
#import "DirectoryPanel.h" #import "DirectoryPanel.h"
#import "IgnoreListDialog.h" #import "IgnoreListDialog.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSAboutBox.h" #import "HSAboutBox.h"
#import "HSRecentFiles.h" #import "HSRecentFiles.h"
#import "HSProgressWindow.h" #import "HSProgressWindow.h"
@interface AppDelegate : NSObject <NSFileManagerDelegate> @interface AppDelegateBase : NSObject
{ {
NSMenu *recentResultsMenu; NSMenu *recentResultsMenu;
NSMenu *columnsMenu; NSMenu *columnsMenu;
SUUpdater *updater; SUUpdater *updater;
PyDupeGuru *model; PyDupeGuru *model;
ResultWindow *_resultWindow; ResultWindowBase *_resultWindow;
DirectoryPanel *_directoryPanel; DirectoryPanel *_directoryPanel;
DetailsPanel *_detailsPanel; DetailsPanel *_detailsPanel;
IgnoreListDialog *_ignoreListDialog; IgnoreListDialog *_ignoreListDialog;
ProblemDialog *_problemDialog;
DeletionOptions *_deletionOptions;
HSProgressWindow *_progressWindow; HSProgressWindow *_progressWindow;
NSWindowController *_preferencesPanel; NSWindowController *_preferencesPanel;
HSAboutBox *_aboutBox; HSAboutBox *_aboutBox;
@@ -46,17 +41,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
/* Virtual */ /* Virtual */
+ (NSDictionary *)defaultPreferences; + (NSDictionary *)defaultPreferences;
- (PyDupeGuru *)model; - (PyDupeGuru *)model;
- (ResultWindowBase *)createResultWindow;
- (DirectoryPanel *)createDirectoryPanel;
- (DetailsPanel *)createDetailsPanel; - (DetailsPanel *)createDetailsPanel;
- (void)setScanOptions; - (NSString *)homepageURL;
/* Public */ /* Public */
- (void)finalizeInit; - (void)finalizeInit;
- (ResultWindow *)resultWindow; - (ResultWindowBase *)resultWindow;
- (DirectoryPanel *)directoryPanel; - (DirectoryPanel *)directoryPanel;
- (DetailsPanel *)detailsPanel; - (DetailsPanel *)detailsPanel;
- (HSRecentFiles *)recentResults; - (HSRecentFiles *)recentResults;
- (NSInteger)getAppMode;
- (void)setAppMode:(NSInteger)appMode;
/* Delegate */ /* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
@@ -66,7 +61,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)recentFileClicked:(NSString *)path; - (void)recentFileClicked:(NSString *)path;
/* Actions */ /* Actions */
- (void)clearPictureCache;
- (void)loadResults; - (void)loadResults;
- (void)openWebsite; - (void)openWebsite;
- (void)openHelp; - (void)openHelp;

View File

@@ -6,19 +6,16 @@ which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "AppDelegate.h" #import "AppDelegateBase.h"
#import "ProgressController.h" #import "ProgressController.h"
#import "HSPyUtil.h" #import "HSPyUtil.h"
#import "Consts.h" #import "Consts.h"
#import "Dialogs.h" #import "Dialogs.h"
#import "Utils.h" #import "Utils.h"
#import "ValueTransformers.h" #import "ValueTransformers.h"
#import "DetailsPanelPicture.h" #import "PreferencesPanel_UI.h"
#import "PreferencesPanelStandard_UI.h"
#import "PreferencesPanelMusic_UI.h"
#import "PreferencesPanelPicture_UI.h"
@implementation AppDelegate @implementation AppDelegateBase
@synthesize recentResultsMenu; @synthesize recentResultsMenu;
@synthesize columnsMenu; @synthesize columnsMenu;
@@ -27,21 +24,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
+ (NSDictionary *)defaultPreferences + (NSDictionary *)defaultPreferences
{ {
NSMutableDictionary *d = [NSMutableDictionary dictionary]; 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(1) forKey:@"recreatePathType"];
[d setObject:i2n(11) forKey:TableFontSize]; [d setObject:i2n(11) forKey:TableFontSize];
[d setObject:b2n(YES) forKey:@"mixFileKind"]; [d setObject:b2n(YES) forKey:@"mixFileKind"];
@@ -71,19 +53,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
model = [[PyDupeGuru alloc] init]; model = [[PyDupeGuru alloc] init];
[model bindCallback:createCallback(@"DupeGuruView", self)]; [model bindCallback:createCallback(@"DupeGuruView", self)];
[self setUpdater:[SUUpdater sharedUpdater]]; [self setUpdater:[SUUpdater sharedUpdater]];
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; return self;
} }
@@ -100,17 +69,14 @@ http://www.gnu.org/licenses/gpl-3.0.html
} }
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu]; _recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self]; [_recentResults setDelegate:self];
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self]; _resultWindow = [self createResultWindow];
_directoryPanel = [self createDirectoryPanel];
_detailsPanel = [self createDetailsPanel];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]]; _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 = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_directoryPanel window]]; [_progressWindow setParentWindow:[_resultWindow window]];
// Lazily loaded _aboutBox = nil; // Lazily loaded
_aboutBox = nil; _preferencesPanel = nil; // Lazily loaded
_preferencesPanel = nil;
_resultWindow = nil;
_detailsPanel = nil;
[[[self directoryPanel] window] makeKeyAndOrderFront:self]; [[[self directoryPanel] window] makeKeyAndOrderFront:self];
} }
@@ -121,51 +87,28 @@ http://www.gnu.org/licenses/gpl-3.0.html
return model; return model;
} }
- (DetailsPanel *)createDetailsPanel - (ResultWindowBase *)createResultWindow
{ {
NSInteger appMode = [self getAppMode]; return nil; // must be overriden by all editions
if (appMode == AppModePicture) {
return [[DetailsPanelPicture alloc] initWithApp:model];
}
else {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
} }
- (void)setScanOptions - (DirectoryPanel *)createDirectoryPanel
{ {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; return [[DirectoryPanel alloc] initWithParentApp:self];
NSString *scanTypeOptionName; }
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) { - (DetailsPanel *)createDetailsPanel
scanTypeOptionName = @"scanTypePicture"; {
} return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
else if (appMode == AppModeMusic) { }
scanTypeOptionName = @"scanTypeMusic";
} - (NSString *)homepageURL
else { {
scanTypeOptionName = @"scanTypeStandard"; return @""; // must be overriden by all editions
}
[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 */ /* Public */
- (ResultWindow *)resultWindow - (ResultWindowBase *)resultWindow
{ {
return _resultWindow; return _resultWindow;
} }
@@ -185,29 +128,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
return _recentResults; return _recentResults;
} }
- (NSInteger)getAppMode
{
return [model getAppMode];
}
- (void)setAppMode:(NSInteger)appMode
{
[model setAppMode:appMode];
if (_preferencesPanel != nil) {
[_preferencesPanel release];
_preferencesPanel = nil;
}
}
/* Actions */ /* 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 - (void)loadResults
{ {
NSOpenPanel *op = [NSOpenPanel openPanel]; NSOpenPanel *op = [NSOpenPanel openPanel];
@@ -226,7 +147,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)openWebsite - (void)openWebsite
{ {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self homepageURL]]];
} }
- (void)openHelp - (void)openHelp
@@ -253,18 +174,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)showPreferencesPanel - (void)showPreferencesPanel
{ {
if (_preferencesPanel == nil) { if (_preferencesPanel == nil) {
NSWindow *window; _preferencesPanel = [[NSWindowController alloc] initWithWindow:createPreferencesPanel_UI(nil)];
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]; [_preferencesPanel showWindow:nil];
} }
@@ -281,7 +191,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)startScanning - (void)startScanning
{ {
[[self directoryPanel] startDuplicateScan]; [[self resultWindow] startDuplicateScan];
} }
@@ -344,29 +254,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn; 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 - (void)showProblemDialog
{ {
[_problemDialog showWindow:self]; [[self resultWindow] showProblemDialog];
} }
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt - (NSString *)selectDestFolderWithPrompt:(NSString *)prompt

View File

@@ -17,8 +17,3 @@ http://www.gnu.org/licenses/gpl-3.0.html
#define jobDelete @"job_delete" #define jobDelete @"job_delete"
#define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType" #define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType"
#define ImageLoadedNotification @"ImageLoadedNotification"
#define AppModeStandard 0
#define AppModeMusic 1
#define AppModePicture 2

View File

@@ -10,7 +10,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import <Python.h> #import <Python.h>
#import "PyDetailsPanel.h" #import "PyDetailsPanel.h"
@interface DetailsPanel : NSWindowController <NSTableViewDataSource> @interface DetailsPanelBase : NSWindowController <NSTableViewDataSource>
{ {
NSTableView *detailsTable; NSTableView *detailsTable;

View File

@@ -6,11 +6,10 @@ which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "DetailsPanel.h" #import "DetailsPanelBase.h"
#import "HSPyUtil.h" #import "HSPyUtil.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel @implementation DetailsPanelBase
@synthesize detailsTable; @synthesize detailsTable;
@@ -36,7 +35,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (NSWindow *)createWindow - (NSWindow *)createWindow
{ {
return createDetailsPanel_UI(self); return nil; // Virtual
} }
- (void)refreshDetails - (void)refreshDetails

View File

@@ -12,17 +12,15 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "DirectoryOutline.h" #import "DirectoryOutline.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@class AppDelegate; @class AppDelegateBase;
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate> @interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
{ {
AppDelegate *_app; AppDelegateBase *_app;
PyDupeGuru *model; PyDupeGuru *model;
HSRecentFiles *_recentDirectories; HSRecentFiles *_recentDirectories;
DirectoryOutline *outline; DirectoryOutline *outline;
BOOL _alwaysShowPopUp; BOOL _alwaysShowPopUp;
NSSegmentedControl *appModeSelector;
NSPopUpButton *scanTypePopup;
NSPopUpButton *addButtonPopUp; NSPopUpButton *addButtonPopUp;
NSPopUpButton *loadRecentButtonPopUp; NSPopUpButton *loadRecentButtonPopUp;
HSOutlineView *outlineView; HSOutlineView *outlineView;
@@ -30,25 +28,21 @@ http://www.gnu.org/licenses/gpl-3.0.html
NSButton *loadResultsButton; NSButton *loadResultsButton;
} }
@property (readwrite, retain) NSSegmentedControl *appModeSelector;
@property (readwrite, retain) NSPopUpButton *scanTypePopup;
@property (readwrite, retain) NSPopUpButton *addButtonPopUp; @property (readwrite, retain) NSPopUpButton *addButtonPopUp;
@property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp; @property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp;
@property (readwrite, retain) HSOutlineView *outlineView; @property (readwrite, retain) HSOutlineView *outlineView;
@property (readwrite, retain) NSButton *removeButton; @property (readwrite, retain) NSButton *removeButton;
@property (readwrite, retain) NSButton *loadResultsButton; @property (readwrite, retain) NSButton *loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp; - (id)initWithParentApp:(AppDelegateBase *)aParentApp;
- (void)fillPopUpMenu; - (void)fillPopUpMenu; // Virtual
- (void)fillScanTypeMenu;
- (void)adjustUIToLocalization; - (void)adjustUIToLocalization;
- (void)askForDirectory; - (void)askForDirectory;
- (void)popupAddDirectoryMenu:(id)sender; - (void)popupAddDirectoryMenu:(id)sender;
- (void)popupLoadRecentMenu:(id)sender; - (void)popupLoadRecentMenu:(id)sender;
- (void)removeSelectedDirectory; - (void)removeSelectedDirectory;
- (void)startDuplicateScan;
- (void)addDirectory:(NSString *)directory; - (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText; - (void)refreshRemoveButtonText;

View File

@@ -11,27 +11,22 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "Dialogs.h" #import "Dialogs.h"
#import "Utils.h" #import "Utils.h"
#import "AppDelegate.h" #import "AppDelegate.h"
#import "Consts.h"
@implementation DirectoryPanel @implementation DirectoryPanel
@synthesize appModeSelector;
@synthesize scanTypePopup;
@synthesize addButtonPopUp; @synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp; @synthesize loadRecentButtonPopUp;
@synthesize outlineView; @synthesize outlineView;
@synthesize removeButton; @synthesize removeButton;
@synthesize loadResultsButton; @synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp - (id)initWithParentApp:(AppDelegateBase *)aParentApp
{ {
self = [super initWithWindow:nil]; self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)]; [self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp; _app = aParentApp;
model = [_app model]; model = [_app model];
[[self window] setTitle:[model appName]]; [[self window] setTitle:[model appName]];
self.appModeSelector.selectedSegment = 0;
[self fillScanTypeMenu];
_alwaysShowPopUp = NO; _alwaysShowPopUp = NO;
[self fillPopUpMenu]; [self fillPopUpMenu];
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]]; _recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
@@ -64,25 +59,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
[m addItem:[NSMenuItem separatorItem]]; [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 - (void)adjustUIToLocalization
{ {
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0]; NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
@@ -121,23 +97,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
} }
} }
- (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 - (void)popupAddDirectoryMenu:(id)sender
{ {
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) { if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
@@ -175,16 +134,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
[self refreshRemoveButtonText]; [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 */ /* Public */
- (void)addDirectory:(NSString *)directory - (void)addDirectory:(NSString *)directory
{ {

View File

@@ -10,12 +10,14 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import <Quartz/Quartz.h> #import <Quartz/Quartz.h>
#import "StatsLabel.h" #import "StatsLabel.h"
#import "ResultTable.h" #import "ResultTable.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSTableView.h" #import "HSTableView.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@class AppDelegate; @class AppDelegateBase;
@interface ResultWindow : NSWindowController @interface ResultWindowBase : NSWindowController
{ {
@protected @protected
NSSegmentedControl *optionsSwitch; NSSegmentedControl *optionsSwitch;
@@ -24,10 +26,12 @@ http://www.gnu.org/licenses/gpl-3.0.html
NSTextField *stats; NSTextField *stats;
NSSearchField *filterField; NSSearchField *filterField;
AppDelegate *app; AppDelegateBase *app;
PyDupeGuru *model; PyDupeGuru *model;
ResultTable *table; ResultTable *table;
StatsLabel *statsLabel; StatsLabel *statsLabel;
ProblemDialog *problemDialog;
DeletionOptions *deletionOptions;
QLPreviewPanel* previewPanel; QLPreviewPanel* previewPanel;
} }
@@ -37,13 +41,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
@property (readwrite, retain) NSTextField *stats; @property (readwrite, retain) NSTextField *stats;
@property (readwrite, retain) NSSearchField *filterField; @property (readwrite, retain) NSSearchField *filterField;
- (id)initWithParentApp:(AppDelegate *)app; - (id)initWithParentApp:(AppDelegateBase *)app;
/* Virtual */
- (void)initResultColumns;
- (void)setScanOptions;
/* Helpers */ /* Helpers */
- (void)fillColumnsMenu; - (void)fillColumnsMenu;
- (void)updateOptionSegments; - (void)updateOptionSegments;
- (void)showProblemDialog;
- (void)adjustUIToLocalization; - (void)adjustUIToLocalization;
- (void)initResultColumns:(ResultTable *)aTable;
/* Actions */ /* Actions */
- (void)changeOptions; - (void)changeOptions;
@@ -67,6 +75,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)resetColumnsToDefault; - (void)resetColumnsToDefault;
- (void)revealSelected; - (void)revealSelected;
- (void)saveResults; - (void)saveResults;
- (void)startDuplicateScan;
- (void)switchSelected; - (void)switchSelected;
- (void)toggleColumn:(id)sender; - (void)toggleColumn:(id)sender;
- (void)toggleDelta; - (void)toggleDelta;

View File

@@ -6,7 +6,7 @@ which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "ResultWindow.h" #import "ResultWindowBase.h"
#import "ResultWindow_UI.h" #import "ResultWindow_UI.h"
#import "Dialogs.h" #import "Dialogs.h"
#import "ProgressController.h" #import "ProgressController.h"
@@ -15,7 +15,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "Consts.h" #import "Consts.h"
#import "PrioritizeDialog.h" #import "PrioritizeDialog.h"
@implementation ResultWindow @implementation ResultWindowBase
@synthesize optionsSwitch; @synthesize optionsSwitch;
@synthesize optionsToolbarItem; @synthesize optionsToolbarItem;
@@ -23,7 +23,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
@synthesize stats; @synthesize stats;
@synthesize filterField; @synthesize filterField;
- (id)initWithParentApp:(AppDelegate *)aApp; - (id)initWithParentApp:(AppDelegateBase *)aApp;
{ {
self = [super initWithWindow:nil]; self = [super initWithWindow:nil];
app = aApp; app = aApp;
@@ -34,7 +34,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge]; [[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches]; table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats]; statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
[self initResultColumns:table]; problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
[self initResultColumns];
[[table columns] setColumnsAsReadOnly]; [[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu]; [self fillColumnsMenu];
[matches setTarget:self]; [matches setTarget:self];
@@ -47,9 +49,19 @@ http://www.gnu.org/licenses/gpl-3.0.html
{ {
[table release]; [table release];
[statsLabel release]; [statsLabel release];
[problemDialog release];
[super dealloc]; [super dealloc];
} }
/* Virtual */
- (void)initResultColumns
{
}
- (void)setScanOptions
{
}
/* Helpers */ /* Helpers */
- (void)fillColumnsMenu - (void)fillColumnsMenu
{ {
@@ -77,6 +89,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2]; [optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
} }
- (void)showProblemDialog
{
[problemDialog showWindow:self];
}
- (void)adjustUIToLocalization - (void)adjustUIToLocalization
{ {
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0]; NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
@@ -101,87 +118,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
} }
} }
- (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 */ /* Actions */
- (void)changeOptions - (void)changeOptions
{ {
@@ -327,6 +263,16 @@ http://www.gnu.org/licenses/gpl-3.0.html
} }
} }
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[self setScanOptions];
[model doScan];
}
- (void)switchSelected - (void)switchSelected
{ {
[model makeSelectedReference]; [model makeSelectedReference];

View File

@@ -3,15 +3,18 @@
"About dupeGuru" = "About dupeGuru"; "About dupeGuru" = "About dupeGuru";
"Action" = "Action"; "Action" = "Action";
"Actions" = "Actions"; "Actions" = "Actions";
"Add Aperture Library" = "Add Aperture Library";
"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 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 iPhoto Library" = "Add iPhoto Library";
"Add iTunes Library" = "Add iTunes Library";
"Add New Folder..." = "Add New Folder..."; "Add New Folder..." = "Add New Folder...";
"Add Selected to Ignore List" = "Add Selected to Ignore List"; "Add Selected to Ignore List" = "Add Selected to Ignore List";
"Advanced" = "Advanced"; "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."; "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"; "Album" = "Album";
"Application Mode:" = "Application Mode:";
"Artist" = "Artist"; "Artist" = "Artist";
"Attribute" = "Attribute"; "Attribute" = "Attribute";
"Audio Content" = "Audio Content";
"Automatically check for updates" = "Automatically check for updates"; "Automatically check for updates" = "Automatically check for updates";
"Basic" = "Basic"; "Basic" = "Basic";
"Bring All to Front" = "Bring All to Front"; "Bring All to Front" = "Bring All to Front";
@@ -23,6 +26,8 @@
"Close" = "Close"; "Close" = "Close";
"Close Window" = "Close Window"; "Close Window" = "Close Window";
"Columns" = "Columns"; "Columns" = "Columns";
"Content" = "Content";
"Contents" = "Contents";
"Copy" = "Copy"; "Copy" = "Copy";
"Copy and Move:" = "Copy and Move:"; "Copy and Move:" = "Copy and Move:";
"Copy Marked to..." = "Copy Marked to..."; "Copy Marked to..." = "Copy Marked to...";
@@ -39,20 +44,27 @@
"Do you really want to remove all your cached picture analysis?" = "Do you really want to remove all your cached picture analysis?"; "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" = "dupeGuru";
"dupeGuru Help" = "dupeGuru Help"; "dupeGuru Help" = "dupeGuru Help";
"dupeGuru ME Preferences" = "dupeGuru ME Preferences";
"dupeGuru PE Preferences" = "dupeGuru PE Preferences";
"dupeGuru Preferences" = "dupeGuru Preferences"; "dupeGuru Preferences" = "dupeGuru Preferences";
"dupeGuru Results" = "dupeGuru Results"; "dupeGuru Results" = "dupeGuru Results";
"dupeGuru Website" = "dupeGuru Website"; "dupeGuru Website" = "dupeGuru Website";
"Dupes Only" = "Dupes Only"; "Dupes Only" = "Dupes Only";
"Edit" = "Edit"; "Edit" = "Edit";
"Excluded" = "Excluded"; "Excluded" = "Excluded";
"EXIF Timestamp" = "EXIF Timestamp";
"Export Results to CSV" = "Export Results to CSV"; "Export Results to CSV" = "Export Results to CSV";
"Export Results to XHTML" = "Export Results to XHTML"; "Export Results to XHTML" = "Export Results to XHTML";
"Fewer results" = "Fewer results"; "Fewer results" = "Fewer results";
"File" = "File"; "File" = "File";
"Filename" = "Filename";
"Filename - Fields" = "Filename - Fields";
"Filename - Fields (No Order)" = "Filename - Fields (No Order)";
"Filter" = "Filter"; "Filter" = "Filter";
"Filter hardness:" = "Filter hardness:"; "Filter hardness:" = "Filter hardness:";
"Filter Results..." = "Filter Results..."; "Filter Results..." = "Filter Results...";
"Folder Selection Window" = "Folder Selection Window"; "Folder Selection Window" = "Folder Selection Window";
"Folders" = "Folders";
"Font Size:" = "Font Size:"; "Font Size:" = "Font Size:";
"Genre" = "Genre"; "Genre" = "Genre";
"Help" = "Help"; "Help" = "Help";
@@ -80,14 +92,12 @@
"Mode" = "Mode"; "Mode" = "Mode";
"More results" = "More results"; "More results" = "More results";
"Move Marked to..." = "Move Marked to..."; "Move Marked to..." = "Move Marked to...";
"Music" = "Music";
"Name" = "Name"; "Name" = "Name";
"Normal" = "Normal"; "Normal" = "Normal";
"Ok" = "Ok"; "Ok" = "Ok";
"Open Selected with Default Application" = "Open Selected with Default Application"; "Open Selected with Default Application" = "Open Selected with Default Application";
"Options" = "Options"; "Options" = "Options";
"Paste" = "Paste"; "Paste" = "Paste";
"Picture" = "Picture";
"Preferences..." = "Preferences..."; "Preferences..." = "Preferences...";
"Problems!" = "Problems!"; "Problems!" = "Problems!";
"Proceed" = "Proceed"; "Proceed" = "Proceed";
@@ -98,6 +108,7 @@
"Recreate absolute path" = "Recreate absolute path"; "Recreate absolute path" = "Recreate absolute path";
"Recreate relative path" = "Recreate relative path"; "Recreate relative path" = "Recreate relative path";
"Reference" = "Reference"; "Reference" = "Reference";
"Remove Dead Tracks in iTunes" = "Remove Dead Tracks in iTunes";
"Remove empty folders on delete or move" = "Remove empty folders on delete or move"; "Remove empty folders on delete or move" = "Remove empty folders on delete or move";
"Remove Marked from Results" = "Remove Marked from Results"; "Remove Marked from Results" = "Remove Marked from Results";
"Remove Selected" = "Remove Selected"; "Remove Selected" = "Remove Selected";
@@ -123,9 +134,9 @@
"Show All" = "Show All"; "Show All" = "Show All";
"Show Delta Values" = "Show Delta Values"; "Show Delta Values" = "Show Delta Values";
"Show Dupes Only" = "Show Dupes Only"; "Show Dupes Only" = "Show Dupes Only";
"Standard" = "Standard";
"Start Duplicate Scan" = "Start Duplicate Scan"; "Start Duplicate Scan" = "Start Duplicate Scan";
"State" = "State"; "State" = "State";
"Tags" = "Tags";
"Tags to scan:" = "Tags to scan:"; "Tags to scan:" = "Tags to scan:";
"The name '%@' already exists." = "The name '%@' already exists."; "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."; "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.";

View File

@@ -5,10 +5,6 @@ result = Window(425, 300, "dupeGuru")
promptLabel = Label(result, "Select folders to scan and press \"Scan\".") promptLabel = Label(result, "Select folders to scan and press \"Scan\".")
directoryOutline = OutlineView(result) directoryOutline = OutlineView(result)
directoryOutline.OBJC_CLASS = 'HSOutlineView' directoryOutline.OBJC_CLASS = 'HSOutlineView'
appModeSelector = SegmentedControl(result)
appModeLabel = Label(result, "Application Mode:")
scanTypePopup = Popup(result)
scanTypeLabel = Label(result, "Scan Type:")
addButton = Button(result, "") addButton = Button(result, "")
removeButton = Button(result, "") removeButton = Button(result, "")
loadResultsButton = Button(result, "Load Results") loadResultsButton = Button(result, "Load Results")
@@ -17,8 +13,6 @@ addPopup = Popup(None)
loadRecentPopup = Popup(None) loadRecentPopup = Popup(None)
owner.outlineView = directoryOutline owner.outlineView = directoryOutline
owner.appModeSelector = appModeSelector
owner.scanTypePopup = scanTypePopup
owner.removeButton = removeButton owner.removeButton = removeButton
owner.loadResultsButton = loadResultsButton owner.loadResultsButton = loadResultsButton
owner.addButtonPopUp = addPopup owner.addButtonPopUp = addPopup
@@ -26,9 +20,7 @@ owner.loadRecentButtonPopUp = loadRecentPopup
result.autosaveName = 'DirectoryPanel' result.autosaveName = 'DirectoryPanel'
result.canMinimize = False result.canMinimize = False
result.minSize = Size(400, 270) result.minSize = Size(370, 270)
for label in ["Standard", "Music", "Picture"]:
appModeSelector.addSegment(label, 80)
addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle
addButton.image = 'NSAddTemplate' addButton.image = 'NSAddTemplate'
removeButton.image = 'NSRemoveTemplate' removeButton.image = 'NSRemoveTemplate'
@@ -36,7 +28,6 @@ for button in (addButton, removeButton):
button.style = const.NSTexturedRoundedBezelStyle button.style = const.NSTexturedRoundedBezelStyle
button.imagePosition = const.NSImageOnly button.imagePosition = const.NSImageOnly
scanButton.keyEquivalent = '\\r' scanButton.keyEquivalent = '\\r'
appModeSelector.action = Action(owner, 'changeAppMode:')
addButton.action = Action(owner, 'popupAddDirectoryMenu:') addButton.action = Action(owner, 'popupAddDirectoryMenu:')
removeButton.action = Action(owner, 'removeSelectedDirectory') removeButton.action = Action(owner, 'removeSelectedDirectory')
loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:') loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:')
@@ -55,21 +46,18 @@ directoryOutline.allowsColumnReordering = False
directoryOutline.allowsColumnSelection = False directoryOutline.allowsColumnSelection = False
directoryOutline.allowsMultipleSelection = True directoryOutline.allowsMultipleSelection = True
appModeLabel.width = scanTypeLabel.width = 110
scanTypePopup.width = 248
appModeLayout = HLayout([appModeLabel, appModeSelector])
scanTypeLayout = HLayout([scanTypeLabel, scanTypePopup])
for button in (addButton, removeButton): for button in (addButton, removeButton):
button.width = 28 button.width = 28
for button in (loadResultsButton, scanButton): for button in (loadResultsButton, scanButton):
button.width = 118 button.width = 118
buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton]) buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton])
mainLayout = VLayout([appModeLayout, scanTypeLayout, promptLabel, directoryOutline, buttonLayout], filler=directoryOutline) promptLabel.packToCorner(Pack.UpperLeft)
mainLayout.packToCorner(Pack.UpperLeft) promptLabel.fill(Pack.Right)
mainLayout.fill(Pack.LowerRight)
directoryOutline.packRelativeTo(promptLabel, Pack.Below) directoryOutline.packRelativeTo(promptLabel, Pack.Below)
buttonLayout.packRelativeTo(directoryOutline, Pack.Below, margin=8)
directoryOutline.fill(Pack.LowerRight)
buttonLayout.fill(Pack.Right)
promptLabel.setAnchor(Pack.UpperLeft, growX=True) promptLabel.setAnchor(Pack.UpperLeft, growX=True)
directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True) directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@@ -1,5 +1,6 @@
ownerclass = 'AppDelegate' ownerclass = 'AppDelegateBase'
ownerimport = 'AppDelegate.h' ownerimport = 'AppDelegateBase.h'
edition = args.get('edition', 'se')
result = Menu("") result = Menu("")
appMenu = result.addMenu("dupeGuru") appMenu = result.addMenu("dupeGuru")
@@ -29,7 +30,10 @@ owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s') 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 XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e')
fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV')) fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV'))
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p') if edition == 'pe':
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
elif edition == 'me':
fileMenu.addItem("Remove Dead Tracks in iTunes", Action(owner, 'removeDeadTracks'))
editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a') editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a')
editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a') editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a')

View File

@@ -1,14 +1,26 @@
appmode = args.get('appmode', 'standard') edition = args.get('edition', 'se')
dialogTitles = {
'se': "dupeGuru Preferences",
'me': "dupeGuru ME Preferences",
'pe': "dupeGuru PE Preferences",
}
dialogHeights = { dialogHeights = {
'standard': 325, 'se': 345,
'music': 345, 'me': 365,
'picture': 255, 'pe': 275,
}
scanTypeNames = {
'se': ["Filename", "Content", "Folders"],
'me': ["Filename", "Filename - Fields", "Filename - Fields (No Order)", "Tags", "Content", "Audio Content"],
'pe': ["Contents", "EXIF Timestamp"],
} }
result = Window(410, dialogHeights[appmode], "dupeGuru Preferences") result = Window(410, dialogHeights[edition], dialogTitles[edition])
tabView = TabView(result) tabView = TabView(result)
basicTab = tabView.addTab("Basic") basicTab = tabView.addTab("Basic")
advancedTab = tabView.addTab("Advanced") advancedTab = tabView.addTab("Advanced")
scanTypePopup = Popup(basicTab.view, scanTypeNames[edition])
scanTypeLabel = Label(basicTab.view, "Scan Type:")
thresholdSlider = Slider(basicTab.view, 1, 100, 80) thresholdSlider = Slider(basicTab.view, 1, 100, 80)
thresholdLabel = Label(basicTab.view, "Filter hardness:") thresholdLabel = Label(basicTab.view, "Filter hardness:")
moreResultsLabel = Label(basicTab.view, "More results") moreResultsLabel = Label(basicTab.view, "More results")
@@ -16,19 +28,19 @@ fewerResultsLabel = Label(basicTab.view, "Fewer results")
thresholdValueLabel = Label(basicTab.view, "") thresholdValueLabel = Label(basicTab.view, "")
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"]) fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
fontSizeLabel = Label(basicTab.view, "Font Size:") fontSizeLabel = Label(basicTab.view, "Font Size:")
if appmode in ('standard', 'music'): if edition in ('se', 'me'):
wordWeightingBox = Checkbox(basicTab.view, "Word weighting") wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words") matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
elif appmode == 'picture': elif edition == 'pe':
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions") matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
mixKindBox = Checkbox(basicTab.view, "Can mix file kind") mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move") removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates") checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
if appmode == 'standard': if edition == 'se':
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:") ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
smallFilesThresholdText = TextField(basicTab.view, "") smallFilesThresholdText = TextField(basicTab.view, "")
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB") smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
elif appmode == 'music': elif edition == 'me':
tagsToScanLabel = Label(basicTab.view, "Tags to scan:") tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
trackBox = Checkbox(basicTab.view, "Track") trackBox = Checkbox(basicTab.view, "Track")
artistBox = Checkbox(basicTab.view, "Artist") artistBox = Checkbox(basicTab.view, "Artist")
@@ -47,6 +59,8 @@ copyMoveLabel = Label(advancedTab.view, "Copy and Move:")
copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"]) copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"])
resetToDefaultsButton = Button(result, "Reset To Defaults") resetToDefaultsButton = Button(result, "Reset To Defaults")
scanTypePopup.bind('selectedIndex', defaults, 'values.scanType')
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage') thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage') thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
fontSizeCombo.bind('value', defaults, 'values.TableFontSize') fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
@@ -58,61 +72,59 @@ ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
debugModeCheckbox.bind('value', defaults, 'values.DebugMode') debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
customCommandText.bind('value', defaults, 'values.CustomCommand') customCommandText.bind('value', defaults, 'values.CustomCommand')
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType') copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
if appmode in ('standard', 'music'): if edition in ('se', 'me'):
wordWeightingBox.bind('value', defaults, 'values.wordWeighting') wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords') matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox] disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
for control in disableWhenContentScan: for control in disableWhenContentScan:
vtname = 'vtScanTypeMusicIsNotContent' if appmode == 'music' else 'vtScanTypeIsNotContent' control.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsNotContent')
prefname = 'values.scanTypeMusic' if appmode == 'music' else 'values.scanTypeStandard' if edition == 'se':
control.bind('enabled', defaults, prefname, valueTransformer=vtname)
if appmode == 'standard':
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles') ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold') smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
elif appmode == 'music': elif edition == 'me':
for box in tagBoxes: for box in tagBoxes:
box.bind('enabled', defaults, 'values.scanTypeMusic', valueTransformer='vtScanTypeIsTag') box.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsTag')
trackBox.bind('value', defaults, 'values.scanTagTrack') trackBox.bind('value', defaults, 'values.scanTagTrack')
artistBox.bind('value', defaults, 'values.scanTagArtist') artistBox.bind('value', defaults, 'values.scanTagArtist')
albumBox.bind('value', defaults, 'values.scanTagAlbum') albumBox.bind('value', defaults, 'values.scanTagAlbum')
titleBox.bind('value', defaults, 'values.scanTagTitle') titleBox.bind('value', defaults, 'values.scanTagTitle')
genreBox.bind('value', defaults, 'values.scanTagGenre') genreBox.bind('value', defaults, 'values.scanTagGenre')
yearBox.bind('value', defaults, 'values.scanTagYear') yearBox.bind('value', defaults, 'values.scanTagYear')
elif appmode == 'picture': elif edition == 'pe':
matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled') matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled')
thresholdSlider.bind('enabled', defaults, 'values.scanTypePicture', valueTransformer='vtScanTypeIsFuzzy') thresholdSlider.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsFuzzy')
result.canResize = False result.canResize = False
result.canMinimize = False result.canMinimize = False
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal) thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
thresholdValueLabel.formatter.maximumFractionDigits = 0 thresholdValueLabel.formatter.maximumFractionDigits = 0
allLabels = [thresholdValueLabel, moreResultsLabel, fewerResultsLabel, allLabels = [scanTypeLabel, thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel] thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox, allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
ignoreHardlinksBox, debugModeCheckbox] ignoreHardlinksBox, debugModeCheckbox]
if appmode == 'standard': if edition == 'se':
allLabels += [smallFilesThresholdSuffixLabel] allLabels += [smallFilesThresholdSuffixLabel]
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox] allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'music': elif edition == 'me':
allLabels += [tagsToScanLabel] allLabels += [tagsToScanLabel]
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox] allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'picture': elif edition == 'pe':
allCheckboxes += [matchDifferentDimensionsBox] allCheckboxes += [matchDifferentDimensionsBox]
for label in allLabels: for label in allLabels:
label.controlSize = ControlSize.Small label.controlSize = ControlSize.Small
fewerResultsLabel.alignment = TextAlignment.Right fewerResultsLabel.alignment = TextAlignment.Right
for checkbox in allCheckboxes: for checkbox in allCheckboxes:
checkbox.font = thresholdValueLabel.font checkbox.font = scanTypeLabel.font
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:') resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
thresholdLabel.width = fontSizeLabel.width = 94 scanTypeLabel.width = thresholdLabel.width = fontSizeLabel.width = 94
fontSizeCombo.width = 66 fontSizeCombo.width = 66
thresholdValueLabel.width = 25 thresholdValueLabel.width = 25
resetToDefaultsButton.width = 136 resetToDefaultsButton.width = 136
if appmode == 'standard': if edition == 'se':
smallFilesThresholdText.width = 60 smallFilesThresholdText.width = 60
smallFilesThresholdSuffixLabel.width = 40 smallFilesThresholdSuffixLabel.width = 40
elif appmode == 'music': elif edition == 'me':
for box in tagBoxes: for box in tagBoxes:
box.width = 70 box.width = 70
@@ -121,18 +133,22 @@ tabView.fill(Pack.Right)
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right) resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
tabView.fill(Pack.Below, margin=14) tabView.fill(Pack.Below, margin=14)
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True) tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
thresholdLayout = HLayout([thresholdLabel, thresholdSlider, thresholdValueLabel], filler=thresholdSlider) scanTypePopup.packToCorner(Pack.UpperRight)
thresholdLayout.packToCorner(Pack.UpperLeft) scanTypeLabel.packRelativeTo(scanTypePopup, Pack.Left)
thresholdLayout.fill(Pack.Right) scanTypePopup.fill(Pack.Left)
thresholdSlider.packRelativeTo(scanTypePopup, Pack.Below)
thresholdValueLabel.packRelativeTo(thresholdSlider, Pack.Right)
thresholdSlider.fill(Pack.Right)
# We want to give the labels as much space as possible, and we only "know" how much is available # 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. # after the slider's fill operation.
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2 moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6) moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6) fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
thresholdLabel.packRelativeTo(thresholdSlider, Pack.Left)
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below) fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left) fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
if appmode == 'music': if edition == 'me':
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below) tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
tagsToScanLabel.fill(Pack.Left) tagsToScanLabel.fill(Pack.Left)
tagsToScanLabel.fill(Pack.Right) tagsToScanLabel.fill(Pack.Right)
@@ -147,13 +163,13 @@ if appmode == 'music':
else: else:
viewToPackCheckboxesUnder = fontSizeCombo viewToPackCheckboxesUnder = fontSizeCombo
if appmode == 'standard': if edition == 'se':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
ignoreSmallFilesBox] ignoreSmallFilesBox]
elif appmode == 'music': elif edition == 'me':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox] checkForUpdatesBox]
elif appmode == 'picture': elif edition == 'pe':
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox] checkForUpdatesBox]
checkboxLayout = VLayout(checkboxesToLayout) checkboxLayout = VLayout(checkboxesToLayout)
@@ -161,7 +177,7 @@ checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
checkboxLayout.fill(Pack.Left) checkboxLayout.fill(Pack.Left)
checkboxLayout.fill(Pack.Right) checkboxLayout.fill(Pack.Right)
if appmode == 'standard': if edition == 'se':
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4) smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4) checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
checkForUpdatesBox.fill(Pack.Right) checkForUpdatesBox.fill(Pack.Right)

View File

@@ -1,5 +1,5 @@
ownerclass = 'ResultWindow' ownerclass = 'ResultWindowBase'
ownerimport = 'ResultWindow.h' ownerimport = 'ResultWindowBase.h'
result = Window(557, 400, "dupeGuru Results") result = Window(557, 400, "dupeGuru Results")
toolbar = result.createToolbar('ResultsToolbar') toolbar = result.createToolbar('ResultsToolbar')

View File

@@ -1,56 +1,23 @@
import logging import logging
from objp.util import pyref, dontwrap from objp.util import pyref, dontwrap
from hscommon.path import Path, pathify
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
from cocoa.inter import PyBaseApp, BaseAppView 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):
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): class DupeGuruView(BaseAppView):
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
def createResultsWindow(self): pass
def showResultsWindow(self): pass
def showProblemDialog(self): pass def showProblemDialog(self): pass
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
class PyDupeGuru(PyBaseApp): class PyDupeGuruBase(PyBaseApp):
@dontwrap @dontwrap
def __init__(self): def _init(self, modelclass):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s') logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
install_exception_hook('https://github.com/hsoft/dupeguru/issues') install_exception_hook('https://github.com/hsoft/dupeguru/issues')
install_cocoa_logger() install_cocoa_logger()
patch_threaded_job_performer() patch_threaded_job_performer()
self.model = DupeGuru(self) self.model = modelclass(self)
#---Sub-proxies #---Sub-proxies
def detailsPanel(self) -> pyref: def detailsPanel(self) -> pyref:
@@ -155,62 +122,13 @@ class PyDupeGuru(PyBaseApp):
def showIgnoreList(self): def showIgnoreList(self):
self.model.ignore_list_dialog.show() self.model.ignore_list_dialog.show()
def clearPictureCache(self):
self.model.clear_picture_cache()
#---Information #---Information
def getScanOptions(self) -> list:
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
def resultsAreModified(self) -> bool: def resultsAreModified(self) -> bool:
return self.model.results.is_modified 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 #---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): def setMixFileKind_(self, mix_file_kind: bool):
self.model.options['mix_file_kind'] = mix_file_kind self.model.scanner.mix_file_kind = mix_file_kind
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool): def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
self.model.options['escape_filter_regexp'] = escape_filter_regexp self.model.options['escape_filter_regexp'] = escape_filter_regexp
@@ -229,13 +147,12 @@ class PyDupeGuru(PyBaseApp):
def ask_yes_no(self, prompt): def ask_yes_no(self, prompt):
return self.callback.askYesNoWithPrompt_(prompt) return self.callback.askYesNoWithPrompt_(prompt)
@dontwrap
def create_results_window(self):
self.callback.createResultsWindow()
@dontwrap @dontwrap
def show_results_window(self): def show_results_window(self):
self.callback.showResultsWindow() # Not needed yet because our progress dialog is shown as a sheet of the results window,
# which causes it to be already visible when the scan/load ends.
# XXX Make progress sheet be a child of the folder selection window.
pass
@dontwrap @dontwrap
def show_problem_dialog(self): def show_problem_dialog(self):

287
cocoa/inter/app_me.py Normal file
View File

@@ -0,0 +1,287 @@
# Created By: Virgil Dupras
# Created On: 2006/11/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 logging
import plistlib
import time
import os.path as op
from appscript import app, its, k, CommandError, ApplicationNotFoundError
from . import tunes
from cocoa import as_fetch, proxy
from hscommon.trans import trget
from hscommon.path import Path
from hscommon.util import remove_invalid_xml
from core import directories
from core.app import JobType, JOBID2TITLE
from core.scanner import ScanType
from core_me.app import DupeGuru as DupeGuruBase
from core_me import fs
from .app import PyDupeGuruBase
tr = trget('ui')
JobType.RemoveDeadTracks = 'jobRemoveDeadTracks'
JobType.ScanDeadTracks = 'jobScanDeadTracks'
JOBID2TITLE.update({
JobType.RemoveDeadTracks: tr("Removing dead tracks from your iTunes Library"),
JobType.ScanDeadTracks: tr("Scanning the iTunes Library"),
})
ITUNES = 'iTunes'
ITUNES_PATH = Path('iTunes Library')
def get_itunes_library(a):
try:
[source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library]
[library] = source.library_playlists(timeout=0)
return library
except ValueError:
logging.warning('Some unexpected iTunes configuration encountered')
return None
class ITunesSong(fs.MusicFile):
def __init__(self, song_data):
path = Path(proxy.url2path_(song_data['Location']))
fs.MusicFile.__init__(self, path)
self.id = song_data['Track ID']
def remove_from_library(self):
try:
a = app(ITUNES, terms=tunes)
library = get_itunes_library(a)
if library is None:
return
[song] = library.file_tracks[its.database_ID == self.id]()
a.delete(song, timeout=0)
except ValueError:
msg = "Could not find song '{}' (trackid: {}) in iTunes Library".format(str(self.path), self.id)
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
display_folder_path = ITUNES_PATH
def get_itunes_database_path():
plisturls = proxy.prefValue_inDomain_('iTunesRecentDatabases', 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_itunes_songs(plistpath):
if not plistpath.exists():
return []
s = plistpath.open('rt', encoding='utf-8').read()
# iTunes sometimes produces XML files with invalid characters in it.
s = remove_invalid_xml(s, replace_with='')
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for song_data in plist['Tracks'].values():
try:
if song_data['Track Type'] != 'File':
continue
song = ITunesSong(song_data)
except KeyError: # No "Track Type", "Location" or "Track ID" key in track
continue
if song.path.exists():
result.append(song)
return result
class Directories(directories.Directories):
def __init__(self, fileclasses):
directories.Directories.__init__(self, fileclasses)
try:
self.itunes_libpath = get_itunes_database_path()
except directories.InvalidPathError:
self.itunes_libpath = None
def _get_files(self, from_path, j):
if from_path == ITUNES_PATH:
if self.itunes_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
songs = get_itunes_songs(self.itunes_libpath)
for song in songs:
song.is_ref = is_ref
return songs
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path == ITUNES_PATH:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path == ITUNES_PATH:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_itunes_path(self):
return any(path == ITUNES_PATH for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iTunes library, we assume we have at least one file.
if self.has_itunes_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruME(DupeGuruBase):
def __init__(self, view):
DupeGuruBase.__init__(self, view)
# Use fileclasses set in DupeGuruBase.__init__()
self.directories = Directories(fileclasses=self.directories.fileclasses)
self.dead_tracks = []
def _do_delete(self, j, *args):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, *args)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, ITunesSong) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iTunes. Don't touch it!"))
try:
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, ITunesSong):
dupe.remove_from_library()
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath.parent()):
if not hasattr(self, 'itunes_songs'):
songs = get_itunes_songs(self.directories.itunes_libpath)
self.itunes_songs = {song.path: song for song in songs}
if path in self.itunes_songs:
return self.itunes_songs[path]
else:
pass # We'll return the default file type, as per the last line of this method
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid):
# XXX Just before release, I'm realizing that this piece of code below is why I was passing
# job exception as an argument to _job_completed(). I have to comment it for now. It's not
# the end of the world, but I should find an elegant solution to this at some point.
# if (jobid in {JobType.RemoveDeadTracks, JobType.ScanDeadTracks}) and (exc is not None):
# msg = tr("There were communication problems with iTunes. The operation couldn't be completed.")
# self.view.show_message(msg)
# return True
if jobid == JobType.ScanDeadTracks:
dead_tracks_count = len(self.dead_tracks)
if dead_tracks_count > 0:
msg = tr("Your iTunes Library contains %d dead tracks ready to be removed. Continue?")
if self.view.ask_yes_no(msg % dead_tracks_count):
self.remove_dead_tracks()
else:
msg = tr("You have no dead tracks in your iTunes Library")
self.view.show_message(msg)
if jobid == JobType.Load:
if hasattr(self, 'itunes_songs'):
# If we load another file, we want a refresh song list
del self.itunes_songs
DupeGuruBase._job_completed(self, jobid)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, ITunesSong):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
def start_scanning(self):
if self.directories.has_itunes_path():
try:
app(ITUNES, terms=tunes)
except ApplicationNotFoundError:
self.view.show_message(tr("The iTunes application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
def remove_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
for index, track in enumerate(j.iter_with_progress(self.dead_tracks)):
if index % 100 == 0:
time.sleep(.1)
try:
track.delete(timeout=0)
except CommandError as e:
logging.warning('Error while trying to remove a track from iTunes: %s' % str(e))
self._start_job(JobType.RemoveDeadTracks, do)
def scan_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
library = get_itunes_library(a)
if library is None:
return
self.dead_tracks = []
tracks = as_fetch(library.file_tracks, k.file_track)
for index, track in enumerate(j.iter_with_progress(tracks)):
if index % 100 == 0:
time.sleep(.1)
if track.location(timeout=0) == k.missing_value:
self.dead_tracks.append(track)
logging.info('Found %d dead tracks' % len(self.dead_tracks))
self._start_job(JobType.ScanDeadTracks, do)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruME)
def scanDeadTracks(self):
self.model.scan_dead_tracks()
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = percentage
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Fields,
ScanType.FieldsNoOrder,
ScanType.Tag,
ScanType.Contents,
ScanType.ContentsAudio,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if enable:
self.model.scanner.scanned_tags.add(scan_tag)
else:
self.model.scanner.scanned_tags.discard(scan_tag)

342
cocoa/inter/app_pe.py Normal file
View File

@@ -0,0 +1,342 @@
# Created By: Virgil Dupras
# Created On: 2006/11/13
# 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 logging
import re
import io
from appscript import app, its, k, CommandError, ApplicationNotFoundError
from hscommon.util import remove_invalid_xml, first
from hscommon.path import Path, pathify
from hscommon.trans import trget
from cocoa import proxy
from core.scanner import ScanType
from core import directories
from core.app import JobType
from core_pe import _block_osx
from core_pe.photo import Photo as PhotoBase
from core_pe.app import DupeGuru as DupeGuruBase
from core_pe.iphoto_plist import IPhotoPlistParser
from .app import PyDupeGuruBase
tr = trget('ui')
IPHOTO_PATH = Path('iPhoto Library')
APERTURE_PATH = Path('Aperture Library')
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 ''
class IPhoto(Photo):
def __init__(self, path, db_id):
# In IPhoto, we don't care about the db_id, we find photos by path.
Photo.__init__(self, path)
@property
def display_folder_path(self):
return IPHOTO_PATH
class AperturePhoto(Photo):
def __init__(self, path, db_id):
Photo.__init__(self, path)
self.db_id = db_id
@property
def display_folder_path(self):
return APERTURE_PATH
@pathify
def get_iphoto_or_aperture_pictures(plistpath: Path, photo_class):
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
if not plistpath.exists():
return []
s = plistpath.open('rt', encoding='utf-8').read()
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
s = remove_invalid_xml(s, replace_with='')
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
# any & char that is not a &-based entity (&amp;, &quot;, etc.). based on TextMate's XML
# bundle's regexp
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
if count:
logging.warning("%d invalid XML entities replacement made", count)
parser = IPhotoPlistParser()
try:
plist = parser.parse(io.BytesIO(s.encode('utf-8')))
except Exception:
logging.warning("iPhoto plist parsing choked on data: %r", parser.lastdata)
raise
result = []
for key, photo_data in plist['Master Image List'].items():
if photo_data['MediaType'] != 'Image':
continue
photo_path = Path(photo_data['ImagePath'])
photo = photo_class(photo_path, key)
result.append(photo)
return result
def get_iphoto_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, IPhoto)
def get_aperture_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, AperturePhoto)
def get_iapps_database_path(prefname):
plisturls = proxy.prefValue_inDomain_(prefname, 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_iphoto_database_path():
return get_iapps_database_path('iPhotoRecentDatabases')
def get_aperture_database_path():
return get_iapps_database_path('ApertureLibraries')
class Directories(directories.Directories):
def __init__(self):
directories.Directories.__init__(self, fileclasses=[Photo])
try:
self.iphoto_libpath = get_iphoto_database_path()
self.set_state(self.iphoto_libpath.parent(), directories.DirectoryState.Excluded)
except directories.InvalidPathError:
self.iphoto_libpath = None
try:
self.aperture_libpath = get_aperture_database_path()
self.set_state(self.aperture_libpath.parent(), directories.DirectoryState.Excluded)
except directories.InvalidPathError:
self.aperture_libpath = None
def _get_files(self, from_path, j):
if from_path == IPHOTO_PATH:
if self.iphoto_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_iphoto_pictures(self.iphoto_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
elif from_path == APERTURE_PATH:
if self.aperture_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_aperture_pictures(self.aperture_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_iphoto_path(self):
return any(path in {IPHOTO_PATH, APERTURE_PATH} for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iPhoto library, we assume we have at least one file.
if self.has_iphoto_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruPE(DupeGuruBase):
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.directories = Directories()
def _do_delete(self, j, *args):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, *args)
self.deleted_aperture_photos = False
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, IPhoto) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iPhoto. Don't touch it!"))
try:
a = app('iPhoto')
a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
if any(isinstance(dupe, AperturePhoto) for dupe in marked):
self.deleted_aperture_photos = True
j.add_progress(0, desc=tr("Talking to Aperture. Don't touch it!"))
try:
a = app('Aperture')
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, IPhoto):
try:
a = app('iPhoto')
album = a.photo_library_album()
if album is None:
msg = "There are communication problems with iPhoto. Try opening iPhoto first, it might solve it."
raise EnvironmentError(msg)
[photo] = album.photos[its.image_path == str(dupe.path)]()
a.remove(photo, timeout=0)
except ValueError:
msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
if isinstance(dupe, AperturePhoto):
try:
a = app('Aperture')
# I'm flying blind here. In my own test library, all photos are in an album with the
# id "LibraryFolder", so I'm going to guess that it's the case at least most of the
# time. As a safeguard, if we don't find any library with that id, we'll use the
# first album.
# Now, about deleting: All attempts I've made at sending photos to trash failed,
# even with normal applescript. So, what we're going to do here is to create a
# "dupeGuru Trash" project and tell the user to manually send those photos to trash.
libraries = a.libraries()
library = first(l for l in libraries if l.id == 'LibraryFolder')
if library is None:
library = libraries[0]
trash_project = a.projects["dupeGuru Trash"]
if trash_project.exists():
trash_project = trash_project()
else:
trash_project = library.make(new=k.project, with_properties={k.name: "dupeGuru Trash"})
[photo] = library.image_versions[its.id == dupe.db_id]()
photo.move(to=trash_project)
except (IndexError, ValueError):
msg = "Could not find photo '{}' in Aperture Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else:
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath.parent()):
if not hasattr(self, 'path2iphoto'):
photos = get_iphoto_pictures(self.directories.iphoto_libpath)
self.path2iphoto = {p.path: p for p in photos}
return self.path2iphoto.get(path)
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath.parent()):
if not hasattr(self, 'path2aperture'):
photos = get_aperture_pictures(self.directories.aperture_libpath)
self.path2aperture = {p.path: p for p in photos}
return self.path2aperture.get(path)
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid):
DupeGuruBase._job_completed(self, jobid)
if jobid == JobType.Load:
if hasattr(self, 'path2iphoto'):
del self.path2iphoto
if hasattr(self, 'path2aperture'):
del self.path2aperture
if jobid == JobType.Delete and self.deleted_aperture_photos:
msg = tr("Deleted Aperture photos were sent to a project called \"dupeGuru Trash\".")
self.view.show_message(msg)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, (IPhoto, AperturePhoto)):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
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 start_scanning(self):
if self.directories.has_iphoto_path():
try:
app('iPhoto')
except ApplicationNotFoundError:
self.view.show_message(tr("The iPhoto application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruPE)
def clearPictureCache(self):
self.model.scanner.clear_picture_cache()
#---Information
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 setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.FuzzyBlock,
ScanType.ExifTimestamp,
][scan_type]
except IndexError:
pass
def setMatchScaled_(self, match_scaled: bool):
self.model.scanner.match_scaled = match_scaled
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.threshold = percentage

View File

@@ -1,13 +1,22 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # Created By: Virgil Dupras
# Created On: 2009-05-24
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# #
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy import logging
import os.path as op
from hscommon.path import Path, pathify from hscommon.path import Path, pathify
from core.se import fs from cocoa import proxy
from core.scanner import ScanType
from core.directories import Directories as DirectoriesBase, DirectoryState from core.directories import Directories as DirectoriesBase, DirectoryState
from core_se.app import DupeGuru as DupeGuruBase
from core_se import fs
from .app import PyDupeGuruBase
def is_bundle(str_path): def is_bundle(str_path):
uti = proxy.getUTI_(str_path) uti = proxy.getUTI_(str_path)
@@ -20,11 +29,15 @@ class Bundle(fs.Folder):
@pathify @pathify
def can_handle(cls, path: Path): def can_handle(cls, path: Path):
return not path.islink() and path.isdir() and is_bundle(str(path)) return not path.islink() and path.isdir() and is_bundle(str(path))
class Directories(DirectoriesBase): class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev'])) ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')] HOME_PATH_TO_EXCLUDE = [Path('Library')]
def __init__(self):
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])
self.folderclass = fs.Folder
def _default_state_for_path(self, path): def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path) result = DirectoriesBase._default_state_for_path(self, path)
if result is not None: if result is not None:
@@ -45,9 +58,47 @@ class Directories(DirectoriesBase):
yield from_folder yield from_folder
return return
else: else:
yield from DirectoriesBase._get_folders(self, from_folder, j) for folder in DirectoriesBase._get_folders(self, from_folder, j):
yield folder
@staticmethod @staticmethod
def get_subfolders(path): def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path) result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))] return [p for p in result if not is_bundle(str(p))]
class DupeGuru(DupeGuruBase):
def __init__(self, view):
# appdata = op.join(appdata, 'dupeGuru')
# print(repr(appdata))
DupeGuruBase.__init__(self, view)
self.directories = Directories()
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuru)
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = int(percentage)
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Contents,
ScanType.Folders,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.scanner.size_threshold = size_threshold

View File

@@ -1,35 +0,0 @@
# 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 ''

282
cocoa/inter/tunes.py Normal file
View File

@@ -0,0 +1,282 @@
# Taken from https://github.com/abarnert/itunesterms
version = 1.1
path = '/Applications/iTunes.app'
classes = \
[('print_settings', b'pset'),
('application', b'capp'),
('artwork', b'cArt'),
('audio_CD_playlist', b'cCDP'),
('audio_CD_track', b'cCDT'),
('browser_window', b'cBrW'),
('device_playlist', b'cDvP'),
('device_track', b'cDvT'),
('encoder', b'cEnc'),
('EQ_preset', b'cEQP'),
('EQ_window', b'cEQW'),
('file_track', b'cFlT'),
('folder_playlist', b'cFoP'),
('item', b'cobj'),
('library_playlist', b'cLiP'),
('playlist', b'cPly'),
('playlist_window', b'cPlW'),
('radio_tuner_playlist', b'cRTP'),
('shared_track', b'cShT'),
('source', b'cSrc'),
('track', b'cTrk'),
('URL_track', b'cURT'),
('user_playlist', b'cUsP'),
('visual', b'cVis'),
('window', b'cwin')]
enums = \
[('track_listing', b'kTrk'),
('album_listing', b'kAlb'),
('cd_insert', b'kCDi'),
('standard', b'lwst'),
('detailed', b'lwdt'),
('stopped', b'kPSS'),
('playing', b'kPSP'),
('paused', b'kPSp'),
('fast_forwarding', b'kPSF'),
('rewinding', b'kPSR'),
('off', b'kRpO'),
('one', b'kRp1'),
('all', b'kAll'),
('small', b'kVSS'),
('medium', b'kVSM'),
('large', b'kVSL'),
('library', b'kLib'),
('iPod', b'kPod'),
('audio_CD', b'kACD'),
('MP3_CD', b'kMCD'),
('device', b'kDev'),
('radio_tuner', b'kTun'),
('shared_library', b'kShd'),
('unknown', b'kUnk'),
('albums', b'kSrL'),
('artists', b'kSrR'),
('composers', b'kSrC'),
('displayed', b'kSrV'),
('songs', b'kSrS'),
('none', b'kNon'),
('Books', b'kSpA'),
('folder', b'kSpF'),
('Genius', b'kSpG'),
('iTunes_U', b'kSpU'),
('Library', b'kSpL'),
('Movies', b'kSpI'),
('Music', b'kSpZ'),
('Party_Shuffle', b'kSpS'),
('Podcasts', b'kSpP'),
('Purchased_Music', b'kSpM'),
('TV_Shows', b'kSpT'),
('movie', b'kVdM'),
('music_video', b'kVdV'),
('TV_show', b'kVdT'),
('user', b'kRtU'),
('computed', b'kRtC')]
properties = \
[('copies', b'lwcp'),
('collating', b'lwcl'),
('starting_page', b'lwfp'),
('ending_page', b'lwlp'),
('pages_across', b'lwla'),
('pages_down', b'lwld'),
('error_handling', b'lweh'),
('requested_print_time', b'lwqt'),
('printer_features', b'lwpf'),
('fax_number', b'faxn'),
('target_printer', b'trpr'),
('current_encoder', b'pEnc'),
('current_EQ_preset', b'pEQP'),
('current_playlist', b'pPla'),
('current_stream_title', b'pStT'),
('current_stream_URL', b'pStU'),
('current_track', b'pTrk'),
('current_visual', b'pVis'),
('EQ_enabled', b'pEQ '),
('fixed_indexing', b'pFix'),
('frontmost', b'pisf'),
('full_screen', b'pFSc'),
('name', b'pnam'),
('mute', b'pMut'),
('player_position', b'pPos'),
('player_state', b'pPlS'),
('selection', b'sele'),
('sound_volume', b'pVol'),
('version', b'vers'),
('visuals_enabled', b'pVsE'),
('visual_size', b'pVSz'),
('data', b'pPCT'),
('description', b'pDes'),
('downloaded', b'pDlA'),
('format', b'pFmt'),
('kind', b'pKnd'),
('raw_data', b'pRaw'),
('artist', b'pArt'),
('compilation', b'pAnt'),
('composer', b'pCmp'),
('disc_count', b'pDsC'),
('disc_number', b'pDsN'),
('genre', b'pGen'),
('year', b'pYr '),
('location', b'pLoc'),
('minimized', b'pMin'),
('view', b'pPly'),
('band_1', b'pEQ1'),
('band_2', b'pEQ2'),
('band_3', b'pEQ3'),
('band_4', b'pEQ4'),
('band_5', b'pEQ5'),
('band_6', b'pEQ6'),
('band_7', b'pEQ7'),
('band_8', b'pEQ8'),
('band_9', b'pEQ9'),
('band_10', b'pEQ0'),
('modifiable', b'pMod'),
('preamp', b'pEQA'),
('update_tracks', b'pUTC'),
('container', b'ctnr'),
('id', b'ID '),
('index', b'pidx'),
('persistent_ID', b'pPIS'),
('duration', b'pDur'),
('parent', b'pPlP'),
('shuffle', b'pShf'),
('size', b'pSiz'),
('song_repeat', b'pRpt'),
('special_kind', b'pSpK'),
('time', b'pTim'),
('visible', b'pvis'),
('capacity', b'capa'),
('free_space', b'frsp'),
('album', b'pAlb'),
('album_artist', b'pAlA'),
('album_rating', b'pAlR'),
('album_rating_kind', b'pARk'),
('bit_rate', b'pBRt'),
('bookmark', b'pBkt'),
('bookmarkable', b'pBkm'),
('bpm', b'pBPM'),
('category', b'pCat'),
('comment', b'pCmt'),
('database_ID', b'pDID'),
('date_added', b'pAdd'),
('enabled', b'enbl'),
('episode_ID', b'pEpD'),
('episode_number', b'pEpN'),
('EQ', b'pEQp'),
('finish', b'pStp'),
('gapless', b'pGpl'),
('grouping', b'pGrp'),
('long_description', b'pLds'),
('lyrics', b'pLyr'),
('modification_date', b'asmo'),
('played_count', b'pPlC'),
('played_date', b'pPlD'),
('podcast', b'pTPc'),
('rating', b'pRte'),
('rating_kind', b'pRtk'),
('release_date', b'pRlD'),
('sample_rate', b'pSRt'),
('season_number', b'pSeN'),
('shufflable', b'pSfa'),
('skipped_count', b'pSkC'),
('skipped_date', b'pSkD'),
('show', b'pShw'),
('sort_album', b'pSAl'),
('sort_artist', b'pSAr'),
('sort_album_artist', b'pSAA'),
('sort_name', b'pSNm'),
('sort_composer', b'pSCm'),
('sort_show', b'pSSN'),
('start', b'pStr'),
('track_count', b'pTrC'),
('track_number', b'pTrN'),
('unplayed', b'pUnp'),
('video_kind', b'pVdK'),
('volume_adjustment', b'pAdj'),
('address', b'pURL'),
('shared', b'pShr'),
('smart', b'pSmt'),
('bounds', b'pbnd'),
('closeable', b'hclb'),
('collapseable', b'pWSh'),
('collapsed', b'wshd'),
('position', b'ppos'),
('resizable', b'prsz'),
('zoomable', b'iszm'),
('zoomed', b'pzum')]
elements = \
[('artworks', b'cArt'),
('audio_CD_playlists', b'cCDP'),
('audio_CD_tracks', b'cCDT'),
('browser_windows', b'cBrW'),
('device_playlists', b'cDvP'),
('device_tracks', b'cDvT'),
('encoders', b'cEnc'),
('EQ_presets', b'cEQP'),
('EQ_windows', b'cEQW'),
('file_tracks', b'cFlT'),
('folder_playlists', b'cFoP'),
('items', b'cobj'),
('library_playlists', b'cLiP'),
('playlists', b'cPly'),
('playlist_windows', b'cPlW'),
('radio_tuner_playlists', b'cRTP'),
('shared_tracks', b'cShT'),
('sources', b'cSrc'),
('tracks', b'cTrk'),
('URL_tracks', b'cURT'),
('user_playlists', b'cUsP'),
('visuals', b'cVis'),
('windows', b'cwin'),
('application', b'capp'),
('print_settings', b'pset')]
commands = \
[('set', b'coresetd', [('to', b'data')]),
('exists', b'coredoex', []),
('move', b'coremove', [('to', b'insh')]),
('subscribe', b'hookpSub', []),
('playpause', b'hookPlPs', []),
('download', b'hookDwnl', []),
('close', b'coreclos', []),
('open', b'aevtodoc', []),
('open_location', b'GURLGURL', []),
('quit', b'aevtquit', []),
('pause', b'hookPaus', []),
('make',
'corecrel',
[('new', b'kocl'), ('at', b'insh'), ('with_properties', b'prdt')]),
('duplicate', b'coreclon', [('to', b'insh')]),
('print_',
'aevtpdoc',
[('print_dialog', b'pdlg'),
('with_properties', b'prdt'),
('kind', b'pKnd'),
('theme', b'pThm')]),
('add', b'hookAdd ', [('to', b'insh')]),
('rewind', b'hookRwnd', []),
('play', b'hookPlay', [('once', b'POne')]),
('run', b'aevtoapp', []),
('resume', b'hookResu', []),
('updatePodcast', b'hookUpd1', []),
('next_track', b'hookNext', []),
('stop', b'hookStop', []),
('search', b'hookSrch', [('for_', b'pTrm'), ('only', b'pAre')]),
('updateAllPodcasts', b'hookUpdp', []),
('update', b'hookUpdt', []),
('previous_track', b'hookPrev', []),
('fast_forward', b'hookFast', []),
('count', b'corecnte', [('each', b'kocl')]),
('reveal', b'hookRevl', []),
('convert', b'hookConv', []),
('eject', b'hookEjct', []),
('back_track', b'hookBack', []),
('refresh', b'hookRfrs', []),
('delete', b'coredelo', [])]

16
cocoa/me/AppDelegate.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 "AppDelegateBase.h"
#import "ResultWindow.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase {}
- (void)removeDeadTracks;
@end

68
cocoa/me/AppDelegate.m Normal file
View File

@@ -0,0 +1,68 @@
/*
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 "Utils.h"
#import "ValueTransformers.h"
#import "Dialogs.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
#import "Consts.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(3) forKey:@"scanType"];
[d setObject:i2n(80) forKey:@"minMatchPercentage"];
[d setObject:b2n(NO) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[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"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:4];
[i addIndex:5];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
_directoryPanel = nil;
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru_me/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanelME alloc] initWithParentApp:self];
}
- (void)removeDeadTracks
{
[(ResultWindow *)[self resultWindow] removeDeadTracks];
}
@end

11
cocoa/me/Consts.h Normal file
View File

@@ -0,0 +1,11 @@
/*
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 "../base/Consts.h"
#define jobScanDeadTracks @"jobScanDeadTracks"

13
cocoa/me/DetailsPanel.h Normal file
View File

@@ -0,0 +1,13 @@
/*
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 "DetailsPanelBase.h"
@interface DetailsPanel : DetailsPanelBase
@end

17
cocoa/me/DetailsPanel.m 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 "DetailsPanel.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
@end

16
cocoa/me/DirectoryPanel.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 "../base/DirectoryPanel.h"
@interface DirectoryPanelME : DirectoryPanel
{
}
- (IBAction)addiTunes:(id)sender;
@end

32
cocoa/me/DirectoryPanel.m Normal file
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 "DirectoryPanel.h"
@implementation DirectoryPanelME
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithParentApp:aParentApp];
_alwaysShowPopUp = YES;
return self;
}
- (void)fillPopUpMenu
{
[super fillPopUpMenu];
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m insertItemWithTitle:NSLocalizedString(@"Add iTunes Library", @"") action:@selector(addiTunes:)
keyEquivalent:@"" atIndex:1];
[mi setTarget:self];
}
- (IBAction)addiTunes:(id)sender
{
[self addDirectory:@"iTunes Library"];
}
@end

View File

@@ -0,0 +1,40 @@
<?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_me_help</string>
<key>CFBundleHelpBookName</key>
<string>dupeGuru ME Help</string>
<key>CFBundleIconFile</key>
<string>dupeguru</string>
<key>CFBundleIdentifier</key>
<string>com.hardcoded-software.dupeguru-me</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>dupeGuru ME</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>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</dict>
</plist>

14
cocoa/me/ResultWindow.h Normal file
View File

@@ -0,0 +1,14 @@
/*
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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
- (void)removeDeadTracks;
@end

77
cocoa/me/ResultWindow.m Normal file
View File

@@ -0,0 +1,77 @@
/*
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 "Dialogs.h"
#import "Utils.h"
#import "PyDupeGuru.h"
#import "Consts.h"
#import "ProgressController.h"
@implementation ResultWindow
/* Override */
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[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 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"])];
}
- (void)initResultColumns
{
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
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [matches tableColumnWithIdentifier:@"duration"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [matches tableColumnWithIdentifier:@"bitrate"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
/* Actions */
- (void)removeDeadTracks
{
[model scanDeadTracks];
}
@end

View File

@@ -10,7 +10,7 @@ install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import * from inter.all import *
from inter.app import PyDupeGuru from inter.app_me import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it # When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module. # to see the module.

BIN
cocoa/me/dupeguru.icns Executable file

Binary file not shown.

14
cocoa/pe/AppDelegate.h Normal file
View File

@@ -0,0 +1,14 @@
/*
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 "AppDelegateBase.h"
@interface AppDelegate : AppDelegateBase {}
- (void)clearPictureCache;
@end

61
cocoa/pe/AppDelegate.m Normal file
View File

@@ -0,0 +1,61 @@
/*
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 "Utils.h"
#import "ValueTransformers.h"
#import "Consts.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(0) forKey:@"scanType"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru_pe/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanelPE alloc] initWithParentApp:self];
}
- (DetailsPanel *)createDetailsPanel
{
return [[DetailsPanel alloc] initWithApp:model];
}
- (void)clearPictureCache
{
[(ResultWindow *)[self resultWindow] clearPictureCache];
}
@end

11
cocoa/pe/Consts.h Normal file
View File

@@ -0,0 +1,11 @@
/*
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 "../base/Consts.h"
#define ImageLoadedNotification @"ImageLoadedNotification"

View File

@@ -7,10 +7,10 @@ http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import "DetailsPanel.h" #import "DetailsPanelBase.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@interface DetailsPanelPicture : DetailsPanel @interface DetailsPanel : DetailsPanelBase
{ {
NSImageView *dupeImage; NSImageView *dupeImage;
NSProgressIndicator *dupeProgressIndicator; NSProgressIndicator *dupeProgressIndicator;

View File

@@ -10,11 +10,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "NSNotificationAdditions.h" #import "NSNotificationAdditions.h"
#import "NSImageAdditions.h" #import "NSImageAdditions.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
#import "DetailsPanelPicture.h" #import "DetailsPanel.h"
#import "Consts.h" #import "Consts.h"
#import "DetailsPanelPicture_UI.h" #import "DetailsPanel_UI.h"
@implementation DetailsPanelPicture @implementation DetailsPanel
@synthesize dupeImage; @synthesize dupeImage;
@synthesize dupeProgressIndicator; @synthesize dupeProgressIndicator;
@@ -32,7 +32,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (NSWindow *)createWindow - (NSWindow *)createWindow
{ {
return createDetailsPanelPicture_UI(self); return createDetailsPanel_UI(self);
} }
- (void)loadImageAsync:(NSString *)imagePath - (void)loadImageAsync:(NSString *)imagePath

17
cocoa/pe/DirectoryPanel.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 "../base/DirectoryPanel.h"
@interface DirectoryPanelPE : DirectoryPanel
{
}
- (IBAction)addiPhoto:(id)sender;
- (IBAction)addAperture:(id)sender;
@end

40
cocoa/pe/DirectoryPanel.m Normal file
View File

@@ -0,0 +1,40 @@
/*
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"
@implementation DirectoryPanelPE
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithParentApp:aParentApp];
_alwaysShowPopUp = YES;
return self;
}
- (void)fillPopUpMenu
{
[super fillPopUpMenu];
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m insertItemWithTitle:NSLocalizedString(@"Add iPhoto Library", @"") action:@selector(addiPhoto:)
keyEquivalent:@"" atIndex:1];
[mi setTarget:self];
mi = [m insertItemWithTitle:NSLocalizedString(@"Add Aperture Library", @"") action:@selector(addAperture:)
keyEquivalent:@"" atIndex:2];
[mi setTarget:self];
}
- (IBAction)addiPhoto:(id)sender
{
[self addDirectory:@"iPhoto Library"];
}
- (IBAction)addAperture:(id)sender
{
[self addDirectory:@"Aperture Library"];
}
@end

View File

@@ -0,0 +1,40 @@
<?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_pe_help</string>
<key>CFBundleHelpBookName</key>
<string>dupeGuru PE Help</string>
<key>CFBundleIconFile</key>
<string>dupeguru</string>
<key>CFBundleIdentifier</key>
<string>com.hardcoded-software.dupeguru-pe</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>dupeGuru PE</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>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</dict>
</plist>

14
cocoa/pe/ResultWindow.h Normal file
View File

@@ -0,0 +1,14 @@
/*
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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
- (void)clearPictureCache;
@end

58
cocoa/pe/ResultWindow.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 "ResultWindow.h"
#import "Dialogs.h"
#import "Utils.h"
#import "PyDupeGuru.h"
@implementation ResultWindow
/* Override */
- (void)initResultColumns
{
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
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
}
/* 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];
}
@end

17
cocoa/pe/dg_cocoa.py 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
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_pe 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/pe/dupeguru.icns Executable file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
ownerclass = 'DetailsPanelPicture' ownerclass = 'DetailsPanel'
ownerimport = 'DetailsPanelPicture.h' ownerimport = 'DetailsPanel.h'
result = Panel(593, 398, "Details of Selected File") result = Panel(593, 398, "Details of Selected File")
table = TableView(result) table = TableView(result)

14
cocoa/se/AppDelegate.h Normal file
View File

@@ -0,0 +1,14 @@
/*
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 "AppDelegateBase.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase {}
@end

52
cocoa/se/AppDelegate.m Normal file
View File

@@ -0,0 +1,52 @@
/*
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 "Utils.h"
#import "ValueTransformers.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
#import "Consts.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(1) forKey:@"scanType"];
[d setObject:i2n(80) 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"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
_directoryPanel = nil;
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
@end

13
cocoa/se/DetailsPanel.h Normal file
View File

@@ -0,0 +1,13 @@
/*
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 "DetailsPanelBase.h"
@interface DetailsPanel : DetailsPanelBase
@end

17
cocoa/se/DetailsPanel.m 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 "DetailsPanel.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
@end

View File

@@ -29,9 +29,9 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2016</string> <string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>https://www.hardcoded.net/updates/dupeguru.appcast</string> <string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string> <string>dsa_pub.pem</string>
</dict> </dict>

13
cocoa/se/ResultWindow.h Normal file
View File

@@ -0,0 +1,13 @@
/*
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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
@end

52
cocoa/se/ResultWindow.m Normal file
View File

@@ -0,0 +1,52 @@
/*
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 "Utils.h"
#import "Consts.h"
#import "PyDupeGuru.h"
@implementation ResultWindow
/* Override */
- (void)initResultColumns
{
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
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[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];
}
@end

17
cocoa/se/dg_cocoa.py 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
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_se 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/waf vendored

Binary file not shown.

View File

@@ -9,8 +9,13 @@ out = 'build'
def options(opt): def options(opt):
opt.load('compiler_c python') opt.load('compiler_c python')
opt.add_option('--edition', default='se', help="dupeGuru edition to build (se, me pe)")
def configure(conf): def configure(conf):
if conf.options.edition not in ('se', 'me', 'pe'):
conf.options.edition = 'se'
print("Building dupeGuru {}".format(conf.options.edition.upper()))
conf.env.DGEDITION = conf.options.edition
# We use clang to compile our app # We use clang to compile our app
conf.env.CC = 'clang' conf.env.CC = 'clang'
# WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking # WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking
@@ -21,18 +26,18 @@ def configure(conf):
# I did a lot of fiddling-around, but I didn't find how to tell WAF the Python library name # 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 # to look for without making the whole compilation process fail, so I just create a symlink
# with the name WAF is looking for. # with the name WAF is looking for.
versioned_dylib_path = '../build/libpython{}m.dylib'.format(sys.version[:3]) versioned_dylib_path = '../build/libpython{}.dylib'.format(sys.version[:3])
if not op.exists(versioned_dylib_path): if not op.exists(versioned_dylib_path):
os.symlink('../build/Python', 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. # The rest is standard WAF code that you can find the the python and macapp demos.
conf.load('compiler_c python') conf.load('compiler_c python')
conf.check_python_version((3,4,0)) conf.check_python_version((3,3,0))
conf.check_python_headers() conf.check_python_headers()
conf.env.FRAMEWORK_COCOA = 'Cocoa' conf.env.FRAMEWORK_COCOA = 'Cocoa'
conf.env.ARCH_COCOA = ['x86_64'] conf.env.ARCH_COCOA = ['x86_64']
conf.env.MACOSX_DEPLOYMENT_TARGET = '10.8' # Add cocoalib dir to the framework search path so we can find Sparkle.
conf.env.CFLAGS = ['-F'+op.abspath('Sparkle/build/Release')] conf.env.CFLAGS = ['-F'+op.abspath('../cocoalib')]
conf.env.LINKFLAGS = ['-F'+op.abspath('Sparkle/build/Release')] conf.env.LINKFLAGS = ['-F'+op.abspath('../cocoalib')]
def build(ctx): def build(ctx):
# What do we compile? # What do we compile?
@@ -48,8 +53,8 @@ def build(ctx):
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList', 'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
'controllers/HSTextField', 'controllers/HSProgressWindow'] 'controllers/HSTextField', 'controllers/HSProgressWindow']
cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m') 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_folders = ['autogen', 'base', ctx.env.DGEDITION]
project_src = ctx.srcnode.ant_glob('autogen/*.m') + ctx.srcnode.ant_glob('*.m') project_src = sum([ctx.srcnode.ant_glob('%s/*.m' % folder) for folder in project_folders], [])
# Compile # Compile
ctx.program( ctx.program(

Submodule cocoalib deleted from d059aa9b79

15
cocoalib/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
.DS_Store
__pycache__
autogen
*.pyc
*.so
nl.lproj
cs.lproj
de.lproj
fr.lproj
it.lproj
hy.lproj
ru.lproj
uk.lproj
zh_CN.lproj
pt_BR.lproj

8
cocoalib/.tx/config Normal file
View File

@@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[hscommon.cocoalib]
file_filter = locale/<lang>/LC_MESSAGES/cocoalib.po
source_file = locale/cocoalib.pot
source_lang = en
type = PO

14
cocoalib/Dialogs.h Normal file
View File

@@ -0,0 +1,14 @@
/*
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>
@interface Dialogs : NSObject
+ (void)showMessage:(NSString *)message;
+ (NSInteger)askYesNo:(NSString *)message;
@end

31
cocoalib/Dialogs.m 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 "Dialogs.h"
@implementation Dialogs
+ (void)showMessage:(NSString *)message
{
NSAlert *a = [[NSAlert alloc] init];
[a addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"cocoalib", @"")];
[a setMessageText:message];
[a runModal];
[a release];
}
+ (NSInteger)askYesNo:(NSString *)message
{
NSAlert *a = [[NSAlert alloc] init];
[a addButtonWithTitle:NSLocalizedStringFromTable(@"Yes", @"cocoalib", @"")];
[[a addButtonWithTitle:NSLocalizedStringFromTable(@"No", @"cocoalib", @"")] setKeyEquivalent:@"\E"];
[a setMessageText:message];
NSInteger r = [a runModal];
[a release];
return r;
}
@end

27
cocoalib/HSAboutBox.h Normal file
View File

@@ -0,0 +1,27 @@
/*
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 "PyBaseApp.h"
@interface HSAboutBox : NSWindowController
{
NSTextField *titleTextField;
NSTextField *versionTextField;
NSTextField *copyrightTextField;
PyBaseApp *app;
}
@property (readwrite, retain) NSTextField *titleTextField;
@property (readwrite, retain) NSTextField *versionTextField;
@property (readwrite, retain) NSTextField *copyrightTextField;
- (id)initWithApp:(PyBaseApp *)app;
- (void)updateFields;
@end

42
cocoalib/HSAboutBox.m Normal file
View File

@@ -0,0 +1,42 @@
/*
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 "HSAboutBox.h"
#import "HSAboutBox_UI.h"
@implementation HSAboutBox
@synthesize titleTextField;
@synthesize versionTextField;
@synthesize copyrightTextField;
- (id)initWithApp:(PyBaseApp *)aApp
{
self = [super initWithWindow:nil];
[self setWindow:createHSAboutBox_UI(self)];
app = [aApp retain];
[self updateFields];
return self;
}
- (void)dealloc
{
[app release];
[super dealloc];
}
- (void)updateFields
{
[titleTextField setStringValue:[app appLongName]];
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
[versionTextField setStringValue:[NSString stringWithFormat:@"Version: %@",version]];
NSString *copyright = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSHumanReadableCopyright"];
[copyrightTextField setStringValue:copyright];
}
@end

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>
@interface HSErrorReportWindow : NSWindowController
{
NSTextView *contentTextView;
NSString *githubUrl;
}
@property (readwrite, retain) NSTextView *contentTextView;
@property (readwrite, retain) NSString *githubUrl;
// True if the user wants to send the report
+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl;
- (id)initWithContent:(NSString *)content githubUrl:(NSString *)githubUrl;
- (void)goToGithub;
- (void)close;
@end

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 "HSErrorReportWindow.h"
#import "HSErrorReportWindow_UI.h"
@implementation HSErrorReportWindow
@synthesize contentTextView;
@synthesize githubUrl;
+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl
{
HSErrorReportWindow *report = [[HSErrorReportWindow alloc] initWithContent:content githubUrl:githubUrl];
[NSApp runModalForWindow:[report window]];
[report release];
}
- (id)initWithContent:(NSString *)content githubUrl:(NSString *)aGithubUrl
{
self = [super initWithWindow:nil];
[self setWindow:createHSErrorReportWindow_UI(self)];
[contentTextView alignLeft:nil];
[[[contentTextView textStorage] mutableString] setString:content];
self.githubUrl = aGithubUrl;
return self;
}
- (void)goToGithub
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:self.githubUrl]];
}
- (void)close
{
[[self window] orderOut:self];
[NSApp stopModalWithCode:NSOKButton];
}
@end

15
cocoalib/HSGeometry.h Normal file
View File

@@ -0,0 +1,15 @@
/*
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 <math.h>
CGFloat deg2rad(CGFloat deg);
CGFloat distance(NSPoint p1, NSPoint p2);
NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle);
CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2);

71
cocoalib/HSGeometry.m Normal file
View File

@@ -0,0 +1,71 @@
/*
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 "HSGeometry.h"
CGFloat deg2rad(CGFloat deg)
{
return deg * M_PI / 180;
}
CGFloat distance(NSPoint p1, NSPoint p2)
{
CGFloat dX = p1.x - p2.x;
CGFloat dY = p1.y - p2.y;
return sqrt(dX * dX + dY * dY);
}
NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle)
{
// a/sin(A) = b/sin(B) = c/sin(C) = 2R
// the start point it (center.x + radius, center.y) and goes counterclockwise
angle = fmod(angle, M_PI*2);
CGFloat C = M_PI/2;
CGFloat A = fmod(angle, M_PI/2);
CGFloat B = C - A;
CGFloat c = radius;
CGFloat ratio = c / sin(C);
CGFloat b = ratio * sin(B);
CGFloat a = ratio * sin(A);
if (angle >= M_PI * 1.5)
return NSMakePoint(center.x + a, center.y - b);
else if (angle >= M_PI)
return NSMakePoint(center.x - b, center.y - a);
else if (angle >= M_PI/2)
return NSMakePoint(center.x - a, center.y + b);
else
return NSMakePoint(center.x + b, center.y + a);
}
CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2)
{
// Returns the angle (radian) formed by the line pt1-pt2. The angle follows the same logic
// as in pointInCircle.
// What we do here is that we take the line and reduce it to fit a "unit circle" (circle with
// a radius of 1). Then, either asin(adjusted_dy) or acos(adjusted_dx) will give us our angle.
// We'll use asin(adjusted_dy).
CGFloat length = distance(pt1, pt2);
CGFloat dx = pt2.x - pt1.x;
CGFloat dy = pt2.y - pt1.y;
CGFloat ajdusted_dy = ABS(dy) / length;
CGFloat angle = asin(ajdusted_dy);
if ((dx < 0) && (dy >= 0)) {
// top-left quadrant
angle = M_PI - angle;
}
else if ((dx < 0) && (dy < 0)) {
// bottom-left quadrant
angle = M_PI + angle;
}
else if ((dx >= 0) && (dy < 0)) {
// bottom-right quadrant
angle = (2 * M_PI) - angle;
}
return angle;
}

13
cocoalib/HSPyUtil.h Normal file
View File

@@ -0,0 +1,13 @@
/*
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>
void setCocoaViewsModuleName(NSString *moduleName);
PyObject* createCallback(NSString *aViewClassName, id aViewRef);

Some files were not shown because too many files have changed in this diff Show More