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

Compare commits

..

3 Commits
4.0.3 ... 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
487 changed files with 48568 additions and 12208 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
__pycache__
*.pyc
*.so
*.mo
*.pyd
*.waf*
.lock-waf*
.idea
.tox
/tags
build
dist
install
installer_tmp-cache
env
/deps
cocoa/autogen
/run.py
/conf.json
/cocoa/*/Info.plist
/cocoa/*/build
/qt/*_rc.py
/qt/base/*_rc.py
/help/*/conf.py
/help/*/changelog.rst

9
.gitmodules vendored
View File

@@ -1,9 +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

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

143
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] 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
[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
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]
and let's get started.
### Slowed development
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
# Contents of this folder
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:
* 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.
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
* images: Images used by the different UI codebases.
@@ -58,113 +42,78 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu
* locale: .po files for localisation.
There are also other sub-folder that comes from external repositories and are part of this repo as
git submodules:
git subtrees:
* hscommon: A collection of helpers used across 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.
## 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
$ make run
./bootstrap.sh
The `Makefile` is a recent addition, however. You might have to fallback to the legacy build
scripts.
and follow instructions from the script. You can then ignore the rest of the build documentation.
### Legacy build
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 installation
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
to be installed manually.
* All systems: [Python 3.4+][python]
* Mac OS X: OS X 10.10+ with XCode command line tools.
* Linux: PyQt5
* All systems: [Python 3.3+][python] and [setuptools][setuptools]
* Mac OS X: The last XCode to have the 10.7 SDK included. Python 3.4+.
* 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:
$ 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
will not compile with a pyenv's python unless it's been built with `--enable-framework`. You can do
this with:
$ pacman -S python-pyqt5
$ env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.4.3
### 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.*
## Setting up the virtual environment
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.
For Linux (`--system-site-packages` is to be able to import PyQt):
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
manually:
$ pyvenv --system-site-packages env
$ 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
$ source env/bin/activate
$ pip install -r requirements-osx.txt
$ pip install -r requirements-[osx|win].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:
$ python configure.py
$ python build.py
$ python run.py
You can also package dupeGuru into an installable package with:
$ 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/
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
[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/
[python]: http://www.python.org/
[setuptools]: https://pypi.python.org/pypi/setuptools
[pyqt]: http://www.riverbankcomputing.com
[pyenv]: https://github.com/yyuu/pyenv
[tox]: https://tox.readthedocs.org/en/latest/
[cxfreeze]: http://cx-freeze.sourceforge.net/
[advinst]: http://www.advancedinstaller.com

View File

@@ -1,16 +1,12 @@
#!/bin/bash
PYTHON=python3
ret=`$PYTHON -c "import sys; print(int(sys.version_info[:2] >= (3, 4)))"`
if [ $ret -ne 1 ]; then
echo "Python 3.4+ required. Aborting."
exit 1
fi
command -v $PYTHON -m venv >/dev/null 2>&1 || { echo >&2 "Python 3.3 required. Install it and try again. Aborting"; exit 1; }
if [ -d ".git" ]; then
git submodule init
git submodule update
if [ -d "deps" ]; then
# We have a collection of dependencies in our source package. We might as well use it instead
# of downloading it from PyPI.
PIPARGS="--no-index --find-links=deps"
fi
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
# a system-site-packages once pip is installed.
if ! $PYTHON -m venv env ; then
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
exit 1
# We're probably under braindead Ubuntu 14.04 which completely messed up ensurepip.
# 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
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
$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
@@ -31,11 +43,11 @@ source env/bin/activate
echo "Installing pip requirements"
if [ "$(uname)" == "Darwin" ]; then
./env/bin/pip install -r requirements-osx.txt
pip install $PIPARGS -r requirements-osx.txt
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; }
./env/bin/pip install -r requirements.txt
python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.1+ required. Install it and try again. Aborting"; exit 1; }
pip install $PIPARGS -r requirements.txt
fi
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"

203
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,
# which should be included with this package. The terms are also available at
@@ -9,6 +11,8 @@ import os
import os.path as op
from optparse import OptionParser
import shutil
import json
import importlib
import compileall
from setuptools import setup, Extension
@@ -18,10 +22,10 @@ from hscommon.build import (
add_to_pythonpath, print_and_do, copy_packages, filereplace,
get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies
collect_stdlib_dependencies, copy
)
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
def parse_args():
@@ -35,14 +39,6 @@ def parse_args():
'--doc', action='store_true', dest='doc',
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(
'--loc', action='store_true', dest='loc',
help="Build only localization"
@@ -74,11 +70,18 @@ def parse_args():
(options, args) = parser.parse_args()
return options
def cocoa_app():
app_path = 'build/dupeGuru.app'
def cocoa_compile_command(edition):
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)
def build_xibless(dest='cocoa/autogen'):
def build_xibless(edition, dest='cocoa/autogen'):
import xibless
ensure_folder(dest)
FNPAIRS = [
@@ -89,51 +92,62 @@ def build_xibless(dest='cocoa/autogen'):
('prioritize_dialog.py', 'PrioritizeDialog_UI'),
('result_window.py', 'ResultWindow_UI'),
('main_menu.py', 'MainMenu_UI'),
('details_panel.py', 'DetailsPanel_UI'),
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
('preferences_panel.py', 'PreferencesPanel_UI'),
]
for srcname, dstname in FNPAIRS:
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'
)
for appmode in ('standard', 'music', 'picture'):
else:
xibless.generate(
op.join('cocoa', 'ui', 'preferences_panel.py'),
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()),
localizationTable='Localizable',
args={'appmode': appmode},
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
localizationTable='Localizable'
)
def build_cocoa(dev):
def build_cocoa(edition, dev):
print("Creating OS X app structure")
app = cocoa_app()
app_version = get_module_version('core')
cocoa_project_path = 'cocoa'
ed = lambda s: s.format(edition)
app = cocoa_app(edition)
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)
app.create(op.join('build', 'Info.plist'))
print("Building localizations")
build_localizations('cocoa')
build_localizations('cocoa', edition)
print("Building xibless UIs")
build_cocoalib_xibless()
build_xibless()
build_xibless(edition)
print("Building Python extensions")
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
build_cocoa_bridging_interfaces(edition)
print("Building the cocoa layer")
copy_embeddable_python_dylib('build')
pydep_folder = op.join(app.resources, 'py')
if not op.exists(pydep_folder):
os.mkdir(pydep_folder)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
appscript_pkgs = ['appscript', 'aem', 'mactypes', 'osax']
specific_packages = {
'se': ['core_se'],
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
'pe': ['core_pe'] + appscript_pkgs,
}[edition]
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)
sys.path.insert(0, 'build')
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
# to manually specify it.
extra_deps = ['multiprocessing']
extra_deps = None
if edition == 'pe':
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
# to manually specify it.
extra_deps = ['multiprocessing']
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
del sys.path[0]
# Views are not referenced by python code, so they're not found by the collector.
@@ -146,38 +160,40 @@ def build_cocoa(dev):
delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF")
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
print_and_do(cocoa_compile_command(edition))
os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru')
build_help()
build_help(edition)
print("Copying resources and frameworks")
image_path = 'cocoa/dupeguru.icns'
resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
image_path = ed('cocoa/{}/dupeguru.icns')
resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python')
app.copy_frameworks('build/Python', 'cocoalib/Sparkle.framework')
print("Creating the run.py file")
tmpl = open('cocoa/run_template.py', 'rt').read()
run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents)
def build_qt(dev):
def build_qt(edition, dev, conf):
print("Building localizations")
build_localizations('qt')
build_localizations('qt', edition)
print("Building Qt stuff")
print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'dg.qrc'), op.join('qt', 'dg_rc.py')))
fix_qt_resource_file(op.join('qt', 'dg_rc.py'))
build_help()
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', 'base', 'dg_rc.py'))
build_help(edition)
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")
current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help')
changelog_path = op.join(current_path, 'help', 'changelog')
help_destpath = op.join(current_path, 'build', 'help'.format(edition))
changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition))
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')
conftmpl = op.join(current_path, 'help', 'conf.tmpl')
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
@@ -186,11 +202,11 @@ def build_qt_localizations():
loc.compile_all_po(op.join('qtlib', 'locale'))
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
def build_localizations(ui):
def build_localizations(ui, edition):
loc.compile_all_po('locale')
if ui == 'cocoa':
app = cocoa_app()
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings'))
app = cocoa_app(edition)
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale')
elif ui == 'qt':
build_qt_localizations()
@@ -198,19 +214,32 @@ def build_localizations(ui):
if op.exists(locale_dest):
shutil.rmtree(locale_dest)
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():
if ISOSX:
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')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
build_xibless()
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj')
for edition in ('se', 'me', 'pe'):
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 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")
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")
# 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.
@@ -225,7 +254,7 @@ def build_updatepot():
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
print("Enhancing ui.pot with Cocoa's strings files")
loc.strings2pot(
op.join('cocoa', 'en.lproj', 'Localizable.strings'),
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
op.join('locale', 'ui.pot')
)
@@ -254,7 +283,7 @@ def build_cocoa_proxy_module():
['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces():
def build_cocoa_bridging_interfaces(edition):
print("Building Cocoa Bridging Interfaces")
import objp.o2p
import objp.p2o
@@ -274,12 +303,13 @@ def build_cocoa_bridging_interfaces():
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuru, DupeGuruView
from inter.app import PyDupeGuruBase, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition))
allclasses = [
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru,
PyTextField, PyProgressWindow
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
PyTextField, PyProgressWindow, appmod.PyDupeGuru
]
for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
@@ -296,21 +326,14 @@ def build_cocoa_bridging_interfaces():
def build_pe_modules(ui):
print("Building PE Modules")
exts = [
Extension(
"_block",
[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')]
),
Extension("_block", [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':
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
elif ui == 'cocoa':
exts.append(Extension(
"_block_osx",
[op.join('core', 'pe', 'modules', 'block_osx.m'), op.join('core', 'pe', 'modules', 'common.c')],
"_block_osx", [op.join('core_pe', 'modules', 'block_osx.m'), op.join('core_pe', 'modules', 'common.c')],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
@@ -322,25 +345,27 @@ def build_pe_modules(ui):
ext_modules=exts,
)
move_all('_block_qt*', op.join('qt', 'pe'))
move_all('_block*', op.join('core', 'pe'))
move_all('_cache*', op.join('core', 'pe'))
move_all('_block*', 'core_pe')
move_all('_cache*', 'core_pe')
def build_normal(ui, dev):
print("Building dupeGuru with UI {}".format(ui))
def build_normal(edition, ui, dev, conf):
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
add_to_pythonpath('.')
print("Building dupeGuru")
build_pe_modules(ui)
if edition == 'pe':
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(dev)
build_cocoa(edition, dev)
elif ui == 'qt':
build_qt(dev)
build_qt(edition, dev, conf)
def main():
options = parse_args()
ui = options.ui
if ui not in ('cocoa', 'qt'):
ui = 'cocoa' if ISOSX else 'qt'
if options.dev:
conf = json.load(open('conf.json'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
if dev:
print("Building in Dev mode")
if options.clean:
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
@@ -349,9 +374,9 @@ def main():
if not op.exists('build'):
os.mkdir('build')
if options.doc:
build_help()
build_help(edition)
elif options.loc:
build_localizations(ui)
build_localizations(ui, edition)
elif options.updatepot:
build_updatepot()
elif options.mergepot:
@@ -360,17 +385,17 @@ def main():
build_normpo()
elif options.cocoa_ext:
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
build_cocoa_bridging_interfaces(edition)
elif options.cocoa_compile:
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
print_and_do(cocoa_compile_command(edition))
os.chdir('..')
cocoa_app().copy_executable('cocoa/build/dupeGuru')
cocoa_app(edition).copy_executable('cocoa/build/dupeGuru')
elif options.xibless:
build_cocoalib_xibless()
build_xibless()
build_xibless(edition)
else:
build_normal(ui, options.dev)
build_normal(edition, ui, dev, conf)
if __name__ == '__main__':
main()

View File

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

View File

@@ -6,41 +6,24 @@ 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 "AppDelegateBase.h"
#import "ProgressController.h"
#import "HSPyUtil.h"
#import "Consts.h"
#import "Dialogs.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "DetailsPanelPicture.h"
#import "PreferencesPanelStandard_UI.h"
#import "PreferencesPanelMusic_UI.h"
#import "PreferencesPanelPicture_UI.h"
#import "PreferencesPanel_UI.h"
@implementation AppDelegate
@implementation AppDelegateBase
@synthesize recentResultsMenu;
@synthesize columnsMenu;
@synthesize updater;
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionary];
[d setObject:i2n(1) forKey:@"scanTypeStandard"];
[d setObject:i2n(3) forKey:@"scanTypeMusic"];
[d setObject:i2n(0) forKey:@"scanTypePicture"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
[d setObject:b2n(YES) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
[d setObject:b2n(NO) forKey:@"scanTagYear"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
[d setObject:i2n(1) forKey:@"recreatePathType"];
[d setObject:i2n(11) forKey:TableFontSize];
[d setObject:b2n(YES) forKey:@"mixFileKind"];
@@ -69,19 +52,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
self = [super init];
model = [[PyDupeGuru alloc] init];
[model bindCallback:createCallback(@"DupeGuruView", self)];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
i = [NSMutableIndexSet indexSetWithIndex:4];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
[self setUpdater:[SUUpdater sharedUpdater]];
return self;
}
@@ -90,19 +61,22 @@ http://www.gnu.org/licenses/gpl-3.0.html
// We can only finalize initialization once the main menu has been created, which cannot happen
// before AppDelegate is created.
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
/* Because the pref pane is lazily loaded, we have to manually do the update check if the
preference is set.
*/
if ([ud boolForKey:@"SUEnableAutomaticChecks"]) {
[[SUUpdater sharedUpdater] checkForUpdatesInBackground];
}
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self];
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
_resultWindow = [self createResultWindow];
_directoryPanel = [self createDirectoryPanel];
_detailsPanel = [self createDetailsPanel];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
_problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
_deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_directoryPanel window]];
// Lazily loaded
_aboutBox = nil;
_preferencesPanel = nil;
_resultWindow = nil;
_detailsPanel = nil;
[_progressWindow setParentWindow:[_resultWindow window]];
_aboutBox = nil; // Lazily loaded
_preferencesPanel = nil; // Lazily loaded
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
}
@@ -113,51 +87,28 @@ http://www.gnu.org/licenses/gpl-3.0.html
return model;
}
- (DetailsPanel *)createDetailsPanel
- (ResultWindowBase *)createResultWindow
{
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
return [[DetailsPanelPicture alloc] initWithApp:model];
}
else {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
return nil; // must be overriden by all editions
}
- (void)setScanOptions
- (DirectoryPanel *)createDirectoryPanel
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *scanTypeOptionName;
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
scanTypeOptionName = @"scanTypePicture";
}
else if (appMode == AppModeMusic) {
scanTypeOptionName = @"scanTypeMusic";
}
else {
scanTypeOptionName = @"scanTypeStandard";
}
[model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[model setSizeThreshold:sizeThreshold];
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
return [[DirectoryPanel alloc] initWithParentApp:self];
}
- (DetailsPanel *)createDetailsPanel
{
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
- (NSString *)homepageURL
{
return @""; // must be overriden by all editions
}
/* Public */
- (ResultWindow *)resultWindow
- (ResultWindowBase *)resultWindow
{
return _resultWindow;
}
@@ -177,29 +128,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
return _recentResults;
}
- (NSInteger)getAppMode
{
return [model getAppMode];
}
- (void)setAppMode:(NSInteger)appMode
{
[model setAppMode:appMode];
if (_preferencesPanel != nil) {
[_preferencesPanel release];
_preferencesPanel = nil;
}
}
/* Actions */
- (void)clearPictureCache
{
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[model clearPictureCache];
}
- (void)loadResults
{
NSOpenPanel *op = [NSOpenPanel openPanel];
@@ -218,7 +147,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)openWebsite
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self homepageURL]]];
}
- (void)openHelp
@@ -245,18 +174,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)showPreferencesPanel
{
if (_preferencesPanel == nil) {
NSWindow *window;
NSInteger appMode = [model getAppMode];
if (appMode == AppModePicture) {
window = createPreferencesPanelPicture_UI(nil);
}
else if (appMode == AppModeMusic) {
window = createPreferencesPanelMusic_UI(nil);
}
else {
window = createPreferencesPanelStandard_UI(nil);
}
_preferencesPanel = [[NSWindowController alloc] initWithWindow:window];
_preferencesPanel = [[NSWindowController alloc] initWithWindow:createPreferencesPanel_UI(nil)];
}
[_preferencesPanel showWindow:nil];
}
@@ -273,7 +191,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)startScanning
{
[[self directoryPanel] startDuplicateScan];
[[self resultWindow] startDuplicateScan];
}
@@ -336,29 +254,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
}
- (void)createResultsWindow
{
if (_resultWindow != nil) {
[_resultWindow release];
}
if (_detailsPanel != nil) {
[_detailsPanel release];
}
// Warning: creation order is important
// If the details panel is not created first and that there are some results in the model
// (happens if we load results), a dupe selection event triggers a details refresh in the
// core before we have the chance to initialize it, and then we crash.
_detailsPanel = [self createDetailsPanel];
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
}
- (void)showResultsWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showProblemDialog
{
[_problemDialog showWindow:self];
[[self resultWindow] showProblemDialog];
}
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt

View File

@@ -17,8 +17,3 @@ http://www.gnu.org/licenses/gpl-3.0.html
#define jobDelete @"job_delete"
#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 "PyDetailsPanel.h"
@interface DetailsPanel : NSWindowController <NSTableViewDataSource>
@interface DetailsPanelBase : NSWindowController <NSTableViewDataSource>
{
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
*/
#import "DetailsPanel.h"
#import "DetailsPanelBase.h"
#import "HSPyUtil.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
@implementation DetailsPanelBase
@synthesize detailsTable;
@@ -36,7 +35,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
return nil; // Virtual
}
- (void)refreshDetails

View File

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

View File

@@ -11,27 +11,22 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "Dialogs.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
@implementation DirectoryPanel
@synthesize appModeSelector;
@synthesize scanTypePopup;
@synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp;
@synthesize outlineView;
@synthesize removeButton;
@synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp
- (id)initWithParentApp:(AppDelegateBase *)aParentApp
{
self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp;
model = [_app model];
[[self window] setTitle:[model appName]];
self.appModeSelector.selectedSegment = 0;
[self fillScanTypeMenu];
_alwaysShowPopUp = NO;
[self fillPopUpMenu];
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
@@ -64,25 +59,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
[m addItem:[NSMenuItem separatorItem]];
}
- (void)fillScanTypeMenu
{
[[self scanTypePopup] unbind:@"selectedIndex"];
[[self scanTypePopup] removeAllItems];
[[self scanTypePopup] addItemsWithTitles:[[_app model] getScanOptions]];
NSString *keypath;
NSInteger appMode = [_app getAppMode];
if (appMode == AppModePicture) {
keypath = @"values.scanTypePicture";
}
else if (appMode == AppModeMusic) {
keypath = @"values.scanTypeMusic";
}
else {
keypath = @"values.scanTypeStandard";
}
[[self scanTypePopup] bind:@"selectedIndex" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:keypath options:nil];
}
- (void)adjustUIToLocalization
{
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
@@ -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
{
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
@@ -175,16 +134,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
[self refreshRemoveButtonText];
}
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[_app setScanOptions];
[model doScan];
}
/* Public */
- (void)addDirectory:(NSString *)directory
{

View File

@@ -10,12 +10,14 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import <Quartz/Quartz.h>
#import "StatsLabel.h"
#import "ResultTable.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSTableView.h"
#import "PyDupeGuru.h"
@class AppDelegate;
@class AppDelegateBase;
@interface ResultWindow : NSWindowController
@interface ResultWindowBase : NSWindowController
{
@protected
NSSegmentedControl *optionsSwitch;
@@ -24,10 +26,12 @@ http://www.gnu.org/licenses/gpl-3.0.html
NSTextField *stats;
NSSearchField *filterField;
AppDelegate *app;
AppDelegateBase *app;
PyDupeGuru *model;
ResultTable *table;
StatsLabel *statsLabel;
ProblemDialog *problemDialog;
DeletionOptions *deletionOptions;
QLPreviewPanel* previewPanel;
}
@@ -37,13 +41,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
@property (readwrite, retain) NSTextField *stats;
@property (readwrite, retain) NSSearchField *filterField;
- (id)initWithParentApp:(AppDelegate *)app;
- (id)initWithParentApp:(AppDelegateBase *)app;
/* Virtual */
- (void)initResultColumns;
- (void)setScanOptions;
/* Helpers */
- (void)fillColumnsMenu;
- (void)updateOptionSegments;
- (void)showProblemDialog;
- (void)adjustUIToLocalization;
- (void)initResultColumns:(ResultTable *)aTable;
/* Actions */
- (void)changeOptions;
@@ -67,6 +75,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (void)resetColumnsToDefault;
- (void)revealSelected;
- (void)saveResults;
- (void)startDuplicateScan;
- (void)switchSelected;
- (void)toggleColumn:(id)sender;
- (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
*/
#import "ResultWindow.h"
#import "ResultWindowBase.h"
#import "ResultWindow_UI.h"
#import "Dialogs.h"
#import "ProgressController.h"
@@ -15,7 +15,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "Consts.h"
#import "PrioritizeDialog.h"
@implementation ResultWindow
@implementation ResultWindowBase
@synthesize optionsSwitch;
@synthesize optionsToolbarItem;
@@ -23,7 +23,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
@synthesize stats;
@synthesize filterField;
- (id)initWithParentApp:(AppDelegate *)aApp;
- (id)initWithParentApp:(AppDelegateBase *)aApp;
{
self = [super initWithWindow:nil];
app = aApp;
@@ -34,7 +34,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
[self initResultColumns:table];
problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
[self initResultColumns];
[[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu];
[matches setTarget:self];
@@ -47,9 +49,19 @@ http://www.gnu.org/licenses/gpl-3.0.html
{
[table release];
[statsLabel release];
[problemDialog release];
[super dealloc];
}
/* Virtual */
- (void)initResultColumns
{
}
- (void)setScanOptions
{
}
/* Helpers */
- (void)fillColumnsMenu
{
@@ -77,6 +89,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
}
- (void)showProblemDialog
{
[problemDialog showWindow:self];
}
- (void)adjustUIToLocalization
{
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 */
- (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
{
[model makeSelectedReference];

View File

@@ -3,15 +3,18 @@
"About dupeGuru" = "About dupeGuru";
"Action" = "Action";
"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 iPhoto Library" = "Add iPhoto Library";
"Add iTunes Library" = "Add iTunes Library";
"Add New Folder..." = "Add New Folder...";
"Add Selected to Ignore List" = "Add Selected to Ignore List";
"Advanced" = "Advanced";
"After having deleted a duplicate, place a link targeting the reference file to replace the deleted file." = "After having deleted a duplicate, place a link targeting the reference file to replace the deleted file.";
"Album" = "Album";
"Application Mode:" = "Application Mode:";
"Artist" = "Artist";
"Attribute" = "Attribute";
"Audio Content" = "Audio Content";
"Automatically check for updates" = "Automatically check for updates";
"Basic" = "Basic";
"Bring All to Front" = "Bring All to Front";
@@ -23,6 +26,8 @@
"Close" = "Close";
"Close Window" = "Close Window";
"Columns" = "Columns";
"Content" = "Content";
"Contents" = "Contents";
"Copy" = "Copy";
"Copy and Move:" = "Copy and Move:";
"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?";
"dupeGuru" = "dupeGuru";
"dupeGuru Help" = "dupeGuru Help";
"dupeGuru ME Preferences" = "dupeGuru ME Preferences";
"dupeGuru PE Preferences" = "dupeGuru PE Preferences";
"dupeGuru Preferences" = "dupeGuru Preferences";
"dupeGuru Results" = "dupeGuru Results";
"dupeGuru Website" = "dupeGuru Website";
"Dupes Only" = "Dupes Only";
"Edit" = "Edit";
"Excluded" = "Excluded";
"EXIF Timestamp" = "EXIF Timestamp";
"Export Results to CSV" = "Export Results to CSV";
"Export Results to XHTML" = "Export Results to XHTML";
"Fewer results" = "Fewer results";
"File" = "File";
"Filename" = "Filename";
"Filename - Fields" = "Filename - Fields";
"Filename - Fields (No Order)" = "Filename - Fields (No Order)";
"Filter" = "Filter";
"Filter hardness:" = "Filter hardness:";
"Filter Results..." = "Filter Results...";
"Folder Selection Window" = "Folder Selection Window";
"Folders" = "Folders";
"Font Size:" = "Font Size:";
"Genre" = "Genre";
"Help" = "Help";
@@ -80,14 +92,12 @@
"Mode" = "Mode";
"More results" = "More results";
"Move Marked to..." = "Move Marked to...";
"Music" = "Music";
"Name" = "Name";
"Normal" = "Normal";
"Ok" = "Ok";
"Open Selected with Default Application" = "Open Selected with Default Application";
"Options" = "Options";
"Paste" = "Paste";
"Picture" = "Picture";
"Preferences..." = "Preferences...";
"Problems!" = "Problems!";
"Proceed" = "Proceed";
@@ -98,6 +108,7 @@
"Recreate absolute path" = "Recreate absolute path";
"Recreate relative path" = "Recreate relative path";
"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 Marked from Results" = "Remove Marked from Results";
"Remove Selected" = "Remove Selected";
@@ -123,9 +134,9 @@
"Show All" = "Show All";
"Show Delta Values" = "Show Delta Values";
"Show Dupes Only" = "Show Dupes Only";
"Standard" = "Standard";
"Start Duplicate Scan" = "Start Duplicate Scan";
"State" = "State";
"Tags" = "Tags";
"Tags to scan:" = "Tags to scan:";
"The name '%@' already exists." = "The name '%@' already exists.";
"There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results." = "There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results.";

View File

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

View File

@@ -1,5 +1,6 @@
ownerclass = 'AppDelegate'
ownerimport = 'AppDelegate.h'
ownerclass = 'AppDelegateBase'
ownerimport = 'AppDelegateBase.h'
edition = args.get('edition', 'se')
result = Menu("")
appMenu = result.addMenu("dupeGuru")
@@ -12,6 +13,7 @@ windowMenu = result.addMenu("Window")
helpMenu = result.addMenu("Help")
appMenu.addItem("About dupeGuru", Action(owner, 'showAboutBox'))
appMenu.addItem("Check for update...", Action(owner.updater, 'checkForUpdates:'))
appMenu.addSeparator()
appMenu.addItem("Preferences...", Action(owner, 'showPreferencesPanel'), 'cmd+,')
appMenu.addSeparator()
@@ -28,7 +30,10 @@ owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s')
fileMenu.addItem("Export Results to XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e')
fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV'))
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
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 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 = {
'standard': 325,
'music': 345,
'picture': 255,
'se': 345,
'me': 365,
'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)
basicTab = tabView.addTab("Basic")
advancedTab = tabView.addTab("Advanced")
scanTypePopup = Popup(basicTab.view, scanTypeNames[edition])
scanTypeLabel = Label(basicTab.view, "Scan Type:")
thresholdSlider = Slider(basicTab.view, 1, 100, 80)
thresholdLabel = Label(basicTab.view, "Filter hardness:")
moreResultsLabel = Label(basicTab.view, "More results")
@@ -16,19 +28,19 @@ fewerResultsLabel = Label(basicTab.view, "Fewer results")
thresholdValueLabel = Label(basicTab.view, "")
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
fontSizeLabel = Label(basicTab.view, "Font Size:")
if appmode in ('standard', 'music'):
if edition in ('se', 'me'):
wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
elif appmode == 'picture':
elif edition == 'pe':
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
if appmode == 'standard':
if edition == 'se':
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
smallFilesThresholdText = TextField(basicTab.view, "")
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
elif appmode == 'music':
elif edition == 'me':
tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
trackBox = Checkbox(basicTab.view, "Track")
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"])
resetToDefaultsButton = Button(result, "Reset To Defaults")
scanTypePopup.bind('selectedIndex', defaults, 'values.scanType')
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
@@ -58,61 +72,59 @@ ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
customCommandText.bind('value', defaults, 'values.CustomCommand')
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
if appmode in ('standard', 'music'):
if edition in ('se', 'me'):
wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
for control in disableWhenContentScan:
vtname = 'vtScanTypeMusicIsNotContent' if appmode == 'music' else 'vtScanTypeIsNotContent'
prefname = 'values.scanTypeMusic' if appmode == 'music' else 'values.scanTypeStandard'
control.bind('enabled', defaults, prefname, valueTransformer=vtname)
if appmode == 'standard':
control.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsNotContent')
if edition == 'se':
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
elif appmode == 'music':
elif edition == 'me':
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')
artistBox.bind('value', defaults, 'values.scanTagArtist')
albumBox.bind('value', defaults, 'values.scanTagAlbum')
titleBox.bind('value', defaults, 'values.scanTagTitle')
genreBox.bind('value', defaults, 'values.scanTagGenre')
yearBox.bind('value', defaults, 'values.scanTagYear')
elif appmode == 'picture':
elif edition == 'pe':
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.canMinimize = False
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
thresholdValueLabel.formatter.maximumFractionDigits = 0
allLabels = [thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
allLabels = [scanTypeLabel, thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
ignoreHardlinksBox, debugModeCheckbox]
if appmode == 'standard':
if edition == 'se':
allLabels += [smallFilesThresholdSuffixLabel]
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'music':
elif edition == 'me':
allLabels += [tagsToScanLabel]
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
elif appmode == 'picture':
elif edition == 'pe':
allCheckboxes += [matchDifferentDimensionsBox]
for label in allLabels:
label.controlSize = ControlSize.Small
fewerResultsLabel.alignment = TextAlignment.Right
for checkbox in allCheckboxes:
checkbox.font = thresholdValueLabel.font
checkbox.font = scanTypeLabel.font
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
thresholdLabel.width = fontSizeLabel.width = 94
scanTypeLabel.width = thresholdLabel.width = fontSizeLabel.width = 94
fontSizeCombo.width = 66
thresholdValueLabel.width = 25
resetToDefaultsButton.width = 136
if appmode == 'standard':
if edition == 'se':
smallFilesThresholdText.width = 60
smallFilesThresholdSuffixLabel.width = 40
elif appmode == 'music':
elif edition == 'me':
for box in tagBoxes:
box.width = 70
@@ -121,18 +133,22 @@ tabView.fill(Pack.Right)
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
tabView.fill(Pack.Below, margin=14)
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
thresholdLayout = HLayout([thresholdLabel, thresholdSlider, thresholdValueLabel], filler=thresholdSlider)
thresholdLayout.packToCorner(Pack.UpperLeft)
thresholdLayout.fill(Pack.Right)
scanTypePopup.packToCorner(Pack.UpperRight)
scanTypeLabel.packRelativeTo(scanTypePopup, Pack.Left)
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
# after the slider's fill operation.
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
thresholdLabel.packRelativeTo(thresholdSlider, Pack.Left)
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
if appmode == 'music':
if edition == 'me':
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
tagsToScanLabel.fill(Pack.Left)
tagsToScanLabel.fill(Pack.Right)
@@ -147,13 +163,13 @@ if appmode == 'music':
else:
viewToPackCheckboxesUnder = fontSizeCombo
if appmode == 'standard':
if edition == 'se':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
ignoreSmallFilesBox]
elif appmode == 'music':
elif edition == 'me':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
elif appmode == 'picture':
elif edition == 'pe':
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
checkboxLayout = VLayout(checkboxesToLayout)
@@ -161,7 +177,7 @@ checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
checkboxLayout.fill(Pack.Left)
checkboxLayout.fill(Pack.Right)
if appmode == 'standard':
if edition == 'se':
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
checkForUpdatesBox.fill(Pack.Right)

View File

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

View File

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

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,
# 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
import logging
import os.path as op
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_se.app import DupeGuru as DupeGuruBase
from core_se import fs
from .app import PyDupeGuruBase
def is_bundle(str_path):
uti = proxy.getUTI_(str_path)
@@ -20,11 +29,15 @@ class Bundle(fs.Folder):
@pathify
def can_handle(cls, path: Path):
return not path.islink() and path.isdir() and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def __init__(self):
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])
self.folderclass = fs.Folder
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
@@ -45,9 +58,47 @@ class Directories(DirectoriesBase):
yield from_folder
return
else:
yield from DirectoriesBase._get_folders(self, from_folder, j)
for folder in DirectoriesBase._get_folders(self, from_folder, j):
yield folder
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
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 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
# 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 "DetailsPanel.h"
#import "DetailsPanelBase.h"
#import "PyDupeGuru.h"
@interface DetailsPanelPicture : DetailsPanel
@interface DetailsPanel : DetailsPanelBase
{
NSImageView *dupeImage;
NSProgressIndicator *dupeProgressIndicator;

View File

@@ -10,11 +10,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
#import "NSNotificationAdditions.h"
#import "NSImageAdditions.h"
#import "PyDupeGuru.h"
#import "DetailsPanelPicture.h"
#import "DetailsPanel.h"
#import "Consts.h"
#import "DetailsPanelPicture_UI.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanelPicture
@implementation DetailsPanel
@synthesize dupeImage;
@synthesize dupeProgressIndicator;
@@ -32,7 +32,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
- (NSWindow *)createWindow
{
return createDetailsPanelPicture_UI(self);
return createDetailsPanel_UI(self);
}
- (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'
ownerimport = 'DetailsPanelPicture.h'
ownerclass = 'DetailsPanel'
ownerimport = 'DetailsPanel.h'
result = Panel(593, 398, "Details of Selected File")
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>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2016</string>
<string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key>
<string>https://www.hardcoded.net/updates/dupeguru.appcast</string>
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</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):
opt.load('compiler_c python')
opt.add_option('--edition', default='se', help="dupeGuru edition to build (se, me pe)")
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
conf.env.CC = 'clang'
# WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking
@@ -21,16 +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
# to look for without making the whole compilation process fail, so I just create a symlink
# with the name WAF is looking for.
versioned_dylib_path = '../build/libpython{}m.dylib'.format(sys.version[:3])
versioned_dylib_path = '../build/libpython{}.dylib'.format(sys.version[:3])
if not op.exists(versioned_dylib_path):
os.symlink('../build/Python', versioned_dylib_path)
# The rest is standard WAF code that you can find the the python and macapp demos.
conf.load('compiler_c python')
conf.check_python_version((3,4,0))
conf.check_python_version((3,3,0))
conf.check_python_headers()
conf.env.FRAMEWORK_COCOA = 'Cocoa'
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('../cocoalib')]
conf.env.LINKFLAGS = ['-F'+op.abspath('../cocoalib')]
def build(ctx):
# What do we compile?
@@ -46,8 +53,8 @@ def build(ctx):
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
'controllers/HSTextField', 'controllers/HSProgressWindow']
cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m')
project_folders = [ctx.srcnode, ctx.srcnode.find_dir('autogen')]
project_src = ctx.srcnode.ant_glob('autogen/*.m') + ctx.srcnode.ant_glob('*.m')
project_folders = ['autogen', 'base', ctx.env.DGEDITION]
project_src = sum([ctx.srcnode.ant_glob('%s/*.m' % folder) for folder in project_folders], [])
# Compile
ctx.program(
@@ -60,7 +67,7 @@ def build(ctx):
# Because our python lib's install name is "@rpath/Python", we need to set the executable's
# rpath. Fortunately, WAF supports it and we just need to supply the "rpath" argument.
rpath = '@executable_path/../Frameworks',
framework = ['Quartz'],
framework = ['Sparkle', 'Quartz'],
)
from waflib import TaskGen

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);

34
cocoalib/HSPyUtil.m Normal file
View File

@@ -0,0 +1,34 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "HSPyUtil.h"
#import "ObjP.h"
static NSString *gCocoaViewsModuleName;
void setCocoaViewsModuleName(NSString *moduleName)
{
if (gCocoaViewsModuleName != nil) {
[gCocoaViewsModuleName release];
}
gCocoaViewsModuleName = [moduleName retain];
}
PyObject* createCallback(NSString *aViewClassName, id aViewRef)
{
NSString *moduleName;
if (gCocoaViewsModuleName != nil) {
moduleName = gCocoaViewsModuleName;
}
else {
moduleName = @"inter.CocoaViews";
}
PyGILState_STATE gilState = PyGILState_Ensure();
PyObject *pCallback = ObjP_classInstanceWithRef(aViewClassName, moduleName, aViewRef);
PyGILState_Release(gilState);
return pCallback;
}

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